feat(#1689): per-plan agent_hint executor routing (#3417)

* feat(#1689): per-plan agent_hint executor routing

Option A per-plan specialist routing: a plan with an `agent_hint:` frontmatter field is dispatched to that subagent instead of gsd-executor when it resolves on the active runtime; absent/unresolved/disabled falls back to gsd-executor (byte-identical). Default-on via workflow.agent_hint_routing.

- src/phase.cts: parse agent_hint into the plan-index JSON (plan_json.agent_hint)
- agent-install-check.cts: resolveAgentHint() reuses getAgentsDir + runtime filename variants; probes project + global agent dirs; fails closed; rejects path-traversing names
- gsd-tools.cjs: 'resolve-agent' query route (fail-closed to gsd-executor; --raw/--json)
- execute-phase.md: lean per-plan reference + {EXECUTOR_TYPE} placeholder (host stays under the ADR-857 Phase 6 byte ceiling)
- execute-phase/steps/per-plan-executor-routing.md: resolution logic (Agent()-based dispatch; advisory on orchestrator-worktree)
- config: workflow.agent_hint_routing (validKey, default-on via SCHEMA_DEFAULTS, boolean validator)
- docs (CONFIGURATION.md, plan-md.md), changeset, tests/agent-hint-routing-1689.test.cjs (17 tests)

* chore(#1689): backfill changeset PR number (#3417)

* chore(#1689): regenerate install-tree fixtures for new workflow fragment

* chore(#1689): ack deliberate execute-phase.md growth (agent_hint routing)

* test(#1689): SPAWN contract allows parameterized subagent_type placeholder

agent-frontmatter's spawn-type checks scanned subagent_type="..." as a
concrete agent name. execute-phase now uses subagent_type="{EXECUTOR_TYPE}"
(a runtime placeholder resolved via resolve-agent, default gsd-executor).
Skip {TOKEN} placeholders in both the known-type and <available_agent_types>
checks; execute-phase still lists the built-in roster incl. gsd-executor.

* fix(#1689): CI conformance for the routing fragment

- per-plan-executor-routing.md: add the canonical runtime-launcher preamble to
  its gsd_run block (runtime-launcher-parity #373), matching sibling step fragments.
- agent-install-check.cts: drop a literal ~/.claude/agents path from the
  resolveAgentHint JSDoc so it does not leak into the compiled engine .cjs
  (cline install leak guard).

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-13 23:10:52 -04:00
committed by GitHub
parent 470389f3a2
commit 7976b1ca0d
34 changed files with 504 additions and 23 deletions

View File

@@ -75,8 +75,23 @@ must_haves:
| `status` | No | `superseded` | Marks a plan that was deliberately reassigned or abandoned mid-phase and will never be executed. A `status: superseded` plan is excluded from the phase's plan and summary counts, so it never holds the phase below 100%. See [Superseded plans](#superseded-plans). Any other value (or the field's absence) has no effect on counting. |
| `estimate` | No | object | Projected execution cost: `{tokens, raw_tokens, tasks, confidence}` (#2631, [ADR-2629](../adr/2629-phase-effort-estimation-calibration.md)). `tokens` is an `estimateTokens`-scale projection with the project's calibration factor **already applied** (which is why the plan-checker passes `--calibrated` to `estimate-check` — re-applying it would square the correction); `confidence` (`low`/`med`/`high`) is **derived from the calibration sample count, never self-rated**. Additive and optional — a plan without it behaves exactly as before. A plan estimated above `workflow.smart_zone_tokens` is flagged with a split recommendation at plan time; the flag is advisory and never blocks. |
| `must_haves` | Yes | object | Goal-backward verification criteria. See below. |
| `agent_hint` | No | string | Per-plan specialist executor routing (#1689). Name of a subagent that shares the `gsd-executor` execution contract (reads `execute-plan.md`, atomic-commit protocol). When the named agent resolves on the active runtime (an agent file exists in the runtime's agent dir), `execute-phase` dispatches it instead of `gsd-executor`. Unset/unresolved → `gsd-executor`, byte-identical to today. Default-on via `workflow.agent_hint_routing`; set `false` to disable. See [Per-plan executor routing](#per-plan-executor-routing). |
| `gap_closure` | Only in gap-closure mode | string, exact match | Must be exactly the literal lowercase `true` — validated as a string comparison, not a YAML boolean, so `True`, `TRUE`, `yes`, and `1` are all rejected. Required on every plan generated by `/gsd-plan-phase --gaps`, checked by the `plan-gap-closure` schema (`src/frontmatter.cts`) rather than `plan`. `/gsd-execute-phase --gaps-only` filters strictly on this field, so an omitted or wrong-valued `gap_closure` on a gap-closure plan means it is silently skipped — zero executors spawned, no error (#2847). Standard and reviews-mode plans validate against the unmodified `plan` schema, which neither requires nor checks this field (nothing rejects it as an extra field either, if present). |
### Per-plan executor routing
A plan can opt into a **specialist executor** by setting `agent_hint:` to the name of a subagent that shares the `gsd-executor` execution contract — it reads `execute-plan.md`, follows the atomic-commit protocol, and carries Read/Edit/Write/Bash. A Flutter specialist, for example:
```yaml
---
agent_hint: well-me-flutter-engineer
---
```
At dispatch, `execute-phase` resolves the hint against the **active runtime's agent directory** (both project-local and user-global, across the runtime's filename variants — `.md`, `.agent.md`, `.toml`, …) and dispatches the named subagent via `subagent_type`. If the field is absent, blank, or the named agent does not resolve, the plan dispatches to `gsd-executor` — byte-identical to behavior without the field. Routing is gated by `workflow.agent_hint_routing` (default-on; see [CONFIGURATION](../CONFIGURATION.md#workflow-toggles)).
The specialist agent is an ordinary agent file (e.g. `agents/well-me-flutter-engineer.md` on Claude Code); there is no separate registration manifest.
### Superseded plans
A phase reads complete when every `*-PLAN.md` has a matching `*-SUMMARY.md`. When a plan is reassigned or dropped mid-phase — its work folded into a later plan — it will never gain a summary, and without a marker it would pin the phase below 100% forever (the plan-level analogue of a retired phase). Add `status: superseded` to that plan's frontmatter to exclude it from **both** the plan count (denominator) and the summary count (numerator):