Commit Graph

18 Commits

Author SHA1 Message Date
Tom Boucher
c0fd2e3f4c feat(#3673): add dispatch.maxConcurrency axis and dispatch-capacity query (#4162)
* test(#3673): add failing tests for dispatch.maxConcurrency axis and dispatch-capacity CLI route

Extends tests/host-integration.test.cjs with negotiateHostCapabilities
maxConcurrency negotiation coverage (test matrix rows 1-15, including a
fast-check property test) and a new #3673 dispatch-capacity CLI route
describe block spawning the real gsd-tools.cjs (rows 16-25). Extends
tests/host-integration-validator-parity.test.cjs with an all-19-descriptor
maxConcurrency presence/validity sweep (row 26) and adds a hostile-input
validator test to host-integration.test.cjs (row 27).

The dispatch.maxConcurrency field does not exist yet, so these tests fail.

* feat(#3673): add dispatch.maxConcurrency axis, negotiation, validator parity, and the dispatch-capacity query

Adds a numeric dispatch.maxConcurrency sub-field to the Host-Integration
Interface (ADR-1239 Phase 1), following the existing dispatch.isolation
sub-field pattern: DispatchCapability interface, SAFE_DEFAULTS/PROFILE_BASELINES
floors, and a negotiateHostCapabilities branch that passes through a positive
safe integer and fails closed to 1 otherwise (no engine-side reduction, per
the design doc's explicit rejection of a min(host,engine) rule).

capability-validator.cjs gains parity validation for the new field (optional,
positive safe integer or the "undocumented" sentinel — mirroring isolation's
"added after existing descriptors" treatment).

gsd-tools.cjs gains a new `query dispatch-capacity` route, a pure-read sibling
of `dispatch-isolation` with no side effects: live env
(GSD_DISPATCH_MAX_CONCURRENCY) > descriptor > fallback-to-1 precedence.

All 19 capabilities/*/capability.json descriptors now declare
dispatch.maxConcurrency: claude carries the one cited value (20, per
code.claude.com/docs/en/sub-agents); the other 18 carry "undocumented"
(not yet researched for this axis).

* docs(#3673): document dispatch.maxConcurrency and add its citation row to the capability matrix

Updates docs/reference/host-integration-interface.md's dispatch struct entry
(also backfilling the previously-undocumented isolation/backgroundDispatch
sub-fields found stale in the same table) and adds fail-closed/live-transport
precedence prose for the new maxConcurrency field.

Adds a dispatch.maxConcurrency row (with citation) to all 19 host sections in
docs/reference/host-integration-capability-matrix.md — required for
tests/host-integration-descriptors.test.cjs's kimi-code matrix-parity check,
which asserts every declared dispatch sub-axis is documented there.

Updates docs/how-to/add-or-update-a-host-integration.md's dispatch checklist
and example descriptor block to mention maxConcurrency (and, likewise
backfilling a stale gap, isolation/backgroundDispatch).

* fix(#3673): extract shared maxConcurrency validator, drop dead reserved-name check

Exports isPositiveSafeInteger from src/host-integration.cts as the single
source of truth for the dispatch.maxConcurrency positive-safe-integer
contract; negotiateHostCapabilities and gsd-tools.cjs's routeDispatchCapacity
now both call it instead of independently reimplementing the same predicate.

Also removes the __proto__/constructor/prototype reserved-name branch from
capability-validator.cjs's maxConcurrency check — copy-pasted from the
string-enum fields above it, but unreachable for a numeric field (the
generic positive-safe-integer branch already rejects any string) and absent
from maxDepth, the field the code's own comment claims to mirror.

---------

Co-authored-by: sim <sim@local>
2026-09-01 21:39:12 -04:00
Tom Boucher
a3d5841117 fix(#3714): deliver an explicitly pinned model to the Codex worktree executor, and drop an unusable one (#3891)
* test(#3714): failing-first coverage for the dropped Codex worktree model override

Pins the argv contract for the orchestrator-worktree process dispatch: an explicit
model_overrides pin must reach the child as --model, while an unpinned, empty,
inherit, or profile-only configuration must emit no flag at all.

Five of the eight matrix rows are CONTROLS that pass before the fix. They carry the
weight here because this is an over-emission bug waiting to happen: resolve-model
returns 'sonnet' for the unpinned, empty and profile-only cases, so a fix that
threads its return value into argv would satisfy the positive row and emit
--model sonnet to Codex on every unpinned install -- the documented 400 that
ADR-2313 exists to prevent. The controls are what separate the correct fix from
the obvious one.

* fix(#3714): deliver an explicitly pinned model to the Codex worktree executor

resolveOrchestratorExec had no model input at all -- the descriptor carried only
command/args/cwdFlag/promptFlag -- so a resolved override had no way to reach the
spawned process even in principle. The baked gsd-executor.toml could not compensate
because this path spawns a process rather than dispatching a named agent.

Three parts, and the third is the load-bearing one.

The descriptor gains modelFlag (codex: --model), keeping the per-host knowledge as
descriptor data exactly as cwdFlag and promptFlag already are, so the scheduler
grows no per-host branch. No other runtime declares it.

The seam appends [modelFlag, model] and stays MECHANICAL: it does not know the
inherit sentinel, does not know which models Codex rejects, and reads no config.
Its sibling codex-agent-toml states that rule outright -- callers decide what to
strip. Argv order is baseArgs, model, cwd, prompt so the prompt remains the final
positional token. Omitting the model is byte-identical to before. A model starting
with '-' now fails closed as unsafe_leading_dash_model, the same hazard the prompt
and cwd guards already reject and which was silently accepted before.

The policy lives at the caller and passes ONLY an explicit, non-sentinel per-agent
pin. Passing null as the runtime resolver is what keeps profile and tier derived
models out of argv, which is what Codex's session-only model posture requires: the
model resolver returns 'sonnet' for the unpinned, empty and profile-only cases, and
emitting that revives the documented 400 from #2310/#2311 that ADR-2313 removed. So
the gate is the presence of an explicit pin, never that a value came back.

* fix(#3714): enforce the real-Codex value policy the sibling surface already applies

Review found one root cause behind a BLOCKER, two MAJORs, two MINORs and an
argv-injection finding: the dispatch path gated on the PRESENCE of an explicit pin
but never applied the VALUE policy that generateCodexAgentToml already applies to
the same config key. Textbook generative divergence -- and the parity row I wrote
tested resolver parity, not this policy, so it could never have caught it.

BLOCKER: a global ~/.gsd/defaults.json model_overrides.gsd-executor of 'sonnet',
'opus' or 'claude-sonnet-4-5' reached codex exec --model verbatim. That is the
documented 400 from #2310/#2311, arriving through the explicit-pin door rather
than the tier door. The issue asks for an explicit REAL-CODEX pin; real-Codex was
unenforced. Now dropped with a warning via the isAnthropicFlavoredModel predicate
#3241 single-sourced for exactly this reason.

Also: values are trimmed, so a whitespace-only pin is blank rather than
--model "   "; 'inherit' is matched case- and whitespace-insensitively, so
'Inherit' and ' inherit ' no longer reach the wire; and a value outside a model-id
charset is dropped with a warning. That last one closes the injection surface --
.planning/config.json travels with a clone, and values like
'gpt-5 -c approval_policy=never' or a command-substitution value previously reached argv verbatim,
where the spawner is an agent writing bash.

Every rejection DROPS AND WARNS rather than failing closed. An unusable exec is not
degraded, it is fatal: the dispatch step halts the wave after the worktree already
exists, so a config typo would have aborted execute-phase. The stale comment
claiming it degrades to sequential is corrected.

Separately, the seam's own empty-model handling contradicted the committed contract
and failed four tests on the remote runner. An absent, null or empty model is not an
error -- it means use the host default, the same degradation cwdFlag:null already
expresses. Unlike a prompt, where empty is a hang rather than a degraded run, so
that one stays fail-closed. Non-string values still fail closed.

Tests: the CLI rows were not HOME-hermetic and read the developer's real
~/.gsd/defaults.json, which is precisely the file the BLOCKER is about; HOME and
USERPROFILE are now sandboxed per call. Adds the global-pin regression, the
injection shapes, the case-variant inherit rows, and a real cross-surface
divergence guard.

* fix(#3714): close the flag-shaped pin, the case-variant alias, and the warning sink

Round two of review found three more defects, two of which both engines reached
independently, and all three were mine.

The charset allowlist put the dash INSIDE the character class, so a value made only
of allowed characters passed the pin policy silently and then tripped the seam's
leading-dash guard, producing exec:null. That is the wave-fatal path the whole
drop-and-warn design exists to avoid, reachable from a committed config file: -c,
--config, -p and --dangerously-skip-permissions all reproduced it. It also regressed
hosts with no model flag at all, where a dash pin turned a previously working
kimi-code dispatch into exec:null. The first character is now anchored, so a
flag-shaped value is dropped and warned like every other rejection, and the comment
that claimed this path was unreachable is corrected.

isAnthropicFlavoredModel folded case on its substring arm but not on its alias-set
arm, so SONNET, Sonnet, OPUS and HAIKU all reached Codex argv while lowercase sonnet
was correctly dropped -- the same 400 the drop exists to prevent. The predicate is
the one #3241 single-sourced so these surfaces cannot diverge, so folding case there
fixes the install-side .toml surface too.

The warning wrote the rejected value RAW to stderr. Every value that fails the
charset test contains by definition the characters the charset excludes, so it was a
guaranteed-reachable raw-to-terminal sink: an OSC sequence in a committed config
reached the operator's terminal byte for byte, and truncation could sever an escape
before its reset. The value is now sanitized before truncation.

Also from review: the allowlist rejected Vertex version pins like text-bison@002, a
false positive on a real model id; the policy ran host-neutrally so hosts with no
model flag printed a misleading drop warning on every dispatch; the invalid_model
branch had no test at all; and the changeset disclosed only that a pin is delivered,
not that an unusable one is now dropped with a warning.

* fix(#3714): single-source the model-id charset, bound the pin, keep the flag diagnosis

Round three found no blocking findings on either engine. These are the three
correctness items left in code I added.

The charset existed TWICE -- once to accept a pin, once to render a rejected one in
the warning -- and the two copies had already drifted inside a single commit: '@'
was added to the accept class and not the render class, so a Vertex-shaped value
rejected for some other reason rendered as text-bison?002. Both are now derived
from one definition, with a parity test asserting every character the matcher
accepts survives the sanitizer unchanged, so they cannot drift again.

A pin reached argv unbounded. CLAUDE.md documents the hazard: execFileSync aborts
on Windows above 32,767 characters of argv. A model id has no reason to be long, so
a pin over 200 characters is dropped and warned rather than truncated -- a truncated
model id is a different model id. Boundary rows at 199, 200 and 201.

The first character is now required to be alphanumeric, so '@evil' and '/c' no
longer reach argv. Security rates both inert on codex today, so this is hardening
rather than a live defect; it is here because it is one character of regex and
resolveOrchestratorExec documents itself as a general descriptor-to-argv seam that
other hosts may adopt.

Tightening the anchor made the leading-dash branch unreachable and, with it,
regressed the diagnosis: '-c' began reporting 'unsafe characters' instead of
'looks like a flag/option'. The dash check now runs before the charset test, which
both restores the actionable message for the most likely user typo and keeps the
branch live. A test pins the distinction between the three rejection messages so
the branch cannot silently die again.

* test(#3714): make the charset parity guard actually guard, and remove a false-green trap

Review proved by mutation that my parity test could not do what its own comment
claimed. It bound the expected character set to a local that was assigned and
discarded, and the shared definition was not exported, so widening that definition
left the test green. A comment overstating what a test guards is worse than no
comment, because the next reader trusts it. The shared body is now exported and
asserted equal, and I watched the assertion fail against a widened definition
before keeping it.

The sanitizer regex was module-scope, carried the g flag and was exported.
Production only uses it with replace, which resets lastIndex, so there was no live
bug -- but test() on a g-flagged regex alternates between calls, so any future test
reaching for it would false-green. The g-flagged copy is now internal to the one
call site that needs it and the exported companion carries no flags, which removes
the footgun rather than documenting it.

Also notes at the shared definition that it is interpolated into both a positive and
a negated character class, so only plain characters and ranges are safe to add.

No runtime behavior changes: the accept set, the anchor, the length cap, the
sanitizer output, the rejection ordering and all three message texts are
byte-identical, re-verified through the real CLI.

* chore(#3714): backfill changeset pr number

* chore(#3714): re-trigger CI after the GitHub Actions outage

The workflow runs for this branch were created during the Actions major outage on
2026-08-26 and never got scheduled. They are wedged: GitHub reports them queued,
refuses to cancel them, and refuses to rerun them because it believes the workflow
is already running. The Tests run is also pinned to a superseded sha, so no test run
exists for the current head at all.

Actions is operational again and the repo-wide queue has drained, so a fresh push is
what creates schedulable runs. This commit is empty on purpose: nothing about the
change is being altered, and the verified content is byte-identical to 7860c6ccc.

---------

Co-authored-by: sim <sim@local>
2026-08-26 17:01:09 -04:00
Tom Boucher
83a26ed1dc fix(#2939): honor the declared depth budget in shouldFlattenDispatch (#3063)
* test(#2939): prove shouldFlattenDispatch ignores the depth budget

Failing-first regression for #2939. shouldFlattenDispatch checks only
background+backgroundDispatch, never nested/subagentToolkit/maxDepth, so a
maxDepth:1 descriptor (no room for a bg orchestrator plus a leaf) is told it
may background. Row 1 (codex-like, maxDepth:1) asserts true (flatten) and
fails today; rows 2/3 guard the unchanged depth-sufficient cases.

* fix(#2939): honor the declared depth budget in shouldFlattenDispatch

shouldFlattenDispatch checked only background+backgroundDispatch, never
nested/subagentToolkit/maxDepth, so a maxDepth:1 descriptor (no room for a
backgrounded orchestrator plus a delegated leaf) was told it may background —
producing a depth-2 tree (Codex MultiAgent V2) the declared contract cannot
support.

canBackground now ALSO requires nested:true + subagentToolkit:"full" + a depth
budget > 1 (or unbounded -1), reusing the exact predicate shape from
bin/install.js _normalizeDispatchCallSpan and matching degradationFor's
treatment of maxDepth===1 as flat. Non-finite/missing maxDepth fails closed to
flatten. Correct the two existing pins that asserted the buggy output (bare
{bg,bgDispatch} now fail-closes on missing depth; the codex-like maxDepth:1 pin
flips to flatten) and add a maxDepth:2 negative-space row.

* fix(#2939): propagate depth-aware flatten to all pinned descriptors + tests

The isolated adversarial review found the depth-aware predicate reclassifies
codex/kimi/kimi-code (previously background-eligible under the two-field rule)
to flatten — the correct behavior, since each lacks what a backgrounded nesting
orchestrator needs:

  - codex: maxDepth:1 (no room for a depth-2 leaf)
  - kimi: nested:false (cannot host a nesting orchestrator)
  - kimi-code: subagentToolkit:'built-in-only' (cannot delegate to full subagents)

Only cursor (maxDepth:2) remains background-eligible. Update the three test
files that pinned the old contract (host-integration-descriptors EXPECTED_FLATTEN,
kimi-upgrades UPGRADE 2, trae-imperative-reference), and align the unbounded
convention to maxDepth < 0 (matching degradationFor/negotiateHostCapabilities)
with an accurate docstring noting the deliberate nested-check addition over
_normalizeDispatchCallSpan.

* fix(#2939): update dispatch-should-flatten CLI query pins for codex

The depth-aware rule (a0ad0f680) reclassifies codex (maxDepth:1) to flatten, but
command-routing-hub.test.cjs exercises the contract through the CLI query route
(runGsdTools query dispatch-should-flatten), not a direct shouldFlattenDispatch
call — so neither the reviewer's caller-search nor a grep for the symbol found
it; only the full gsd-test matrix did. Update the codex query assertions to
shouldFlatten=true (maxDepth:1 insufficient), preserving cursor (maxDepth:2 →
false) and the backgroundDispatch:true descriptor field.

* chore(#2939): add changeset fragment

pr:0 placeholder backfilled with the real PR number once the PR exists.

* fix(#2939): rephrase changeset for product-name-purity + opencode flatten pin

Two failures from the full gsd-test matrix on the prior sha:

1. product-name-purity: changeset fragments must not include parenthetical product
   descriptions (they render verbatim into CHANGELOG.md). 'Codex (and kimi/kimi-code)'
   tripped it — rephrase to lead with the behavior, naming runtimes inline without
   the parenthetical. lint:ci changeset-lint does not catch this; only the test does.

2. opencode-imperative-reference: the #2087-retraction pin flipped only the two
   background booleans and asserted shouldFlatten:false. Under #2939 that is no
   longer sufficient (opencode lacks nested + full toolkit + depth budget), so the
   retracted axes now correctly flatten — update the pin to true with rationale.

* chore(#2939): backfill changeset PR number 3063

---------

Co-authored-by: sim <sim@local>
2026-08-04 20:33:50 -04:00
Tom Boucher
c07734216f fix(#2603): document kimi-code in the host-integration capability matrix (#2687)
* fix(#2603): document kimi-code in the host-integration matrix; correct 3 inherited axes

The matrix — ADR-1239's deployment source-of-truth — had a section for 18 of 19
installed runtimes but none for `kimi-code`, so its `hostIntegration` axes shipped
with no citation and no evidence quote.

Sourcing every axis independently against Kimi Code CLI's own docs (the issue's
explicit requirement — `kimi` and `kimi-code` are distinct products) showed three
values had been inherited from the Python `kimi` descriptor rather than sourced:

- `embeddingMode` imperative -> declarative. Kimi Code plugins are a
  `kimi.plugin.json` manifest plus markdown Skills with no in-process programmatic
  API (docs/en/customization/plugins.md) — the same shape as `codex`.
- `dispatch.nested` false -> true. The `coder` built-in "can dispatch its own
  nested sub-agents when a task decomposes naturally" (docs/en/customization/agents.md).
  The Python `kimi` CLI genuinely prohibits nesting; Kimi Code does not.
- `dispatch.maxDepth` 1 -> "undocumented". Nesting is documented but no depth
  bound is published, so the fail-closed sentinel applies over a guessed integer.

`dispatch.namedDispatch` deliberately stays `false`: GSD's kimi-code artifact
layout installs Agent Skills only (no `agents` kind), so no named GSD subagent is
registered with the host and `resolveDispatchType` maps every role onto
coder/explore/plan. Flipping it would reintroduce the dispatch failure recorded in
docs/migration/kimi-to-kimi-code.md. The matrix records the host-capability nuance
under Documentation gaps instead.

Behaviourally inert: `namedDispatch:false` already caps nested/maxDepth/background/
backgroundDispatch to false/0 in the effective axes (host-integration.cts:493-499),
and the install adapter is not selected by `embeddingMode` (install.js:543 always
uses the imperative adapter). The one visible effect is the curated profile pin,
which moves programmatic-cli -> declarative-cli.

Also fixes the axes legend, which omitted the `built-in-only` subagentToolkit
member that has been in the closed vocabulary since kimi-code shipped.

Same defect class and countermeasure as #2598: pin the corrected values and require
the matrix to agree with the descriptor, because a descriptor/matrix disagreement is
how the gap survived.

Closes #2603

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT3ibz5qJuDuGqpTGRYVGf

* fix(#2603): report the maxDepth `undocumented` sentinel as a sentinel, not as malformed

Surfaced by the orthogonal review of this change. `negotiateHostCapabilities`
emits a sentinel-specific warning for every dispatch sub-axis carrying the
documented `undocumented` value — namedDispatch, nested, background,
subagentToolkit, backgroundDispatch, isolation — except `maxDepth`, which fell
through to the numeric guard and reported `host dispatch.maxDepth is missing or
not a number — treating as 0`.

That message is indistinguishable from a genuinely malformed descriptor, so a
correctly fail-closed descriptor reads as broken. Six shipped runtimes carry the
sentinel here (antigravity, augment, opencode, trae, windsurf, zcode) and this
PR's kimi-code correction adds a seventh, which is why it is fixed here rather
than left in place.

The numeric guard keeps firing for genuinely malformed values; both paths still
degrade `effective.dispatch.maxDepth` closed to 0. Covered by three tests,
including the boundary case that the sentinel carve-out must not swallow a real
malformed value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QT3ibz5qJuDuGqpTGRYVGf

* chore(#2603): backfill changeset PR number (#2687)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:52:57 -04:00
Tom Boucher
6ad30f74b6 feat(#2584): Phase 3 — scheduler consumer + isolation adapters (#2635)
Final phase of #2584 (ADR-1239 Codex-binding amendment). execute-phase now negotiates dispatch.isolation and dispatches through the matching adapter, so a wave's independent plans run concurrently on six runtimes instead of one — with no runtime=== branch in the scheduler.

harness-worktree passes the host's declared isolation flag (claude, cursor); orchestrator-worktree creates the worktree via the Phase-2 verb and spawns the executor into it with the resolved argv/cwd (codex, opencode, kimi, kimi-code); none stays sequential. Undeclared/unknown/unresolvable isolation degrades to none — never an unisolated parallel run.

Fixes two shipped Phase-2 descriptors that per-host research found would fail at spawn: kimi lacked its headless flag (would launch the interactive TUI and hang the orchestrator), and kimi-code named a non-existent binary (Kimi Code installs as 'kimi'). Adds the worktree-path root confinement Phase 2 deferred here, and leading-dash guards on the resolver's prompt/cwd matching the existing git-argument guard.

Closes #2627

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:50:20 -04:00
Tom Boucher
4a66d62d10 feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver (#2625)
* feat(#2584): Phase 2 — worktree create verb + orchestrator-exec resolver

Phase 2 of the negotiated executor-isolation feature (ADR-1239 Codex-binding amendment). Two building blocks for `dispatch.isolation: orchestrator-worktree` hosts, both unconsumed — no scheduler wires them yet (that is Phase 3), so no runtime behavior changes.

worktree create verb (planWorktreeCreate / executeWorktreeCreatePlan / cmdWorktreeCreate in worktree-safety.cts, routed via routeWorktree in gsd-tools.cjs): validates the wave base, creates a bounded branch+worktree, records it in the run manifest reusing record-agent 4-field entry shape, returns the executor working directory. Bounded git (10s timeout, degrade-not-throw); all manifest read/parse/validate/dedupe precedes the single git side effect (no unmanifested-orphan on a bad manifest); timeout-only best-effort partial rollback (a clean collision-exit never removes a live peer worktree); fail-closed on bad base, unsafe leading-dash / .. inputs, and malformed/mis-shaped manifest.

resolveOrchestratorExec (host-integration.cts): pure descriptor->argv resolver reading the new runtime.orchestratorExec descriptor field (codex/opencode/kimi/kimi-code), fail-closed on missing/invalid shape. Validator (capability-validator.cjs) + a parity guard asserting every orchestrator-worktree host declares a resolvable orchestratorExec.

Adding the create route edits the installed gsd-core/bin/gsd-tools.cjs, so the golden-install-parity fixtures for all 19 runtimes are regenerated (npm run gen:golden) — the only changed hash is gsd-tools.cjs. CONTEXT.md glossary updated; capability-registry regenerated. Behavioral tests (worktree-safety + host-integration) incl. a fast-check property test and the parity sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rebuild tracked state-transition.cjs to match #2400 source

The tracked compiled artifact drifted from src/state-transition.cts: #2400 (commit 2bcfaa2e2) added the progress.total_plans frontmatter sync to source but the tracked bin/lib/state-transition.cjs was never rebuilt, so the fix was not shipping to consumers of the compiled artifact. The mandatory build:lib step for Phase 2 surfaced the drift; recompiling makes the already-merged, already-changelogged #2400 fix effective. Artifact-only resync (no source/test change); drift class tracked by #2591.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:23:06 -04:00
Tom Boucher
ec7978c0b4 feat(#2584): add dispatch.isolation sub-field, descriptors, validator + negotiation (#2604) 2026-07-24 12:51:42 -04:00
Tom Boucher
d579daa3ed docs(#2505): Phase 6 — migration guide + built-in-only subagent-toolkit enum (#2538)
* docs(#2512): Phase 6 — migration guide + built-in-only subagent-toolkit enum

* fix #2512: update CONTRACT-PIN for built-in-only subagentToolkit value

* docs(changeset): backfill PR #2538 for Phase 6 (#2512)
2026-07-22 14:30:26 -04:00
Tom Boucher
f654c24a3e feat(#2505): Phase 4 — runtime-aware subagent dispatch (Option A; resolve-dispatch-type query) (#2525)
* feat(#2508): Phase 4 Option A — runtime-aware subagent dispatch via resolve-dispatch-type query (#2505)

* fix(#2508): prose-variant preamble (avoid scanner-tripping literals) + namedDispatch===false-only mapping

* fix(#2508): remove leftover old-preamble lines (keep prose variant only)

* fix #2508: prose-only reference file

* test #2508: regen golden install parity after workflow preamble additions

* fix #2508: remove preamble from plan-phase.md (Phase 6 capstone ceiling); regen size+golden baselines

* docs(changeset): backfill PR #2525 for Phase 4 (#2508)
2026-07-22 10:22:42 -04:00
Tom Boucher
09b535ac00 feat(#2481): add a negotiated effortSurface axis and wire invocation-time effort
ADR-1239 gains a ninth negotiated axis, effortSurface (argv | none), declaring how
a host accepts reasoning effort. ADR-443 is amended in the same change because its
recorded deferral is what the axis resolves: its Unblock condition offered paths
(a) and (b) and stated the choice was 'a maintainer call this file records but does
not make'. Path (a) is selected and satisfied here.

Before this, effort reached a runtime only through install-time channels
(EFFORT_RENDERING's frontmatter/api), so reviewer CLIs spawned as subprocesses
silently inherited whatever effort sat in the user's own global CLI config. The
review lane now resolves one universal effort through the ADR-443 cascade and
renders it per host through the negotiated descriptor.

Every per-host value is documentation-sourced, never inferred:
- claude   argv  -- verified via 'claude --help' (--effort <level>)
- opencode argv  -- verified via 'opencode run --help' (--variant)
- codex    argv  -- codex-rs/exec/src/cli.rs: model_reasoning_effort is NOT a CLI
                    flag (config.toml key only), so the global -c override is the
                    only argv route
- 15 hosts undocumented -- their docs state no reasoning setting; the sentinel
                    fails closed rather than inheriting a profile baseline

No config-file vocabulary member: the only host that ever had one (Gemini CLI's
thinkingConfig) was removed as a sunset runtime by 8f2ebbe9b (#1928, PR #1996),
and neither Antigravity CLI nor ZCode documents a reasoning setting.

Closes #2481

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:10:22 -04:00
Tom Boucher
79d7657eff feat(#2102): make pi a first-class installable runtime + fix its dispatch (ADR-1239)
Net-new EoS/pi installable runtime — purely additive (no prior runtime==='pi'
branches). pi is a bun-runtime programmatic-CLI whose /gsd command is registered
by a native ExtensionAPI extension and dispatches through the embedded engine.

Stage 1 (install plumbing):
- capabilities/pi/capability.json: full hostIntegration descriptor (imperative /
  slash-programmatic / active-model / native-extension / bun) + hostBehaviors
  {nativePlugin, pluginOnlyInstall}.
- --pi flag + interactive-menu renumber (All 17->18); pi added to RUNTIME_FLAG_IDS,
  RUNTIME_LABELS, RUNTIME_META, allRuntimes/runtimeMap, model-catalog defaults.
- Install mirrors OpenCode: pi installs the gsd.cjs extension + the shared engine
  payload (gsd-core + scripts + config markers) + the shared hooks bundle (spawned
  by the extension at lifecycle events, like OpenCode's plugin). pluginOnlyInstall
  EXCLUDES declarative command/agent/skill markdown, which pi has no host-read
  surface for (its /gsd is programmatic). _installNativePluginIfDeclared (extracted
  from the opencode-family path) copies pi/gsd.cjs -> ~/.pi/agent/extensions/gsd.cjs
  (global) / .pi/extensions/ (local). pi added to package.json files.
- Golden: new pi.json (320 files: extension + engine + 27-file hooks bundle, no
  markdown); the 16 other fixtures + claude-local change only by the shared
  model-catalog hash line.

Stage 2 (real dispatch + upgrades):
- Shared dispatchGsdCommand() (shell-command-projection): bounded, no-throw
  subprocess-shim to gsd-tools.cjs (the only full-surface dispatch path; no
  in-process full-hub factory exists). Fixes pi/gsd.cjs's createHub()-no-args bug
  (every dispatch was UnknownCommand) AND the identical bug in mcp-server.cts's
  gsd_invoke_command, which a vacuous unknown-family-only test had masked (now has
  a real dispatch regression test).
- pi/gsd.cjs: /gsd handler now (args, ctx) - tokenizes (quote-aware, via the
  shipped hooks/lib/git-cmd.js) + dispatches real family/subcommand (not hardcoded
  query/help); gsd_invoke gets a TypeBox (JSON-schema-fallback) parameters schema +
  consumes params; getArgumentCompletions; before_provider_request active-model
  steering (fail-open on null resolution); functional session_start /
  before_agent_start / session_before_compact hook bridges (spawn the shipped GSD
  hook scripts).
- EXTENSION_EVENT_SURFACES.pi expanded from ['tool_call'] to the full 30-event
  vocabulary.

Docs (host-integration matrix + how-to) + changeset (Added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:07:38 -04:00
Tom Boucher
f014ec83bd feat(#2093): migrate Kilo onto EoS imperative adapter + hook-bus/model/MCP/dispatch upgrades (ADR-1239)
Fold remaining isKilo logic branches into descriptor-driven reads:
finishPermissionWriter (uninstall cleanup), skipSharedHooksInstall (hooks
copy), and a skills converter-name registry (the artifactLayout.converter
field is now load-bearing, not decorative). frontmatterDialect stays the
documented dispatch key for frontmatter (no descriptor field for it). Dead
isKilo destructure bindings removed. Byte-identical golden parity for all 16
runtimes (opencode, which shares kilo's combined-family path, verified clean).

UPGRADE 1 (hook bus): install .kilo/plugins/gsd-core.js native plugin +
extensionEvents:"kilo" + EXTENSION_EVENT_SURFACES.kilo (OpenCode-fork bus).
UPGRADE 2 (active model): populate runtimeTierDefaults.kilo + thread
modelOverride through convertClaudeToKiloFrontmatter — model no longer stripped
from agents. UPGRADE 3 (MCP): document the gsd-core MCP companion under kilo's
mcp config key. UPGRADE 4 (named dispatch): agents/*.md mode:subagent roster is
the Task-tool dispatch surface (tested); subagentToolkit stays 'undocumented'
per AC so dispatch degrades to 'degraded' by design.

Model-catalog single-source edit ripples the shared model-catalog.json hash
into all 16 golden fixtures (expected). Inline defect fixes (no-defer): stale-
bake-guard resolveAgentDir 'agent'->'agents' (was a silent no-op for opencode/
codex), hardcoded 'Removed OpenCode plugin' uninstall log -> generic, and the
connect-gsd-mcp-server.md OpenCode mcpServers->mcp doc error.

Tests: kilo-imperative-reference (adapter/axes/fail-closed/degradation/
hostBehaviors + widened isKilo source-grep across 4 modules) + kilo-upgrades
(plugin parity+load, model-override converter, agents dispatch surface, MCP
doc). Matrix + how-to + config docs updated; changeset added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 19:55:15 -04:00
Tom Boucher
f152e9a069 feat(#2091): migrate hermes onto EoS imperative adapter + extensionEvents dialect (ADR-1239)
- Fold 7 hardcoded isHermes/runtime === 'hermes' branches in bin/install.js into
  descriptor-driven _hostBehaviors lookups (skillFrontmatterVersion,
  skillsManifestPrefix, trackCategoryDescription, writeCategoryDescription,
  reportSkillsCount, legacyCommandsGsdCleanup, brandingRewrites)
- Add runtime.hostBehaviors block to capabilities/hermes/capability.json
- Register EXTENSION_EVENT_SURFACES.hermes (13 real plugin hook events) in
  src/host-integration.cts — replaces the borrowed hookEvents:'claude' 6-event
  surface that silently never fired on Hermes
- Add extensionEvents:'hermes' to the descriptor
- Add 'hermes' to VALID_EXTENSION_EVENTS in capability-validator.cjs
- Regenerate capability-registry.cjs
- Tests: hermes-imperative-reference (negotiation, axes, fail-closed, source-guard),
  hermes-dispatch-upgrade (dispatch posture, degradation, fail-closed)
- Changeset + docs update
2026-07-09 20:47:35 -04:00
Tom Boucher
396f44bd0b feat(architecture): [EoS/opencode] Migrate OpenCode onto the Embeddable Orchestration System (ADR-1239, #2087)
Route OpenCode (and its Kilo sibling) through the public Host-Integration Interface and
land two Context7-verified capability upgrades. Byte-identical install output for all 16
runtimes (golden parity asserted).

Through the interface (AC2):
- OpenCode/Kilo's bespoke commands+skills+plugin install (the inline
  `else if (isOpencode || isKilo)` block) moves into the engine
  (installOpencodeFamilyCommands/Artifacts in src/install-engine.cts), dispatched by
  installRuntimeArtifacts when the descriptor declares hostBehaviors.combinedFamilyInstall.
  opencode/kilo now flow CLI -> _runtimeAdapter -> installRuntimeArtifacts like the skills
  runtimes. _isSkillsRuntime no longer excludes them; the bespoke block + dead
  copyFlattenedCommands are removed.
- Every hardcoded `runtime === 'opencode'`/`isOpencode` branch is folded into
  descriptor-driven runtime.hostBehaviors. ZERO `runtime === 'opencode'`/`'kilo'`
  string-equality remain in bin/install.js / install-engine.cts / runtime-artifact-conversion.cts.

Upgrades (AC4):
- Background dispatch: OpenCode shipped experimental background subagents in v1.15 and
  made them default-on in v1.17 -> dispatch.background/backgroundDispatch flip to true;
  shouldFlattenDispatch(opencode) now returns false (behavioral change; type: Changed).
- Expanded event surface: the OpenCode plugin subscribes permission.asked/replied +
  session.error.

Tests: opencode-imperative-reference (adapter/profile, shouldFlattenDispatch pin,
fail-closed negotiate, hostBehaviors, AC2 source-guard) + extended plugin surface test.
Docs: capability matrix v1.15/v1.17 citations. Changeset (Changed). gitignore .memdb//.memtrace/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:17:05 -04:00
Tom Boucher
b51cbf96cf feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface) (#1946)
* feat(#1943): extensionEvents vocabulary — separate from hookEvents (extension-system surface)

* fix(#1943): re-export VALID_EXTENSION_EVENTS from gen-capability-registry (test import path)

* fix(#1943): regenerate capability-registry.cjs + add changeset fragment

* fix(#1943): import VALID_EXTENSION_EVENTS from validator, not gen-capability-registry (golden parity)
2026-07-02 21:45:35 -04:00
Tom Boucher
325e9fad4b feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c (#1930)
* feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c

- Plugin (.opencode/plugins/gsd-core.js): handle session.idle (↔ Claude Stop
  lifecycle point; no-op sentinel — state already persisted to .planning/).
  Completes the compaction/idle pair (#1914 shipped compaction).
- Declare hookEvents: 'opencode-subset' in the OpenCode descriptor — the
  reserved dialect now has a real consumer (no longer zero-consumer).
- host-integration.cts: add HOOK_EVENT_SURFACES + hookEventSurfaceFor() — the
  pure consumer that resolves a dialect to its host-fireable event surface.
  opencode-subset = session/tool/file subset with NO workflow-phase events
  (engine owns phase sequencing; ADR-1239 §OpenCode binding).
- Tests: hookEventSurfaceFor unit tests (claude/gemini/opencode-subset/null);
  plugin session.idle no-throw; compaction breadcrumb; opencode-subset surface
  parity vs the plugin's handlers (Claude parity).

* fix(#1682): don't declare hookEvents on opencode (hooksSurface:none invariant)

The repo invariant couples runtime.hookEvents to the managed settings.json hook
surface: hooksSurface:'none' runtimes (opencode — plugin owns hooks) must NOT
declare hookEvents. Declaring 'opencode-subset' there violated 3 capability-
registry invariants + 2 install-plan golden masters + opencode golden parity.

The opencode-subset dialect is still IMPLEMENTED — just not via the legacy
descriptor field: hookEventSurfaceFor() (host-integration.cts) is its consumer,
and the OpenCode plugin consumes the subset events at runtime (session.idle
added here; compaction shipped in #1914). Declaring it on the descriptor would
require weakening the hooksSurface:none ⇔ no-hookEvents invariant (flagged for
decision).

* fix(#1682): refresh opencode golden parity for plugins/gsd-core.js (session.idle)

* docs(changeset): OpenCode session.idle + opencode-subset dialect (#1682)

* docs(changeset): backfill PR #1930
2026-07-02 16:16:00 -04:00
Tom Boucher
cf2e66b39e feat(#1708): typed documentation-sourced #853 dispatch-flatten (ADR-1239 Phase B) (#1719)
* feat(#1708): typed documentation-sourced #853 dispatch-flatten

Graduate the #853 orchestrator-backgrounding decision from a scattered RUNTIME==='codex' prose check to a typed, documentation-sourced engine decision. Adds a backgroundDispatch dispatch sub-axis (sourced per host: codex+cursor documented true, 9 documented false, 5 undocumented), shouldFlattenDispatch(dispatch) (inline UNLESS background && backgroundDispatch, fail-closed), and a gsd_run query dispatch-should-flatten the plan/execute workflows call. Cursor is newly background-eligible per its docs (inline->background) — a documentation-justified behavior change. No RUNTIME-name residue for this decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1708): backgroundDispatch citations in matrix + CONTEXT note

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1708): address review findings on typed dispatch-flatten

Code/adversarial review: convert the manager.md/autonomous.md Compound Action preamble from hardcoded 'On Codex' to FLATTEN-based branching (the handlers already use the query; the preamble contradicted them and was wrong for cursor); make shouldFlattenDispatch null-safe + type-honest (accepts raw 'undocumented' registry values); make backgroundDispatch a required descriptor field (matching its siblings, all 16 carry it); strengthen the config.runtime behavioral test; update the bug-853 prose-pin test + comment. Security review clean; Codex confirmed no fail-open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1708): backfill backgroundDispatch in role:runtime test fixtures

Making backgroundDispatch a required descriptor field broke role:runtime fixtures in capability-manifest-version/capability-registry/host-integration-descriptors tests that build a dispatch object without it (caught by full gsd-test, not scoped npm test). Backfill backgroundDispatch:false into the well-formed fixtures; the deliberately-malformed 'required-field' test fixture is left malformed by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1708): update fix-1521 dispatch-gating assertion to the FLATTEN gate

fix-1521 pinned the codex-specific run_in_background prose that #1708 graduated to the typed dispatch-should-flatten/FLATTEN gate. Update its assertions to verify FLATTEN=false gating (not a runtime name) + that the old RUNTIME===codex gate is gone. Caught by full gsd-test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1708): add changeset for typed dispatch-flatten

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1708): remove stray temp PR-body file

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1708): add issue ref to bug-853 allow-test-rule annotations

ADR-456 requires every allow-test-rule exemption to carry a see #NNN reference; the source-text-is-the-product annotations added when migrating the prose assertions lacked it (lint-tests CI gate). Add (see #1708).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:37:33 -04:00
Tom Boucher
30d4b85de5 feat(#1684): negotiated host-integration interface (ADR-1239 Phase A) (#1690)
* feat(#1684): add negotiated host-integration interface module

ADR-1239 Phase A: a pure, additive, no-I/O module exposing PROTOCOL_VERSION, the 8-axis HOST_INTEGRATION_AXES closed vocabulary, the UNDOCUMENTED fail-closed sentinel, negotiateHostCapabilities (effective subset of host-declared and engine-known), a typed degradation ladder, and host-capability profiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#1684): validate and document host-integration axes (16 runtimes)

Extend validateRuntimeBody to validate the 8 hostIntegration axes (closed enums + undocumented sentinel + dispatch struct + reserved-key guards) and the widened runtime vocabulary; author a documentation-sourced hostIntegration block in all 16 runtime descriptors; regenerate the registry. Every per-CLI value is documented (cited) or the explicit undocumented sentinel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add host-integration capability matrix and adr amendment

New per-CLI, per-axis citation reference (value/source/evidence for all 16 CLIs); ADR-1239 Phase-A-implemented amendment; CONTEXT.md glossary seam entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1684): harden dispatch negotiation edge cases

Code-review hardening: treat NaN/Infinity maxDepth as missing (fail-closed, +warning); reset nested/background when namedDispatch collapses to false (struct consistency); SAFE_DEFAULTS dispatch floor to read-only; warn on non-finite protocolVersion; symmetric undocumented warnings for dispatch fields. Pure module — no consumers; behaviour fail-closed throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1684): register host-integration.cjs in lint-ignore and inventory

New tsc-generated bin/lib artifact: add to the eslint ignore list (ADR-457 — lint the .cts source), regenerate docs/INVENTORY-MANIFEST.json, and add the docs/INVENTORY.md CLI-modules row. Fixes the 3 gsd-test failures (551-eslint-bin-lib-coverage x2 + inventory-manifest-sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add changeset fragment for host-integration interface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#1684): add how-to for sourcing a host's integration axes

Diataxis how-to guide for adding/updating a host's runtime.hostIntegration axes from authoritative docs, the undocumented-sentinel rule, validation, and extending the closed vocabulary. Completes the Step-5 doc quadrants (reference + explanation + how-to). Indexed in docs/README.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:33:06 -04:00