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

@@ -0,0 +1,5 @@
---
type: Added
pr: 3417
---
**Plans can now opt into a specialist executor via a per-plan `agent_hint:` frontmatter field** — `execute-phase` dispatches the named subagent instead of `gsd-executor` when it resolves on the active runtime, and falls back to `gsd-executor` when the field is absent, blank, or the named agent does not resolve (byte-identical to today). Resolution consults the active runtime's agent directory (project-local and user-global, across filename variants) via a new `gsd-tools resolve-agent` query, and the hint flows through `phase-plan-index` as `plan_json.agent_hint`. Default-on via `workflow.agent_hint_routing` (set `false` to disable); covers the `Agent()`-based dispatch (harness-worktree and sequential). (#1689)

View File

@@ -346,6 +346,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `workflow.skip_discuss` | boolean | `false` | When `true`, `/gsd-autonomous` bypasses the discuss-phase entirely, writing minimal CONTEXT.md from the ROADMAP phase goal. Useful for projects where developer preferences are fully captured in PROJECT.md/REQUIREMENTS.md. Added in v1.28 |
| `workflow.text_mode` | boolean | `false` | Replaces AskUserQuestion TUI menus with plain-text numbered lists. Required for Claude Code remote sessions (`/rc` mode) where TUI menus don't render. Can also be set per-session with `--text` flag on discuss-phase. Added in v1.28 |
| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31. **Branch-divergence note:** when your branch has diverged from `origin/HEAD`, GSD auto-degrades to sequential and prints a warning. See [`worktree.baseRef`](#worktree-settings) to restore parallel execution on a diverged branch. **Per-runtime note:** whether this key can be honored depends on the runtime's declared `dispatch.isolation` capability, not on its name (#2584). Runtimes whose own harness isolates each executor (**Claude Code**, **Cursor**) run parallel worktrees natively; runtimes exposing a headless exec with an explicit working directory (**Codex**, **OpenCode**, **Kimi**, **Kimi Code**) get worktrees GSD itself creates and merges — where a dispatch site can only drive the harness model, those hosts degrade to sequential with a warning rather than aborting. Every other runtime declares no isolation primitive, and forcing `use_worktrees: true` there still fails closed before any executor dispatch. `/gsd-health` reports such a value as warning `W025` (#2486). **Default on a non-Claude install:** if a worktree-capable non-Claude host is not isolating as described above, check whether the install stamped this key's default to `false` and set an explicit `use_worktrees: true`. See [Executor isolation per runtime](#executor-isolation-per-runtime). |
| `workflow.agent_hint_routing` | boolean | `true` | Per-plan specialist executor routing (#1689). When `true`, a plan whose `agent_hint:` frontmatter names a subagent that resolves on the active runtime is dispatched to that specialist instead of `gsd-executor`. Default `true` — a no-op for plans without `agent_hint:`, so existing dispatch is unchanged. Set `false` to disable. See [PLAN.md `agent_hint`](reference/plan-md.md#per-plan-executor-routing). |
| `workflow.worktree_skip_hooks` | boolean | `false` | When `true`, executor agents in worktree mode pass `--no-verify` (skipping pre-commit hooks) and post-wave hook validation runs against the merged result instead. Opt-in escape hatch for projects whose hooks cannot run in agent worktrees. Default `false` runs hooks on every commit (#2924). |
| `workflow.code_review` | boolean | `true` | Enable `/gsd-code-review` and `/gsd-code-review --fix` commands. When `false`, the commands exit with a configuration gate message. Added in v1.34 |
| `workflow.code_review_depth` | string | `standard` | Default review depth for `/gsd-code-review`: `quick` (pattern-matching only), `standard` (per-file analysis), or `deep` (cross-file with import graphs). Can be overridden per-run with `--depth=`. Added in v1.34 |

View File

@@ -579,6 +579,7 @@
"execute-phase/steps/executor-isolation-dispatch.md",
"execute-phase/steps/gap-closure-artifacts.md",
"execute-phase/steps/partial-wave.md",
"execute-phase/steps/per-plan-executor-routing.md",
"execute-phase/steps/per-plan-worktree-gate.md",
"execute-phase/steps/post-merge-gate.md",
"execute-phase/steps/regression-gate-run.md",

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):

View File

@@ -1992,6 +1992,50 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
}
}
function routeResolveAgent({ args, cwd, raw, error }) {
// #1689: resolve a per-plan agent_hint specialist name to the subagent_type
// an Agent() call should use. Returns the name unchanged when a
// matching agent file exists in the active runtime's agent dir(s);
// 'gsd-executor' when the name is absent, blank, or does not resolve.
// Fail-closed is the fallback (gsd-executor) — never echo an
// unvalidated name, which would make Agent() error and block the wave.
//
// Output:
// --raw (default) -> prints the resolved type (the hint, or 'gsd-executor')
// --json -> prints { runtime, requested, resolved, fallback }
const FALLBACK = 'gsd-executor';
try {
const nameIdx = args.indexOf('--name');
const requested = nameIdx !== -1 ? args[nameIdx + 1] : '';
const { resolveRuntime } = require('./lib/runtime-slash.cjs');
const runtimeId = resolveRuntime(cwd);
const { resolveAgentHint } = require('./lib/agent-install-check.cjs');
let resolved = FALLBACK;
let resolvedOk = false; // true only when resolveAgentHint returned a hit
if (requested && !requested.startsWith('-')) {
const hit = resolveAgentHint(requested, runtimeId, cwd);
if (hit !== null) {
resolved = hit;
resolvedOk = true;
}
}
// `fallback` = we did NOT honor a resolvable hint (absent/flag-shaped name,
// the named agent did not resolve, or resolution errored). Requesting
// gsd-executor explicitly and resolving to it is NOT a fallback.
const fellBack = !resolvedOk;
const jsonIdx = args.indexOf('--json');
if (jsonIdx !== -1) {
output({ runtime: runtimeId, requested: requested || null, resolved, fallback: fellBack }, raw);
} else {
process.stdout.write(String(resolved));
}
} catch {
// Fail-closed: degrade to the legacy executor on any error so dispatch
// never blocks on resolution.
process.stdout.write(FALLBACK);
}
}
function routeAgentSkills({ args, cwd, raw, error }) {
// --json emits typed IR { agent_type, block, skills_count } for test assertions
// (#455). Default (no flag) outputs raw XML so workflow shell expansions work.
@@ -3584,6 +3628,7 @@ const HOST_COMMAND_ROUTERS = {
'inspect-dispatch-isolation': routeInspectDispatchIsolation,
'record-dispatch-isolation': routeRecordDispatchIsolation,
'resolve-dispatch-type': routeResolveDispatchType,
'resolve-agent': routeResolveAgent,
'agent-skills': routeAgentSkills,
'skill-manifest': routeSkillManifest,
'history-digest': routeHistoryDigest,
@@ -3836,7 +3881,7 @@ const TOP_LEVEL_USAGE = 'Usage: gsd-tools <command> [args] [--raw] [--pick <fiel
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, restore-custom-files, roadmap, scaffold, smart-entry, state, ' +
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-dispatch-type, ' +
'config-set-model-profile, dispatch-isolation, dispatch-should-flatten, inspect-dispatch-isolation, record-dispatch-isolation, estimate-calibrate, estimate-calibration, estimate-check, resolve-agent, resolve-dispatch-type, ' +
'resolve-execution, review-lane, skill-manifest, skills-root, state-snapshot, stats, summary-extract, teams-status, todo, uat, update-context, verification, websearch, windows, ' +
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
'Global flags:\n' +

View File

@@ -28,6 +28,7 @@
"verifier": true,
"nyquist_validation": true,
"ai_integration_phase": true,
"agent_hint_routing": true,
"human_verify_mode": "end-of-phase",
"auto_advance": false,
"_auto_chain_active": false,

View File

@@ -35,6 +35,7 @@
"workflow.plan_chunked",
"workflow.specless_probe_fallback",
"workflow.plan_review_convergence",
"workflow.agent_hint_routing",
"code_quality.fallow.enabled",
"code_quality.fallow.scope",
"code_quality.fallow.profile",

View File

@@ -653,9 +653,9 @@ increases monotonically across waves. `{status}` is `complete` (success),
`[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} starting ({P}/{Q} plans done)`
Pass paths only — executors read files themselves with their fresh context window.
For 200k models, this keeps orchestrator context lean (~10-15%).
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
Pass paths only — executors read files themselves.
**Executor routing (#1689).** Per plan, run `gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md` to set `EXECUTOR_TYPE` for `subagent_type="{EXECUTOR_TYPE}"` below.
**Worktree mode** (`USE_WORKTREES` and `USE_WORKTREES_FOR_PLAN` not `false`):
@@ -690,8 +690,8 @@ increases monotonically across waves. `{status}` is `complete` (success),
```text
Agent(
subagent_type="gsd-executor",
description="Execute plan {plan_number} of phase {phase_number}",
subagent_type="{EXECUTOR_TYPE}",
description="Execute plan {plan_number} of phase {phase_number}",
# Only include model= when executor_model is an explicit model name.
# When executor_model is "inherit", omit this parameter entirely so
# Claude Code inherits the orchestrator model automatically.

View File

@@ -0,0 +1,54 @@
# Per-plan executor routing (#1689)
Run for each plan, immediately before its `Agent()` dispatch in step 3. Sets
`EXECUTOR_TYPE` so a plan can opt into a specialist executor instead of the
default `gsd-executor`. `plan_json` (the current plan's object from
`phase-plan-index`, same scope step 2.5 uses) is in scope.
## Contract
- Default: `EXECUTOR_TYPE="gsd-executor"` — byte-identical to pre-#1689 dispatch.
- A plan opts into a specialist by declaring `agent_hint: <name>` in its PLAN.md
frontmatter. The field reaches the orchestrator as `plan_json.agent_hint`
(parsed by `phase-plan-index`; `null` when unset).
- When routing is enabled AND the hint is non-empty AND the named agent resolves
on the active runtime, `EXECUTOR_TYPE` becomes the hint. Otherwise it stays
`gsd-executor`.
- The resolved `EXECUTOR_TYPE` is used as `subagent_type` in BOTH worktree and
sequential dispatch (sequential reuses the worktree-mode `Agent()` template).
## Resolution
```bash
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
# Default-on; opt out with: gsd config-set workflow.agent_hint_routing false
AGENT_HINT_ROUTING=$(gsd_run query config-get workflow.agent_hint_routing --raw 2>/dev/null || echo "true")
EXECUTOR_TYPE="gsd-executor"
if [ "${AGENT_HINT_ROUTING:-true}" != "false" ]; then
PLAN_HINT=$(jq -r '.agent_hint // empty' <<<"$plan_json" 2>/dev/null | tr -d '"')
if [ -n "$PLAN_HINT" ]; then
EXECUTOR_TYPE=$(gsd_run query resolve-agent --name "$PLAN_HINT" --raw 2>/dev/null || echo "gsd-executor")
fi
fi
# #1689 v1 routes only the Agent()-based dispatch. On the orchestrator-worktree
# backend (process-spawn; no subagent_type) a resolved hint cannot be honored
# yet — surface it so a set hint is never silently ignored.
if [ "${ISOLATION:-}" = "orchestrator-worktree" ] && [ -n "${PLAN_HINT:-}" ]; then
echo "note: plan ${plan_id} agent_hint='${PLAN_HINT}' resolved, but orchestrator-worktree dispatch does not route subagent types in this release — using the default executor." >&2
fi
```
`gsd_run query resolve-agent` consults the **active runtime's agent directory**
(both project-local and user-global, across runtime filename variants — `.md`,
`.agent.md`, `.toml`, the kimi `subagents/<name>.{yaml,md}` pair) and fails
closed to `gsd-executor` when the named agent does not resolve or on any error,
so a missing or misspelled hint never blocks dispatch.
## Scope
Routing applies to the `Agent()`-based dispatch (harness-worktree and sequential
modes). The `orchestrator-worktree` isolation backend spawns executors via a
separate process path that has no `subagent_type` and is not routed in this
release.

View File

@@ -172,21 +172,7 @@ function checkAgentsInstalled(runtime?: string, projectRoot?: string): AgentsIns
}
for (const agent of expectedAgents) {
const agentFile = path.join(agentsDir, `${agent}.md`);
const agentFileCopilot = path.join(agentsDir, `${agent}.agent.md`);
const agentFileCodex = path.join(agentsDir, `${agent}.toml`);
const agentFileKimiYaml = path.join(agentsDir, 'subagents', `${agent}.yaml`);
const agentFileKimiPrompt = path.join(agentsDir, 'subagents', `${agent}.md`);
const kimiAgentInstalled =
resolvedRuntime === 'kimi' &&
fs.existsSync(agentFileKimiYaml) &&
fs.existsSync(agentFileKimiPrompt);
if (
fs.existsSync(agentFile) ||
fs.existsSync(agentFileCopilot) ||
fs.existsSync(agentFileCodex) ||
kimiAgentInstalled
) {
if (agentFileExists(agentsDir, agent, resolvedRuntime)) {
installed.push(agent);
} else {
missing.push(agent);
@@ -359,9 +345,71 @@ function checkCodexModelPosture(runtime?: string, projectRoot?: string): CodexMo
};
}
/**
* Probe a single agents dir for `<name>` across runtime filename variants.
* Mirrors {@link checkAgentsInstalled}'s probe (`.md`, `.agent.md`, `.toml`,
* and the kimi `subagents/<name>.{yaml,md}` pair) so the two can never disagree
* about which on-disk shapes count as "installed". Not exported — internal to
* {@link resolveAgentHint}.
*/
function agentFileExists(agentsDir: string, name: string, runtime: string): boolean {
const base = path.join(agentsDir, `${name}.md`);
const copilot = path.join(agentsDir, `${name}.agent.md`);
const codex = path.join(agentsDir, `${name}.toml`);
if (fs.existsSync(base) || fs.existsSync(copilot) || fs.existsSync(codex)) {
return true;
}
// kimi requires BOTH the persona yaml and the prompt md (same as checkAgentsInstalled).
const kimiYaml = path.join(agentsDir, 'subagents', `${name}.yaml`);
const kimiPrompt = path.join(agentsDir, 'subagents', `${name}.md`);
return runtime === 'kimi' && fs.existsSync(kimiYaml) && fs.existsSync(kimiPrompt);
}
/**
* Resolve a per-plan `agent_hint` specialist name to a dispatchable subagent
* type on the active runtime (#1689). Unlike {@link checkAgentsInstalled},
* which validates the fixed GSD roster, this answers "does an agent file for
* this ARBITRARY name exist in the active runtime's agent dir(s)?" — so a plan
* can opt into a domain specialist (e.g. a Flutter engineer) that shares the
* gsd-executor contract without being part of the built-in roster.
*
* Probes BOTH the runtime-canonical agents dir ({@link getAgentsDir}, which
* honors `GSD_AGENTS_DIR`, project-local manifest-backed installs, and the
* claude install-relative path) AND the runtime's global config agents dir, so
* a specialist installed at either level is recognized. The decision in #1689
* explicitly requires consulting the active runtime's agent dir rather than
* only the Claude runtime's user-global and project-local agent dirs.
*
* @returns the name when a matching agent file exists; `null` when it does not
* (the caller falls back to `gsd-executor`). An empty/whitespace name always
* returns `null`.
*/
function resolveAgentHint(name: string, runtime?: string, projectRoot?: string): string | null {
const trimmed = String(name ?? '').trim();
if (trimmed === '') return null;
// A hint is a bare agent name. Reject path separators and `..` so a value
// like `../../README` cannot path-traverse out of the agents dir via
// path.join and match an unrelated file — that would echo an invalid
// subagent_type and block the wave, defeating fail-closed resolution.
if (trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) return null;
const resolvedRuntime = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
const candidateDirs = new Set<string>();
candidateDirs.add(getAgentsDir(resolvedRuntime, projectRoot));
candidateDirs.add(path.join(getGlobalConfigDir(resolvedRuntime), 'agents'));
for (const dir of candidateDirs) {
if (agentFileExists(dir, trimmed, resolvedRuntime)) {
return trimmed;
}
}
return null;
}
export = {
getAgentsDir,
checkAgentsInstalled,
checkCodexModelPosture,
POSTURE_REASON,
resolveAgentHint,
};

View File

@@ -91,6 +91,9 @@ const SCHEMA_DEFAULTS: Record<string, unknown> = {
'planner.stall_detect_interval_minutes': 5,
'planner.stall_threshold_minutes': 10,
'git.create_tag': true,
// #1689: per-plan agent_hint executor routing — default-on. A no-op for plans
// without an agent_hint field, so existing dispatch is byte-identical.
'workflow.agent_hint_routing': true,
// Derived from the defaults manifest rather than restated, so the manifest
// stays the single source of truth for the smart-zone budget (#2630).
'workflow.smart_zone_tokens': CONFIG_DEFAULTS.smart_zone_tokens,
@@ -808,6 +811,13 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
}
}
// Per-plan executor routing via agent_hint frontmatter (#1689)
if (kp === 'workflow.agent_hint_routing') {
if (typeof parsedValue !== 'boolean') {
error(`Invalid workflow.agent_hint_routing '${val}'. Must be a boolean (true or false).`);
}
}
// #3086 — git.create_tag: boolean only
if (kp === 'git.create_tag') {
if (typeof parsedValue !== 'boolean') {

View File

@@ -594,6 +594,8 @@ interface RawPlan {
hasSummary: boolean;
/** #2830: true iff this plan's own SUMMARY declares `status: halted` (a designed stop). */
halted: boolean;
/** #1689: optional per-plan specialist executor hint (frontmatter `agent_hint:`). null when unset. */
agentHint: string | null;
}
/**
@@ -814,6 +816,18 @@ function cmdPhasePlanIndex(cwd: string, phase: string, raw: boolean): void {
filesModified = Array.isArray(fmFiles) ? fmFiles.map(String) : [String(fmFiles)];
}
// #1689: optional per-plan specialist executor hint. Read verbatim here; the
// orchestrator resolves it against the active runtime's agent dir at dispatch
// time (execute-phase.md -> `gsd_run query resolve-agent`), falling back to
// gsd-executor when the field is unset or the named agent does not resolve.
let agentHint: string | null = null;
const fmAgentHint = fm['agent_hint'];
if (fmAgentHint !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar-to-string
const hintStr = String(fmAgentHint).trim();
agentHint = hintStr !== '' ? hintStr : null;
}
const hasSummary = !unsummarizedPlanFiles.has(planFile);
// #2830: a plan can have a SUMMARY (hasSummary=true) and still be halted —
@@ -833,6 +847,7 @@ function cmdPhasePlanIndex(cwd: string, phase: string, raw: boolean): void {
autonomous,
objective: extractObjective(content) || (fm['objective'] as string | null) || null,
filesModified,
agentHint,
taskCount,
hasSummary,
halted,
@@ -935,6 +950,7 @@ function cmdPhasePlanIndex(cwd: string, phase: string, raw: boolean): void {
autonomous: rawPlan.autonomous,
objective: rawPlan.objective,
files_modified: rawPlan.filesModified,
agent_hint: rawPlan.agentHint,
task_count: rawPlan.taskCount,
has_summary: rawPlan.hasSummary,
// #2830: additive fields — halted is this plan's OWN status; blocked_by

View File

@@ -106,6 +106,14 @@ describe('HOOK: hooks frontmatter pattern', () => {
// ─── Spawn Type Consistency ──────────────────────────────────────────────────
describe('SPAWN: spawn type consistency', () => {
// #1689: `subagent_type="{TOKEN}"` is a workflow-bound placeholder for
// parameterized executor dispatch (resolved at runtime via `gsd-tools
// resolve-agent`, defaulting to gsd-executor), not a concrete agent name.
// The static spawn-type checks below skip these — the effective value is
// validated at dispatch time, and execute-phase.md still documents the
// built-in roster (incl. gsd-executor) in <available_agent_types>.
const PARAMETERIZED_SPAWN_TYPE = /^\{[^}]+\}$/;
test('no "First, read agent .md" workaround pattern remains', () => {
const dirs = [WORKFLOWS_DIR, COMMANDS_DIR];
for (const dir of dirs) {
@@ -137,6 +145,7 @@ describe('SPAWN: spawn type consistency', () => {
const matches = content.matchAll(/subagent_type="([^"]+)"/g);
for (const match of matches) {
const agentType = match[1];
if (PARAMETERIZED_SPAWN_TYPE.test(agentType)) continue;
assert.ok(
validAgentTypes.has(agentType),
`${file} references unknown agent type: ${agentType}`
@@ -168,11 +177,13 @@ describe('SPAWN: spawn type consistency', () => {
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
for (const file of files) {
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
// Find all named subagent_type references (excluding general-purpose)
// Find all named subagent_type references (excluding general-purpose
// and #1689 runtime placeholders)
const matches = [...content.matchAll(/subagent_type="([^"]+)"/g)];
const namedAgents = matches
.map(m => m[1])
.filter(t => t !== 'general-purpose');
.filter(t => t !== 'general-purpose')
.filter(t => !PARAMETERIZED_SPAWN_TYPE.test(t));
if (namedAgents.length === 0) continue;

View File

@@ -0,0 +1,248 @@
process.env.GSD_TEST_MODE = '1';
/**
* Per-plan executor routing via `agent_hint:` frontmatter (#1689, Option A).
*
* Coverage:
* - resolveAgentHint() unit: resolves specialists present in the active
* runtime's agent dir(s); falls back to null for absent names. Filename
* variants (.md, .agent.md, .toml).
* - `gsd-tools resolve-agent` route: --raw / --json output, fail-closed to
* gsd-executor.
* - phase-plan-index data path: `agent_hint` is parsed into plan JSON (null
* when unset) so the orchestrator reads it from plan_json.
* - execute-phase.md host wiring: a lean per-plan reference + the
* `{EXECUTOR_TYPE}` placeholder, with all detail in the step fragment
* (ADR-857 Phase 6 byte-budget conformance).
* - workflow.agent_hint_routing config key: default-on (SCHEMA_DEFAULTS),
* boolean-validated, settable.
*/
const { test, describe, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const ROOT = path.join(__dirname, '..');
const WORKFLOW_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase.md');
const FRAGMENT_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'execute-phase', 'steps', 'per-plan-executor-routing.md');
const { resolveAgentHint } = require('../gsd-core/bin/lib/agent-install-check.cjs');
// A name unlikely to collide with a real shipped agent, so the global agent dir
// (~/.claude/agents) never produces a false positive during resolution tests.
const SPECIALIST = 'zzz-test-specialist-1689';
const SPECIALIST_TOML = 'zzz-test-specialist-toml-1689';
let tmpAgentsRoot;
let savedAgentsDir;
before(() => {
tmpAgentsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-1689-'));
savedAgentsDir = process.env.GSD_AGENTS_DIR;
});
after(() => {
if (savedAgentsDir === undefined) delete process.env.GSD_AGENTS_DIR;
else process.env.GSD_AGENTS_DIR = savedAgentsDir;
cleanup(tmpAgentsRoot);
});
describe('#1689 resolveAgentHint() — runtime agent-dir resolution', () => {
test('empty / whitespace name never resolves (returns null)', () => {
process.env.GSD_AGENTS_DIR = tmpAgentsRoot;
assert.equal(resolveAgentHint('', 'claude'), null);
assert.equal(resolveAgentHint(' ', 'claude'), null);
});
test('resolves a specialist present as <name>.md in the agent dir', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-md-'));
process.env.GSD_AGENTS_DIR = dir;
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\nbody\n');
try {
assert.equal(resolveAgentHint(SPECIALIST, 'claude'), SPECIALIST);
} finally {
cleanup(dir);
}
});
test('resolves a specialist present as <name>.toml (codex variant)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-toml-'));
process.env.GSD_AGENTS_DIR = dir;
fs.writeFileSync(path.join(dir, `${SPECIALIST_TOML}.toml`), 'name = "' + SPECIALIST_TOML + '"\n');
try {
assert.equal(resolveAgentHint(SPECIALIST_TOML, 'codex'), SPECIALIST_TOML);
} finally {
cleanup(dir);
}
});
test('returns null when the named agent does not resolve (fallback signal)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-empty-'));
process.env.GSD_AGENTS_DIR = dir;
try {
assert.equal(resolveAgentHint('definitely-not-installed-1689', 'claude'), null);
} finally {
cleanup(dir);
}
});
test('resolves a specialist present as <name>.agent.md (copilot variant)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-copilot-'));
process.env.GSD_AGENTS_DIR = dir;
const name = 'zzz-test-specialist-copilot-1689';
fs.writeFileSync(path.join(dir, `${name}.agent.md`), '---\nname: ' + name + '\n---\n');
try {
assert.equal(resolveAgentHint(name, 'copilot'), name);
} finally {
cleanup(dir);
}
});
test('rejects path-traversing names so they cannot escape the agents dir', () => {
process.env.GSD_AGENTS_DIR = tmpAgentsRoot;
assert.equal(resolveAgentHint('../../README', 'claude'), null);
assert.equal(resolveAgentHint('a/b', 'claude'), null);
assert.equal(resolveAgentHint('..', 'claude'), null);
});
});
describe('#1689 gsd-tools resolve-agent route', () => {
test('--raw: echoes the name when the specialist resolves', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-route-'));
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\n');
try {
const r = runGsdTools(['resolve-agent', '--name', SPECIALIST, '--raw'], dir, { GSD_AGENTS_DIR: dir });
assert.equal(r.exitCode, 0);
assert.equal(r.output.trim(), SPECIALIST);
} finally {
cleanup(dir);
}
});
test('--raw: falls back to gsd-executor when the name does not resolve', () => {
const r = runGsdTools(['resolve-agent', '--name', 'no-such-agent-1689', '--raw'], tmpAgentsRoot, { GSD_AGENTS_DIR: tmpAgentsRoot });
assert.equal(r.exitCode, 0);
assert.equal(r.output.trim(), 'gsd-executor');
});
test('--raw: falls back to gsd-executor for a path-traversing name (fail-closed)', () => {
const r = runGsdTools(['resolve-agent', '--name', '../../README', '--raw'], tmpAgentsRoot, { GSD_AGENTS_DIR: tmpAgentsRoot });
assert.equal(r.exitCode, 0);
assert.equal(r.output.trim(), 'gsd-executor');
});
test('--raw: falls back to gsd-executor when --name is missing', () => {
const r = runGsdTools(['resolve-agent', '--raw'], tmpAgentsRoot, { GSD_AGENTS_DIR: tmpAgentsRoot });
assert.equal(r.exitCode, 0);
assert.equal(r.output.trim(), 'gsd-executor');
});
test('--json: resolved (fallback=false) vs fell-back (fallback=true)', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-agents-json-'));
fs.writeFileSync(path.join(dir, `${SPECIALIST}.md`), '---\nname: ' + SPECIALIST + '\n---\n');
try {
const ok = runGsdTools(['resolve-agent', '--name', SPECIALIST, '--json'], dir, { GSD_AGENTS_DIR: dir });
const okJson = JSON.parse(ok.output);
assert.equal(okJson.resolved, SPECIALIST);
assert.equal(okJson.fallback, false);
const bad = runGsdTools(['resolve-agent', '--name', 'no-such-agent-1689', '--json'], dir, { GSD_AGENTS_DIR: dir });
const badJson = JSON.parse(bad.output);
assert.equal(badJson.resolved, 'gsd-executor');
assert.equal(badJson.fallback, true);
} finally {
cleanup(dir);
}
});
});
describe('#1689 phase-plan-index data path — agent_hint flows into plan JSON', () => {
function writePlan(projectDir, phase, file, fmFields) {
const phaseDir = path.join(projectDir, '.planning', 'phases', phase);
fs.mkdirSync(phaseDir, { recursive: true });
const entries = Object.entries({ phase, plan: '"01"', type: 'execute', wave: 1, depends_on: '[]', files_modified: '[]', autonomous: true, ...fmFields });
const fm = entries.map(([k, v]) => `${k}: ${v}`).join('\n');
const body =
'---\n' +
fm + '\n' +
'must_haves:\n' +
' truths: []\n' +
' artifacts: []\n' +
'---\n# plan\n';
fs.writeFileSync(path.join(phaseDir, file), body);
}
test('a plan with agent_hint surfaces the value; a plan without surfaces null', () => {
const project = createTempProject('gsd-1689-planidx-');
try {
writePlan(project, '01-test', '01-01-PLAN.md', { agent_hint: 'well-me-flutter-engineer' });
writePlan(project, '01-test', '01-02-PLAN.md', {});
const r = runGsdTools(['phase-plan-index', '01-test', '--json'], project);
assert.equal(r.exitCode, 0, r.output);
const idx = JSON.parse(r.output);
const byId = Object.fromEntries(idx.plans.map((p) => [p.id, p]));
assert.equal(byId['01-01'].agent_hint, 'well-me-flutter-engineer');
assert.equal(byId['01-02'].agent_hint, null);
} finally {
cleanup(project);
}
});
});
describe('#1689 execute-phase.md host wiring (byte-budget-lean; detail in fragment)', () => {
test('host references the per-plan routing fragment and uses the {EXECUTOR_TYPE} placeholder', () => {
const host = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(host.includes('per-plan-executor-routing.md'), 'host must reference the routing fragment');
assert.ok(host.includes('subagent_type="{EXECUTOR_TYPE}"'), 'host dispatch template must use the {EXECUTOR_TYPE} placeholder');
// The bulky resolution logic lives in the fragment, NOT inline (ADR-857 Phase 6).
assert.ok(!/\bgsd_run query resolve-agent\b/.test(host), 'resolution detail (resolve-agent call) must live in the fragment, not the host');
});
test('the routing fragment exists and carries the resolution contract', () => {
assert.ok(fs.existsSync(FRAGMENT_PATH), 'per-plan-executor-routing.md fragment must exist');
const frag = fs.readFileSync(FRAGMENT_PATH, 'utf-8');
assert.ok(frag.includes('EXECUTOR_TYPE'), 'fragment must set EXECUTOR_TYPE');
assert.ok(frag.includes('gsd_run query resolve-agent'), 'fragment must call the resolve-agent query');
assert.ok(frag.includes('agent_hint'), 'fragment must read plan_json.agent_hint');
assert.ok(frag.includes('workflow.agent_hint_routing'), 'fragment must honor the config gate');
// Fallback is the byte-identical default.
assert.ok(frag.includes('gsd-executor'));
});
});
describe('#1689 workflow.agent_hint_routing config key', () => {
test('default-on: config-get resolves true in a project that does not set it', () => {
const project = createTempProject('gsd-1689-cfg-');
try {
const r = runGsdTools(['config-get', 'workflow.agent_hint_routing', '--raw'], project);
assert.equal(r.exitCode, 0, r.output);
assert.equal(r.output.trim(), 'true');
} finally {
cleanup(project);
}
});
test('settable + opt-out round-trip (config-set false then config-get false)', () => {
const project = createTempProject('gsd-1689-cfgset-');
try {
const set = runGsdTools(['config-set', 'workflow.agent_hint_routing', 'false'], project);
assert.equal(set.exitCode, 0, set.output);
const get = runGsdTools(['config-get', 'workflow.agent_hint_routing', '--raw'], project);
assert.equal(get.output.trim(), 'false');
} finally {
cleanup(project);
}
});
test('boolean-validated: a non-boolean value is rejected', () => {
const project = createTempProject('gsd-1689-cfgvalid-');
try {
const set = runGsdTools(['config-set', 'workflow.agent_hint_routing', 'maybe'], project);
assert.notEqual(set.exitCode, 0, 'non-boolean must be rejected');
} finally {
cleanup(project);
}
});
});

View File

@@ -0,0 +1,6 @@
{
"version": 1,
"paths": {
"execute-phase.md": "#1689: per-plan agent_hint routing adds a one-line per-plan fragment reference and swaps the gsd-executor literal for a {EXECUTOR_TYPE} placeholder. Net +6 bytes, still under the ADR-857 Phase 6 byte ceiling (93600); all resolution detail lives in the execute-phase/steps/per-plan-executor-routing.md fragment to keep the host lean."
}
}

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -339,6 +339,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -338,6 +338,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -267,6 +267,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -271,6 +271,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -339,6 +339,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -374,6 +374,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -269,6 +269,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -339,6 +339,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -300,6 +300,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -336,6 +336,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -339,6 +339,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -236,6 +236,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -268,6 +268,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",

View File

@@ -339,6 +339,7 @@
"gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md",
"gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md",
"gsd-core/workflows/execute-phase/steps/partial-wave.md",
"gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md",
"gsd-core/workflows/execute-phase/steps/regression-gate-run.md",