diff --git a/.changeset/nimble-agents-route.md b/.changeset/nimble-agents-route.md new file mode 100644 index 000000000..b41a31eb9 --- /dev/null +++ b/.changeset/nimble-agents-route.md @@ -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) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ac5a326c3..799a47e94 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 | diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index f225a2f96..9dba0851f 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -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", diff --git a/docs/reference/plan-md.md b/docs/reference/plan-md.md index 0324cea63..747b3a17a 100644 --- a/docs/reference/plan-md.md +++ b/docs/reference/plan-md.md @@ -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): diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index ae2914932..af6f17376 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -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 [args] [--raw] [--pick ` 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/.{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. diff --git a/src/agent-install-check.cts b/src/agent-install-check.cts index 85fbc65d3..86bcf3d2f 100644 --- a/src/agent-install-check.cts +++ b/src/agent-install-check.cts @@ -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 `` across runtime filename variants. + * Mirrors {@link checkAgentsInstalled}'s probe (`.md`, `.agent.md`, `.toml`, + * and the kimi `subagents/.{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(); + 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, }; diff --git a/src/config.cts b/src/config.cts index 13770e218..01a9b2eb5 100644 --- a/src/config.cts +++ b/src/config.cts @@ -91,6 +91,9 @@ const SCHEMA_DEFAULTS: Record = { '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') { diff --git a/src/phase.cts b/src/phase.cts index 751ac7524..20dc67069 100644 --- a/src/phase.cts +++ b/src/phase.cts @@ -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 diff --git a/tests/agent-frontmatter.test.cjs b/tests/agent-frontmatter.test.cjs index cd0f3dc40..569dc07c2 100644 --- a/tests/agent-frontmatter.test.cjs +++ b/tests/agent-frontmatter.test.cjs @@ -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 . + 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; diff --git a/tests/agent-hint-routing-1689.test.cjs b/tests/agent-hint-routing-1689.test.cjs new file mode 100644 index 000000000..4ba202686 --- /dev/null +++ b/tests/agent-hint-routing-1689.test.cjs @@ -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 .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 .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 .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); + } + }); +}); diff --git a/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json b/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json new file mode 100644 index 000000000..90a12e953 --- /dev/null +++ b/tests/emitted-drift-acks/1689-agent-hint-executor-routing.json @@ -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." + } +} diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 2c053b4bc..b018ac9eb 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -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", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 64ed0cbc3..b9d3cb27a 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -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", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index ccf6b2b44..d78034c8b 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -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", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 34e45ab05..d6d7c1e61 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -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", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index aaff7c14e..3d7ae077b 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -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", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 17e5885cb..4772d0aea 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -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", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index 0ba864867..e08a4a9f6 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -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", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index a351152a8..632d23154 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -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", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 9fa7d46bb..ca150c956 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -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", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index 597f4f942..4e074cae0 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -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", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 4ac74fc46..11a2c37c4 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -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", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 39075ff08..9193e165f 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -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", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 8dbb37198..a06bf15cc 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -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", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 1c0b298a4..c466ee7a2 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -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", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index be8e8103b..1ec042b00 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -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", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 31bb02088..d6ece0822 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -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", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 20667051a..603ff7c94 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -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", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index e023b072f..911112d72 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -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", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index b8db25fd3..6c5729610 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -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",