feat(#2505): Phase 4 — runtime-aware subagent dispatch (Option A; resolve-dispatch-type query) (#2525)

* feat(#2508): Phase 4 Option A — runtime-aware subagent dispatch via resolve-dispatch-type query (#2505)

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

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

* fix #2508: prose-only reference file

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

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

* docs(changeset): backfill PR #2525 for Phase 4 (#2508)
This commit is contained in:
Tom Boucher
2026-07-22 10:22:42 -04:00
committed by GitHub
parent 936a345381
commit f654c24a3e
72 changed files with 905 additions and 552 deletions

View File

@@ -545,6 +545,64 @@ function shouldFlattenDispatch(dispatch: UnvalidatedDispatch): boolean {
return !canBackground;
}
// ---------------------------------------------------------------------------
// resolveDispatchType — ADR-1239 / epic #2505 Phase 4 (Option A)
//
// Maps a requested GSD subagent name (e.g. "gsd-planner") to the type an
// Agent() call should actually use on the CURRENT runtime. On runtimes whose
// descriptor declares `hostIntegration.dispatch.namedDispatch: true` (Claude,
// OpenCode, Cursor, …), the requested name is returned unchanged — those hosts
// can dispatch GSD's named subagents directly. On runtimes with
// `namedDispatch: false` (kimi-code — only three built-in subagents
// `coder`/`explore`/`plan`, per moonshotai.github.io/kimi-code/en/customization/
// agents), the name is mapped to the closest built-in by role-suffix
// heuristic. The persona rides the existing `${AGENT_SKILLS_*}` prompt
// injection (Phase 3 / #2510) regardless of the resolved type, so the
// dispatcher does not need to know the persona — only the toolkit tier.
//
// This is Option A of the Phase 4 design (per-workflow runtime detection via
// `gsd_run query resolve-dispatch-type`), not Option B (PreToolUse mutation) —
// Kimi Code's documented hook API supports only allow/deny on PreToolUse, not
// tool_input rewriting, so a hook-based remap is infeasible (see #2508).
//
// Fail-closed: unknown dispatch shape or missing namedDispatch axis ⇒ return
// the requested name unchanged (named-dispatch is the GSD default; degrading
// to it on unknown runtimes preserves behavior for every runtime already in
// the field).
// ---------------------------------------------------------------------------
// Role-suffix → built-in mapping. Order matters: the first match wins.
// `plan`-tier agents plan/design without touching files; `explore`-tier agents
// are read-only; everything else (executors, writers, fixers, debuggers) maps
// to `coder` (the general-purpose built-in with the full tool set).
const DISPATCH_TYPE_SUFFIX_MAP: ReadonlyArray<readonly [RegExp, string]> = Object.freeze([
[/-?(planner|roadmapper|selector|spec)$/i, 'plan'],
[/-?(researcher|mapper|checker|verifier|auditor|analyzer|synthesizer|profiler|curator|classifier|reviewer)$/i, 'explore'],
]);
// Names that are already generic (not gsd-*) and should map to the
// general-purpose built-in on built-in-only runtimes.
const GENERIC_NAMES_TO_CODER: ReadonlySet<string> = Object.freeze(new Set([
'general-purpose', 'general', 'default', 'sonnet', 'opus', 'haiku',
]));
function resolveDispatchType(requested: unknown, dispatch: UnvalidatedDispatch): string {
if (typeof requested !== 'string' || requested.length === 0) return 'coder';
// Built-in-only runtime (EXPLICIT namedDispatch: false, e.g. kimi-code):
// map to coder/explore/plan by suffix heuristic.
if (dispatch && typeof dispatch === 'object' && dispatch.namedDispatch === false) {
if (GENERIC_NAMES_TO_CODER.has(requested)) return 'coder';
for (const [pattern, builtin] of DISPATCH_TYPE_SUFFIX_MAP) {
if (pattern.test(requested)) return builtin;
}
return 'coder';
}
// Named-dispatch runtime (namedDispatch: true OR unknown/absent): use the
// requested name unchanged. Absent namedDispatch degrades to named-dispatch
// (the GSD default) so every runtime already in the field keeps working.
return requested;
}
// ---------------------------------------------------------------------------
// Managed-hook event surface per hookEvents dialect (ADR-1239 / ADR-1016)
// ---------------------------------------------------------------------------
@@ -665,6 +723,7 @@ export = {
profileOf,
negotiateHostCapabilities,
shouldFlattenDispatch,
resolveDispatchType,
hookEventSurfaceFor,
extensionEventSurfaceFor,
};