feat(#3149): add dedicated init.debug entry point for /gsd:debug

/gsd:debug was one of the last workflows with no cmdInit* of its own: its
Step 0 made three separate round-trips (state.load, resolve-model
gsd-debugger, config-get workflow.tdd_mode) to assemble one context. Because
no debug-scoped fact was computed at any entry point, ADR-1671 admission gate
(2) could never be satisfied for debug — an applicability atom naming such a
fact would evaluate FALSE forever and silently exclude its section.

Adds cmdInitDebug (init.debug), registers it in the init router and the
command-alias table, and collapses debug.md Step 0 to one call. Every field
resolves through the same primitive the call it replaces used: loadConfig for
commit_docs, withProjectRoot for response_language (#2402), planningPaths for
debug_dir, resolveModelInternal for debugger_model, and the existing
Boolean(workflow.tdd_mode) idiom for tdd_mode.

PlanningPaths gains a debug field so state.load and init.debug share ONE
debug-directory expression rather than two kept in sync by hand. state.load
keeps emitting debug_dir: it is a shipped query surface with its own test
anchor, so narrowing it would break unseen consumers for no gain.

No WHEN_VOCABULARY atom and no gsd:section marker: gate (1), a consuming
section of at least 400 bytes, belongs to the change that adds the section.

Closes #3149

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sim
2026-08-07 09:30:27 -04:00
parent 0d93a5cfe5
commit 2bead6ca1d
11 changed files with 214 additions and 16 deletions

View File

@@ -448,6 +448,14 @@ export const INIT_COMMAND_ALIASES: CommandAlias[] = [
"subcommand": "transition",
"mutation": false
},
{
"canonical": "init.debug",
"aliases": [
"init debug"
],
"subcommand": "debug",
"mutation": false
},
{
"canonical": "init.new-workspace",
"aliases": [

View File

@@ -47,6 +47,7 @@ interface InitModule {
cmdInitDocsUpdate(cwd: string, raw: boolean, options?: Record<string, string | boolean | null | undefined>): void;
cmdInitUpdate(cwd: string, raw: boolean, options?: Record<string, string | boolean | null | undefined>): void;
cmdInitTransition(cwd: string, raw: boolean, options?: Record<string, string | boolean | null | undefined>): void;
cmdInitDebug(cwd: string, raw: boolean, options?: Record<string, string | boolean | null | undefined>): void;
cmdInitNewWorkspace(cwd: string, raw: boolean): void;
cmdInitListWorkspaces(cwd: string, raw: boolean): void;
cmdInitRemoveWorkspace(cwd: string, name: string | undefined, raw: boolean): void;
@@ -171,6 +172,10 @@ function routeInitCommand({ init, args, cwd, raw, error }: RouteInitCommandOptio
init.cmdInitUpdate(cwd, raw, { next: namedArgs['next'], rc: namedArgs['rc'] });
},
transition: () => init.cmdInitTransition(cwd, raw, {}),
debug: () => {
const namedArgs = parseNamedArgs(args, [], ['diagnose']);
init.cmdInitDebug(cwd, raw, { diagnose: namedArgs['diagnose'] });
},
'new-workspace': () => init.cmdInitNewWorkspace(cwd, raw),
'list-workspaces': () => init.cmdInitListWorkspaces(cwd, raw),
'remove-workspace': () => init.cmdInitRemoveWorkspace(cwd, args[2], raw),

View File

@@ -2690,6 +2690,68 @@ function cmdInitTransition(cwd: string, raw: boolean, options: Record<string, un
output(withProjectRoot(cwd, result), raw);
}
/**
* `debug.md`'s dedicated init entry point (#3149; prerequisite for #3128).
* `debug.md` previously carried NO `gsd_run query init.*` call at all — it made
* THREE separate round-trips instead: `state.load` (for `commit_docs`,
* `config.response_language` and `debug_dir`), `resolve-model gsd-debugger
* --pick model`, and `config-get workflow.tdd_mode --raw`. Because no
* debug-scoped fact was computed at any entry point, a `when=` atom naming one
* would have evaluated FALSE forever — ADR-1671's admission gate (2) and the
* silent-exclusion bug it exists to prevent (`docs/adr/1671-…:122-131`).
*
* Every field is resolved through the SAME primitive the call it replaces used,
* never a second hand-maintained copy (DEFECT.GENERATIVE-FIX):
*
* - `commit_docs` — `loadConfig`, the same loader `cmdStateLoad` calls.
* - `response_language` — NOT read here: `withProjectRoot` already injects it
* when configured (#2402), which is also the shape sibling init bundles use.
* It is absent, not null, when unset.
* - `debug_dir` — `planningPaths(cwd).debug`, the SAME expression `cmdStateLoad`
* now uses; the `debug` field was added to `PlanningPaths` (#3149) so the
* location has one source instead of two kept in sync by hand.
* - `debugger_model` — `resolveModelInternal`, which IS what `query
* resolve-model --pick model` returns (`cmdResolveModel`, src/commands.cts).
* - `tdd_mode` — the `Boolean(wf['tdd_mode'])` idiom `cmdInitExecutePhase` and
* `cmdInitPlanPhase` already use. `/gsd:debug` has no `--tdd` flag, so the
* sibling handlers' `options['tdd'] ||` disjunct is deliberately omitted
* rather than carried as a phantom.
*
* `state.load` is deliberately NOT narrowed — see the note beside its own
* `debug_dir` field. This handler is purely additive alongside it.
*
* `diagnose` is the one flag `/gsd:debug` already documents. Exposing it as a
* top-level fact follows `cmdInitUpdate`'s `next_channel` and
* `cmdInitAutonomous`'s `plan_strategy_converge` precedent, and is what makes
* the router's flag forwarding observable. No `when=` atom consumes it yet:
* admission gate (1) — a consuming section of at least 400 bytes — is #3128's
* to satisfy, and shipping the atom before its section is the same
* silent-exclusion bug from the other direction.
*/
function cmdInitDebug(cwd: string, raw: boolean, options: Record<string, unknown> = {}): void {
const config = loadConfig(cwd);
const wf = (config.workflow ?? {}) as Record<string, unknown>;
const result: Record<string, unknown> = {
commit_docs: config.commit_docs,
// #2376: absolute — debug.md builds `debug_file_path` as
// `{debug_dir}/{slug}.md` for its gsd-debug-session-manager spawns, whose
// own cwd may differ from the orchestrator's.
debug_dir: toPosixPath(planningPaths(cwd).debug),
debugger_model: resolveModelInternal(cwd, 'gsd-debugger'),
tdd_mode: Boolean(wf['tdd_mode']),
diagnose: options['diagnose'] === true,
};
// Additive, optional field — degrades to null while `debug` has no key in
// `gsd-core/workflows/section-manifest.json` (it has no `gsd:section` markers
// until #3128). null means "read everything", which is NOT the same as a
// computed empty selection.
result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'debug', {});
output(withProjectRoot(cwd, result), raw);
}
function cmdInitProgress(cwd: string, raw: boolean, options: Record<string, unknown> = {}): void {
try {
(pruneOrphanedWorktrees as (cwd: string) => void)(cwd);
@@ -3708,6 +3770,7 @@ export = {
cmdInitDocsUpdate,
cmdInitUpdate,
cmdInitTransition,
cmdInitDebug,
cmdInitNewWorkspace,
cmdInitListWorkspaces,
cmdInitRemoveWorkspace,

View File

@@ -166,6 +166,7 @@ interface PlanningPaths {
config: string;
phases: string;
requirements: string;
debug: string;
}
function planningPaths(cwd: string, ws?: string | null): PlanningPaths {
@@ -178,6 +179,10 @@ function planningPaths(cwd: string, ws?: string | null): PlanningPaths {
config: path.join(base, 'config.json'),
phases: path.join(base, 'phases'),
requirements: path.join(base, 'REQUIREMENTS.md'),
// #3149: the debug-session directory. Single source for both `state.load`'s
// `debug_dir` field and `init.debug`'s — previously each composed its own
// `path.join(planning, 'debug')` (DEFECT.GENERATIVE-FIX).
debug: path.join(base, 'debug'),
};
}

View File

@@ -351,10 +351,12 @@ function cmdStateLoad(cwd: string, raw: boolean): void {
config_exists: configExists,
// #2376: absolute (anchored on cwd), not orchestrator-cwd-relative — a
// spawned subagent's own cwd may differ from the orchestrator's.
// debug.md has no init.* call of its own; it reads this field from
// `state load` to build debug_file_path for its gsd-debug-session-manager
// spawns instead of hardcoding '.planning/debug/{slug}.md'.
debug_dir: toPosixPath(path.join(planDir, 'debug')),
// #3149: debug.md now has its own `init.debug` entry point and reads this
// field from there, not from `state load`. This stays on the state.load
// bundle regardless: it is a shipped query surface with its own test anchor
// (tests/state.test.cjs), so narrowing it would break unseen consumers for
// no gain (Hyrum's Law). Both emit the SAME `planningPaths(cwd).debug`.
debug_dir: toPosixPath(planningPaths(cwd).debug),
};
// For --raw, output a condensed key=value format