@opengsd/gsd-core 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +31 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-codebase-mapper.md +1 -1
  6. package/agents/gsd-debug-session-manager.md +36 -0
  7. package/agents/gsd-executor.md +20 -7
  8. package/agents/gsd-intel-updater.md +3 -3
  9. package/agents/gsd-phase-researcher.md +4 -2
  10. package/agents/gsd-plan-checker.md +20 -0
  11. package/agents/gsd-planner.md +15 -23
  12. package/agents/gsd-project-researcher.md +2 -2
  13. package/agents/gsd-ui-auditor.md +0 -40
  14. package/bin/install.js +186 -55
  15. package/commands/gsd/plan-review-convergence.md +5 -1
  16. package/gsd-core/bin/gsd-tools.cjs +849 -2
  17. package/gsd-core/bin/lib/api-coverage.cjs +22 -8
  18. package/gsd-core/bin/lib/audit.cjs +8 -8
  19. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  20. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  21. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  22. package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
  23. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  24. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  25. package/gsd-core/bin/lib/check-command-router.cjs +12 -2
  26. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  27. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
  28. package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
  29. package/gsd-core/bin/lib/commands.cjs +246 -18
  30. package/gsd-core/bin/lib/config-loader.cjs +200 -28
  31. package/gsd-core/bin/lib/config.cjs +90 -5
  32. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  33. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  34. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  35. package/gsd-core/bin/lib/init.cjs +44 -19
  36. package/gsd-core/bin/lib/install-engine.cjs +1 -0
  37. package/gsd-core/bin/lib/milestone.cjs +5 -5
  38. package/gsd-core/bin/lib/model-catalog.cjs +51 -1
  39. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  40. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  41. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  42. package/gsd-core/bin/lib/phase-id.cjs +278 -5
  43. package/gsd-core/bin/lib/phase.cjs +57 -5
  44. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  45. package/gsd-core/bin/lib/plan-scan.cjs +1 -1
  46. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  47. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  48. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  49. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  50. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  51. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
  53. package/gsd-core/bin/lib/roadmap.cjs +10 -4
  54. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
  55. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
  56. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
  57. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  58. package/gsd-core/bin/lib/smart-entry.cjs +1 -1
  59. package/gsd-core/bin/lib/state-document.cjs +164 -20
  60. package/gsd-core/bin/lib/state-transition.cjs +28 -10
  61. package/gsd-core/bin/lib/state.cjs +141 -21
  62. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  63. package/gsd-core/bin/lib/uat.cjs +9 -7
  64. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  65. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  66. package/gsd-core/bin/lib/validate.cjs +32 -0
  67. package/gsd-core/bin/lib/verification.cjs +51 -14
  68. package/gsd-core/bin/lib/verify.cjs +128 -20
  69. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
  72. package/gsd-core/bin/shared/model-catalog.json +5 -0
  73. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  74. package/gsd-core/references/context-budget.md +40 -0
  75. package/gsd-core/references/gate-prompts.md +6 -3
  76. package/gsd-core/references/model-profile-resolution.md +64 -13
  77. package/gsd-core/references/offer-next.md +88 -0
  78. package/gsd-core/references/planning-config.md +2 -1
  79. package/gsd-core/references/reviewer-instances.md +28 -21
  80. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  81. package/gsd-core/references/ui-consideration-probe.md +2 -2
  82. package/gsd-core/references/worktree-branch-check.md +4 -4
  83. package/gsd-core/templates/summary-minimal.md +4 -0
  84. package/gsd-core/templates/summary-standard.md +4 -0
  85. package/gsd-core/templates/summary.md +7 -0
  86. package/gsd-core/workflows/ai-integration-phase.md +4 -4
  87. package/gsd-core/workflows/audit-fix.md +4 -0
  88. package/gsd-core/workflows/audit-milestone.md +8 -0
  89. package/gsd-core/workflows/autonomous.md +19 -15
  90. package/gsd-core/workflows/check-todos.md +2 -2
  91. package/gsd-core/workflows/code-review-fix.md +14 -6
  92. package/gsd-core/workflows/code-review.md +76 -19
  93. package/gsd-core/workflows/debug.md +10 -2
  94. package/gsd-core/workflows/diagnose-issues.md +4 -0
  95. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  96. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  97. package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
  98. package/gsd-core/workflows/discuss-phase.md +2 -2
  99. package/gsd-core/workflows/docs-update.md +8 -0
  100. package/gsd-core/workflows/eval-review.md +1 -1
  101. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  102. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  103. package/gsd-core/workflows/execute-phase.md +85 -115
  104. package/gsd-core/workflows/execute-plan.md +5 -4
  105. package/gsd-core/workflows/explore.md +4 -0
  106. package/gsd-core/workflows/extract-learnings.md +21 -0
  107. package/gsd-core/workflows/help/modes/full.md +3 -3
  108. package/gsd-core/workflows/import.md +4 -1
  109. package/gsd-core/workflows/ingest-docs.md +4 -0
  110. package/gsd-core/workflows/map-codebase.md +13 -6
  111. package/gsd-core/workflows/new-milestone.md +10 -2
  112. package/gsd-core/workflows/new-project.md +11 -4
  113. package/gsd-core/workflows/next.md +5 -2
  114. package/gsd-core/workflows/plan-phase.md +42 -46
  115. package/gsd-core/workflows/plan-review-convergence.md +18 -14
  116. package/gsd-core/workflows/progress.md +1 -1
  117. package/gsd-core/workflows/quick.md +14 -3
  118. package/gsd-core/workflows/review.md +146 -575
  119. package/gsd-core/workflows/scan.md +9 -1
  120. package/gsd-core/workflows/secure-phase.md +10 -2
  121. package/gsd-core/workflows/ship.md +41 -11
  122. package/gsd-core/workflows/smart-entry.md +1 -1
  123. package/gsd-core/workflows/ui-phase.md +8 -1
  124. package/gsd-core/workflows/ui-review.md +8 -1
  125. package/gsd-core/workflows/update.md +104 -5
  126. package/gsd-core/workflows/validate-phase.md +10 -2
  127. package/gsd-core/workflows/verify-work.md +8 -1
  128. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  129. package/hooks/dist/gsd-cursor-stop.js +6 -2
  130. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  131. package/hooks/dist/gsd-graphify-update.sh +9 -0
  132. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  133. package/hooks/dist/gsd-prompt-guard.js +101 -2
  134. package/hooks/dist/gsd-read-guard.js +100 -2
  135. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  136. package/hooks/dist/gsd-statusline.js +9 -6
  137. package/hooks/dist/gsd-workflow-guard.js +110 -6
  138. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  139. package/hooks/dist/lib/cursor-workspace.js +74 -0
  140. package/hooks/gsd-cursor-session-start.js +6 -2
  141. package/hooks/gsd-cursor-stop.js +6 -2
  142. package/hooks/gsd-cursor-subagent-start.js +6 -2
  143. package/hooks/gsd-graphify-update.sh +9 -0
  144. package/hooks/gsd-phase-boundary.sh +14 -2
  145. package/hooks/gsd-prompt-guard.js +101 -2
  146. package/hooks/gsd-read-guard.js +100 -2
  147. package/hooks/gsd-read-injection-scanner.js +109 -2
  148. package/hooks/gsd-statusline.js +9 -6
  149. package/hooks/gsd-workflow-guard.js +110 -6
  150. package/hooks/gsd-worktree-path-guard.js +132 -8
  151. package/hooks/lib/cursor-workspace.js +74 -0
  152. package/package.json +7 -7
  153. package/pi/gsd.cjs +26 -1
  154. package/scripts/check-coverage-gate.cjs +51 -0
  155. package/scripts/check-glossary-refs.cjs +24 -0
  156. package/scripts/ci-test-scope.cjs +67 -17
  157. package/scripts/gen-adr-index.cjs +6 -4
  158. package/scripts/gen-capability-matrix.cjs +26 -2
  159. package/scripts/gen-capability-registry.cjs +132 -34
  160. package/scripts/gen-emitted-baseline.cjs +145 -0
  161. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  162. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  163. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  164. package/scripts/lint-resolution-provenance.cjs +9 -0
  165. package/scripts/mutation-matrix.cjs +4 -0
  166. package/scripts/prompt-injection-scan.sh +6 -0
  167. package/scripts/registry-schema.cjs +57 -8
  168. package/scripts/release-notes/conventional-title.cjs +19 -1
  169. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  170. package/scripts/workflow-size.cjs +16 -8
  171. package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
  172. package/vscode/package.json +1 -1
  173. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  174. package/scripts/update-size-baseline.cjs +0 -68
@@ -37,7 +37,32 @@ const HOST_INTEGRATION_AXES = Object.freeze({
37
37
  stateIO: Object.freeze(['filesystem', 'sandboxed-storage', 'session-log-append']),
38
38
  transport: Object.freeze(['mcp', 'native-extension']),
39
39
  runtime: Object.freeze(['node', 'bun', 'sandboxed-web', 'python', 'go', 'rust', 'electron', 'other']),
40
- subagentToolkit: Object.freeze(['full', 'read-only']),
40
+ subagentToolkit: Object.freeze(['full', 'read-only', 'built-in-only']),
41
+ // ADR-1239 amendment (#2481): how reasoning effort reaches this host.
42
+ // `argv` — deliverable as an argument on the host's own invocation.
43
+ // `none` — the host exposes no reasoning-effort mechanism.
44
+ // `undocumented` is NOT a member here; it is the corpus-wide sentinel above.
45
+ // A config-file-only surface deliberately has NO vocabulary member: the only
46
+ // host that ever had one (Gemini CLI's thinkingConfig) was removed as a sunset
47
+ // runtime in #1928/#1996, and neither its successor Antigravity CLI nor ZCode
48
+ // documents a reasoning setting. Adding a member with no host would be a guess.
49
+ effortSurface: Object.freeze(['argv', 'none']),
50
+ // ADR-1239 Codex-binding amendment (#2584): a `dispatch` sub-field — not a new
51
+ // axis — declaring how a host isolates concurrent same-wave executors.
52
+ // `harness-worktree` — the host's own harness creates + binds a git worktree
53
+ // per executor; GSD passes the host's own isolation flag and calls no git
54
+ // itself (host-driven fan-out).
55
+ // `orchestrator-worktree` — GSD itself process-spawns each executor with an
56
+ // explicit working directory into a worktree GSD created, validated, and
57
+ // merges (GSD-driven fan-out; concurrency is OS-level, not the host's).
58
+ // `none` — no isolation primitive; same-wave plans run inline/sequentially
59
+ // (the #853 flatten rule).
60
+ // `undocumented` is NOT a member here; it is the corpus-wide sentinel above.
61
+ // Mechanism-specific ("worktree"), not abstract — same "name only what a
62
+ // host actually has" rule that kept effortSurface from guessing a
63
+ // config-file member above. A future non-worktree isolation mechanism adds a
64
+ // `*-container` member then, evidence-backed.
65
+ isolation: Object.freeze(['harness-worktree', 'orchestrator-worktree', 'none']),
41
66
  });
42
67
  const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks', 'state', 'artifact']);
43
68
  // ---------------------------------------------------------------------------
@@ -47,43 +72,47 @@ const INTERFACE_POINTS = Object.freeze(['command', 'dispatch', 'model', 'hooks',
47
72
  const SAFE_DEFAULTS = {
48
73
  embeddingMode: 'declarative',
49
74
  commandSurface: 'prose-only',
50
- dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false },
75
+ dispatch: { namedDispatch: false, nested: false, maxDepth: 0, background: false, subagentToolkit: 'read-only', backgroundDispatch: false, isolation: 'none' },
51
76
  modelMode: 'passive',
52
77
  hookBus: 'none',
53
78
  stateIO: 'session-log-append',
54
79
  transport: 'mcp',
55
80
  runtime: 'node',
81
+ effortSurface: 'none',
56
82
  };
57
83
  const PROFILE_BASELINES = Object.freeze({
58
84
  'programmatic-cli': Object.freeze({
59
85
  embeddingMode: 'imperative',
60
86
  commandSurface: 'slash-file',
61
- dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
87
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
62
88
  modelMode: 'passive',
63
89
  hookBus: 'host',
64
90
  stateIO: 'filesystem',
65
91
  transport: 'mcp',
66
92
  runtime: 'node',
93
+ effortSurface: 'none',
67
94
  }),
68
95
  'declarative-cli': Object.freeze({
69
96
  embeddingMode: 'declarative',
70
97
  commandSurface: 'slash-file',
71
- dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false }),
98
+ dispatch: Object.freeze({ namedDispatch: true, nested: false, maxDepth: 1, background: false, subagentToolkit: 'full', backgroundDispatch: false, isolation: 'none' }),
72
99
  modelMode: 'passive',
73
100
  hookBus: 'host',
74
101
  stateIO: 'filesystem',
75
102
  transport: 'mcp',
76
103
  runtime: 'node',
104
+ effortSurface: 'none',
77
105
  }),
78
106
  'ide': Object.freeze({
79
107
  embeddingMode: 'imperative',
80
108
  commandSurface: 'palette',
81
- dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true }),
109
+ dispatch: Object.freeze({ namedDispatch: true, nested: true, maxDepth: 5, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' }),
82
110
  modelMode: 'active',
83
111
  hookBus: 'engine',
84
112
  stateIO: 'sandboxed-storage',
85
113
  transport: 'mcp',
86
114
  runtime: 'sandboxed-web',
115
+ effortSurface: 'none',
87
116
  }),
88
117
  });
89
118
  // ---------------------------------------------------------------------------
@@ -136,6 +165,13 @@ function degradationFor(point, axes) {
136
165
  return { level: 'degraded', fallback: 'flat dispatch — waves run inline' };
137
166
  }
138
167
  case 'model': {
168
+ // NOTE (#2481): the effortSurface axis is deliberately NOT folded into this
169
+ // level. `modelMode` has graded interface point 3 since Phase A, and every
170
+ // existing consumer reads it as "can GSD drive model selection". Widening it
171
+ // to also mean "…and deliver effort" silently redefines an established
172
+ // contract — an `active` host with no declared effort surface would flip
173
+ // from `full` to `absent`. Effort is negotiated on its own axis and read
174
+ // from `effective.effortSurface` by the consumers that care.
139
175
  const mm = axes.modelMode;
140
176
  if (mm === 'active')
141
177
  return { level: 'full', fallback: '' };
@@ -199,12 +235,13 @@ const DEFAULT_ENGINE = {
199
235
  axes: {
200
236
  embeddingMode: 'imperative',
201
237
  commandSurface: 'slash-file',
202
- dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true },
238
+ dispatch: { namedDispatch: true, nested: true, maxDepth: -1, background: true, subagentToolkit: 'full', backgroundDispatch: true, isolation: 'none' },
203
239
  modelMode: 'active',
204
240
  hookBus: 'host',
205
241
  stateIO: 'filesystem',
206
242
  transport: 'mcp',
207
243
  runtime: 'node',
244
+ effortSurface: 'argv',
208
245
  },
209
246
  known: HOST_INTEGRATION_AXES,
210
247
  };
@@ -264,6 +301,15 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
264
301
  if (hostVal === 'active' && engineVal === 'passive')
265
302
  return 'passive';
266
303
  }
304
+ // For effortSurface: 'argv' > 'none'. An engine that cannot deliver the
305
+ // host's richer channel caps the result to what it can drive.
306
+ if (axis === 'effortSurface') {
307
+ const RANK = { argv: 1, none: 0 };
308
+ const hr = RANK[hostVal] ?? 0;
309
+ const er = RANK[engineVal] ?? 0;
310
+ if (hr > er)
311
+ return engineVal;
312
+ }
267
313
  return hostVal;
268
314
  }
269
315
  // Negotiate all scalar axes
@@ -274,6 +320,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
274
320
  const effectiveStateIO = negotiateScalar('stateIO');
275
321
  const effectiveTransport = negotiateScalar('transport');
276
322
  const effectiveRuntime = negotiateScalar('runtime');
323
+ const effectiveEffortSurface = negotiateScalar('effortSurface');
277
324
  // ---------------------------------------------------------------------------
278
325
  // Dispatch struct negotiation
279
326
  // ---------------------------------------------------------------------------
@@ -287,6 +334,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
287
334
  let effectiveBackgroundDispatch;
288
335
  let effectiveSubagentToolkit;
289
336
  let effectiveMaxDepth;
337
+ let effectiveIsolation;
290
338
  if (hostDispatch === null) {
291
339
  // Host didn't declare dispatch at all — fail-closed to most-restrictive values
292
340
  warnings.push(`host did not declare 'dispatch'`);
@@ -296,6 +344,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
296
344
  effectiveBackgroundDispatch = false;
297
345
  effectiveSubagentToolkit = 'read-only';
298
346
  effectiveMaxDepth = 0;
347
+ effectiveIsolation = 'none';
299
348
  }
300
349
  else {
301
350
  // N1: observability warnings for 'undocumented' sentinel on dispatch fields
@@ -314,6 +363,16 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
314
363
  if (hostDispatch.backgroundDispatch === 'undocumented') {
315
364
  warnings.push(`dispatch.backgroundDispatch is undocumented — degraded closed`);
316
365
  }
366
+ if (hostDispatch.isolation === 'undocumented') {
367
+ warnings.push(`dispatch.isolation is undocumented — degraded closed (none)`);
368
+ }
369
+ if (hostDispatch.maxDepth === UNDOCUMENTED) {
370
+ // #2603: maxDepth was the one dispatch sub-axis with no sentinel-specific
371
+ // warning, so a descriptor carrying the documented fail-closed sentinel was
372
+ // reported as `missing or not a number` — indistinguishable from a genuinely
373
+ // malformed descriptor. Six shipped runtimes use the sentinel here.
374
+ warnings.push(`dispatch.maxDepth is undocumented — degraded closed (0)`);
375
+ }
317
376
  effectiveNamedDispatch = (hostDispatch.namedDispatch === true) && engineDispatch.namedDispatch;
318
377
  effectiveNested = (hostDispatch.nested === true) && engineDispatch.nested;
319
378
  effectiveBackground = (hostDispatch.background === true) && engineDispatch.background;
@@ -323,10 +382,22 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
323
382
  const hostToolkit = hostDispatch.subagentToolkit === 'full' ? 'full' : 'read-only';
324
383
  const engineToolkit = engineDispatch.subagentToolkit === 'read-only' ? 'read-only' : 'full';
325
384
  effectiveSubagentToolkit = (hostToolkit === 'read-only' || engineToolkit === 'read-only') ? 'read-only' : 'full';
326
- // maxDepth: missing/non-number/non-finite → 0 + warning
385
+ // isolation: effective = the host's declared value only if it is a known
386
+ // valid vocabulary member; otherwise 'none'. NOT host && engine gated —
387
+ // GSD owns the vocabulary, so "engine-known" == "in the valid set" (this
388
+ // still satisfies effective ⊆ host-declared ∩ engine-known).
389
+ const hostIso = hostDispatch.isolation;
390
+ effectiveIsolation = (typeof hostIso === 'string' && HOST_INTEGRATION_AXES.isolation.includes(hostIso))
391
+ ? hostIso
392
+ : 'none';
393
+ // maxDepth: missing/non-number/non-finite → 0 + warning. The documented
394
+ // 'undocumented' sentinel also degrades to 0, but is reported by the
395
+ // sentinel-specific warning above rather than as a malformed value (#2603).
327
396
  let hostMaxDepth;
328
397
  if (typeof hostDispatch.maxDepth !== 'number' || !Number.isFinite(hostDispatch.maxDepth)) {
329
- warnings.push(`host dispatch.maxDepth is missing or not a number — treating as 0`);
398
+ if (hostDispatch.maxDepth !== UNDOCUMENTED) {
399
+ warnings.push(`host dispatch.maxDepth is missing or not a number — treating as 0`);
400
+ }
330
401
  hostMaxDepth = 0;
331
402
  }
332
403
  else {
@@ -352,6 +423,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
352
423
  background: effectiveBackground,
353
424
  subagentToolkit: effectiveSubagentToolkit,
354
425
  backgroundDispatch: effectiveBackgroundDispatch,
426
+ isolation: effectiveIsolation,
355
427
  };
356
428
  // ---------------------------------------------------------------------------
357
429
  // Assemble effective axes
@@ -365,6 +437,7 @@ function negotiateHostCapabilities(host, engine = DEFAULT_ENGINE) {
365
437
  stateIO: effectiveStateIO,
366
438
  transport: effectiveTransport,
367
439
  runtime: effectiveRuntime,
440
+ effortSurface: effectiveEffortSurface,
368
441
  };
369
442
  // ---------------------------------------------------------------------------
370
443
  // Compute points (fresh objects — mutation-safe)
@@ -395,6 +468,63 @@ function shouldFlattenDispatch(dispatch) {
395
468
  return !canBackground;
396
469
  }
397
470
  // ---------------------------------------------------------------------------
471
+ // resolveDispatchType — ADR-1239 / epic #2505 Phase 4 (Option A)
472
+ //
473
+ // Maps a requested GSD subagent name (e.g. "gsd-planner") to the type an
474
+ // Agent() call should actually use on the CURRENT runtime. On runtimes whose
475
+ // descriptor declares `hostIntegration.dispatch.namedDispatch: true` (Claude,
476
+ // OpenCode, Cursor, …), the requested name is returned unchanged — those hosts
477
+ // can dispatch GSD's named subagents directly. On runtimes with
478
+ // `namedDispatch: false` (kimi-code — only three built-in subagents
479
+ // `coder`/`explore`/`plan`, per moonshotai.github.io/kimi-code/en/customization/
480
+ // agents), the name is mapped to the closest built-in by role-suffix
481
+ // heuristic. The persona rides the existing `${AGENT_SKILLS_*}` prompt
482
+ // injection (Phase 3 / #2510) regardless of the resolved type, so the
483
+ // dispatcher does not need to know the persona — only the toolkit tier.
484
+ //
485
+ // This is Option A of the Phase 4 design (per-workflow runtime detection via
486
+ // `gsd_run query resolve-dispatch-type`), not Option B (PreToolUse mutation) —
487
+ // Kimi Code's documented hook API supports only allow/deny on PreToolUse, not
488
+ // tool_input rewriting, so a hook-based remap is infeasible (see #2508).
489
+ //
490
+ // Fail-closed: unknown dispatch shape or missing namedDispatch axis ⇒ return
491
+ // the requested name unchanged (named-dispatch is the GSD default; degrading
492
+ // to it on unknown runtimes preserves behavior for every runtime already in
493
+ // the field).
494
+ // ---------------------------------------------------------------------------
495
+ // Role-suffix → built-in mapping. Order matters: the first match wins.
496
+ // `plan`-tier agents plan/design without touching files; `explore`-tier agents
497
+ // are read-only; everything else (executors, writers, fixers, debuggers) maps
498
+ // to `coder` (the general-purpose built-in with the full tool set).
499
+ const DISPATCH_TYPE_SUFFIX_MAP = Object.freeze([
500
+ [/-?(planner|roadmapper|selector|spec)$/i, 'plan'],
501
+ [/-?(researcher|mapper|checker|verifier|auditor|analyzer|synthesizer|profiler|curator|classifier|reviewer)$/i, 'explore'],
502
+ ]);
503
+ // Names that are already generic (not gsd-*) and should map to the
504
+ // general-purpose built-in on built-in-only runtimes.
505
+ const GENERIC_NAMES_TO_CODER = Object.freeze(new Set([
506
+ 'general-purpose', 'general', 'default', 'sonnet', 'opus', 'haiku',
507
+ ]));
508
+ function resolveDispatchType(requested, dispatch) {
509
+ if (typeof requested !== 'string' || requested.length === 0)
510
+ return 'coder';
511
+ // Built-in-only runtime (EXPLICIT namedDispatch: false, e.g. kimi-code):
512
+ // map to coder/explore/plan by suffix heuristic.
513
+ if (dispatch && typeof dispatch === 'object' && dispatch.namedDispatch === false) {
514
+ if (GENERIC_NAMES_TO_CODER.has(requested))
515
+ return 'coder';
516
+ for (const [pattern, builtin] of DISPATCH_TYPE_SUFFIX_MAP) {
517
+ if (pattern.test(requested))
518
+ return builtin;
519
+ }
520
+ return 'coder';
521
+ }
522
+ // Named-dispatch runtime (namedDispatch: true OR unknown/absent): use the
523
+ // requested name unchanged. Absent namedDispatch degrades to named-dispatch
524
+ // (the GSD default) so every runtime already in the field keeps working.
525
+ return requested;
526
+ }
527
+ // ---------------------------------------------------------------------------
398
528
  // Managed-hook event surface per hookEvents dialect (ADR-1239 / ADR-1016)
399
529
  // ---------------------------------------------------------------------------
400
530
  // Host-fireable MANAGED-hook events per `hookEvents` dialect. `hookEvents` is the
@@ -492,6 +622,81 @@ function extensionEventSurfaceFor(extensionEvents) {
492
622
  return null;
493
623
  return EXTENSION_EVENT_SURFACES[extensionEvents] || null;
494
624
  }
625
+ /**
626
+ * Resolve an `orchestratorExec` descriptor + target cwd (+ optional executor
627
+ * prompt) into a concrete argv/cwd shape for a process-spawn primitive.
628
+ *
629
+ * Fail-closed: never throws, always returns a discriminated result. When
630
+ * `cwdFlag` is a non-empty string, `[cwdFlag, cwd]` is appended to `args`
631
+ * exactly once (e.g. codex: `exec --cd <cwd>`); when `cwdFlag` is `null` or
632
+ * absent (e.g. kimi-code, which binds via the spawned process's own cwd —
633
+ * "process-cwd" case), no flag is appended and `cwd` is returned for the
634
+ * caller to bind via the subprocess's own working-directory option.
635
+ *
636
+ * Prompt passing (Phase 3, #2627) is descriptor data for the same reason the
637
+ * cwd flag is: the confirmed `orchestrator-worktree` hosts disagree on the
638
+ * shape. `codex exec "<prompt>"` and `opencode run "<prompt>"` take it
639
+ * positionally; `kimi --print --prompt "<p>"` and Kimi Code's `kimi -p "<p>"`
640
+ * take a flag. Encoding that as `promptFlag` keeps the scheduler free of the
641
+ * per-host branch ADR-1239 exists to remove. Omit `prompt` entirely and the
642
+ * resolution is byte-identical to Phase 2's (the unconsumed-resolver shape).
643
+ *
644
+ * Argv order is base args → cwd flag → prompt, so the prompt stays the final
645
+ * positional token for the hosts that read it that way.
646
+ */
647
+ function resolveOrchestratorExec(orchestratorExec, cwd, prompt) {
648
+ if (!orchestratorExec || typeof orchestratorExec !== 'object' || Array.isArray(orchestratorExec)) {
649
+ return { ok: false, reason: 'missing_command' };
650
+ }
651
+ const oe = orchestratorExec;
652
+ if (typeof oe.command !== 'string' || oe.command.length === 0) {
653
+ return { ok: false, reason: 'missing_command' };
654
+ }
655
+ if (typeof cwd !== 'string' || cwd.length === 0) {
656
+ return { ok: false, reason: 'invalid_cwd' };
657
+ }
658
+ if (oe.args !== undefined && (!Array.isArray(oe.args) || !oe.args.every((a) => typeof a === 'string'))) {
659
+ return { ok: false, reason: 'invalid_args' };
660
+ }
661
+ if (oe.cwdFlag !== undefined && oe.cwdFlag !== null && typeof oe.cwdFlag !== 'string') {
662
+ return { ok: false, reason: 'invalid_cwd_flag' };
663
+ }
664
+ if (oe.promptFlag !== undefined && oe.promptFlag !== null && typeof oe.promptFlag !== 'string') {
665
+ return { ok: false, reason: 'invalid_prompt_flag' };
666
+ }
667
+ // An executor spawned with no instruction is a hang, not a degraded run —
668
+ // fail closed rather than launching a prompt-less process.
669
+ if (prompt !== undefined && (typeof prompt !== 'string' || prompt.length === 0)) {
670
+ return { ok: false, reason: 'invalid_prompt' };
671
+ }
672
+ // Leading-dash guard, mirroring worktree-safety.cts's `unsafe_leading_dash`
673
+ // check on git arguments. A positional prompt (or a cwd) beginning with '-'
674
+ // is parsed by the spawned CLI as a FLAG, not a value — the same failure the
675
+ // git path already rejects, and for the same reason: `--` end-of-options
676
+ // support is inconsistent across these CLIs, so rejecting outright is the
677
+ // portable fix rather than relying on a separator. Applied to the resolver
678
+ // (not just its current caller) because this is a general descriptor->argv
679
+ // seam: a future caller must not have to rediscover the hazard.
680
+ if (typeof prompt === 'string' && prompt.startsWith('-')) {
681
+ return { ok: false, reason: 'unsafe_leading_dash_prompt' };
682
+ }
683
+ if (cwd.startsWith('-')) {
684
+ return { ok: false, reason: 'unsafe_leading_dash_cwd' };
685
+ }
686
+ const baseArgs = Array.isArray(oe.args) ? [...oe.args] : [];
687
+ const args = typeof oe.cwdFlag === 'string' && oe.cwdFlag.length > 0
688
+ ? [...baseArgs, oe.cwdFlag, cwd]
689
+ : baseArgs;
690
+ if (typeof prompt === 'string') {
691
+ if (typeof oe.promptFlag === 'string' && oe.promptFlag.length > 0) {
692
+ args.push(oe.promptFlag, prompt);
693
+ }
694
+ else {
695
+ args.push(prompt);
696
+ }
697
+ }
698
+ return { ok: true, command: oe.command, args, cwd };
699
+ }
495
700
  module.exports = {
496
701
  PROTOCOL_VERSION,
497
702
  UNDOCUMENTED,
@@ -505,6 +710,8 @@ module.exports = {
505
710
  profileOf,
506
711
  negotiateHostCapabilities,
507
712
  shouldFlattenDispatch,
713
+ resolveDispatchType,
508
714
  hookEventSurfaceFor,
509
715
  extensionEventSurfaceFor,
716
+ resolveOrchestratorExec,
510
717
  };
@@ -112,18 +112,6 @@ function listPhaseSummaryFiles(phaseDir) {
112
112
  function listPhasePlanFiles(phaseDir) {
113
113
  return scanPhasePlans(phaseDir)['planFiles'];
114
114
  }
115
- function verificationNextCommand(status, phaseNumber, slashRuntime) {
116
- if (status === 'gaps_found') {
117
- return `${(0, runtime_slash_cjs_1.formatGsdSlash)('plan-phase', slashRuntime)} ${phaseNumber} --gaps`;
118
- }
119
- if (status === 'human_needed' || status === 'stale') {
120
- return `${(0, runtime_slash_cjs_1.formatGsdSlash)('verify-work', slashRuntime)} ${phaseNumber}`;
121
- }
122
- if (status === 'missing' || status === 'unknown') {
123
- return `${(0, runtime_slash_cjs_1.formatGsdSlash)('execute-phase', slashRuntime)} ${phaseNumber}`;
124
- }
125
- return '';
126
- }
127
115
  function projectCompletionStatus(implementationComplete, verificationPassed) {
128
116
  if (implementationComplete && verificationPassed)
129
117
  return 'complete';
@@ -134,8 +122,14 @@ function projectCompletionStatus(implementationComplete, verificationPassed) {
134
122
  function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, summaryCount, slashRuntime) {
135
123
  const implementationComplete = planCount > 0 && summaryCount >= planCount;
136
124
  const phaseFullDir = phaseDir ? node_path_1.default.join(cwd, phaseDir) : '';
125
+ // #2617: ONE verification-routing seam. init used to re-derive next_command
126
+ // from the status with its own projector, which had drifted from the router's
127
+ // table — it appended the phase number and answered `human_needed`; the table
128
+ // did neither. The router now owns both the content and the runtime
129
+ // projection, and init passes the phase number it already knows (its phaseDir
130
+ // is unresolved in some branches, where the router could not derive one).
137
131
  const verificationStatus = implementationComplete
138
- ? readVerificationStatus(phaseFullDir)
132
+ ? readVerificationStatus(phaseFullDir, { runtime: slashRuntime, phaseNumber })
139
133
  : { status: 'not_required', next_action: '', next_command: '' };
140
134
  const projectedVerificationStatus = verificationStatus.status;
141
135
  const projectedVerificationAction = verificationStatus.next_action;
@@ -148,7 +142,7 @@ function buildPhaseCompletionProjection(cwd, phaseNumber, phaseDir, planCount, s
148
142
  phase_complete: phaseComplete,
149
143
  completion_status: projectCompletionStatus(implementationComplete, verificationPassed),
150
144
  verification_next_action: projectedVerificationAction,
151
- verification_next_command: verificationNextCommand(projectedVerificationStatus, phaseNumber, slashRuntime),
145
+ verification_next_command: verificationStatus.next_command,
152
146
  };
153
147
  }
154
148
  function getLatestCompletedMilestone(cwd) {
@@ -1854,7 +1848,38 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
1854
1848
  const projectRoot = (0, project_root_cjs_1.findProjectRoot)(cwd);
1855
1849
  const { config, source, degraded } = loadConfigResolved(projectRoot);
1856
1850
  const diagnostics = { warnings: [] };
1857
- const block = buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics);
1851
+ let block = buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics);
1852
+ // #2454: Agent prompt fallback for AGENTS-native runtimes where named
1853
+ // subagents are NOT dispatchable (kimi-code, kimi, opencode, kilo, etc.).
1854
+ // On these runtimes, workflows inject ${AGENT_SKILLS_*} into the dispatch
1855
+ // prompt of a built-in subagent (coder/explore/plan). If no
1856
+ // model_profile_overrides or agent_skills config entry exists, the block
1857
+ // is empty — but the agent's prompt CONTENT is installed on disk at the
1858
+ // runtime's agents directory. Read it as a fallback so the persona survives
1859
+ // the dispatch even without explicit config opt-in.
1860
+ //
1861
+ // GATED to non-claude runtimes: Claude Code supports named subagent dispatch
1862
+ // and its ${AGENT_SKILLS_*} contract is a skills-injection path, not a
1863
+ // persona fallback. Triggering the fallback for claude would change the
1864
+ // documented "unconfigured → empty block" contract that agent-skills tests
1865
+ // pin.
1866
+ if (!block) {
1867
+ const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
1868
+ if (runtime !== 'claude') {
1869
+ const agentCheck = checkAgentsInstalled(runtime);
1870
+ const agentsDir = agentCheck?.agents_dir;
1871
+ if (typeof agentsDir === 'string' && agentsDir.length > 0) {
1872
+ const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
1873
+ try {
1874
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(agentFile);
1875
+ if (content && content.length > 0) {
1876
+ block = content;
1877
+ }
1878
+ }
1879
+ catch { /* agent file not found — fall through to empty block */ }
1880
+ }
1881
+ }
1882
+ }
1858
1883
  // Compute configured + reason for diagnostic output.
1859
1884
  const agentSkillsMap = (config && config['agent_skills'] && typeof config['agent_skills'] === 'object')
1860
1885
  ? config['agent_skills']
@@ -2053,8 +2078,8 @@ function buildSkillManifest(cwd, skillsDir = null) {
2053
2078
  // posix-style for cross-platform stability; flat entries use template
2054
2079
  // literals that always produce '/'; nested entries are joined below
2055
2080
  // with explicit '/' separators rather than path.join).
2056
- relPath, content) {
2057
- const frontmatter = extractFrontmatter(content);
2081
+ relPath, content, sourcePath) {
2082
+ const frontmatter = extractFrontmatter(content, sourcePath);
2058
2083
  const dirPart = relPath.replace(/\/SKILL\.md$/, '');
2059
2084
  const stem = dirPart.includes('/') ? dirPart.split('/').pop() : dirPart;
2060
2085
  const name = frontmatter['name'] || stem;
@@ -2095,7 +2120,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
2095
2120
  const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md');
2096
2121
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath);
2097
2122
  if (content !== null) {
2098
- if (pushSkillEntry(`${entry.name}/SKILL.md`, content))
2123
+ if (pushSkillEntry(`${entry.name}/SKILL.md`, content, skillMdPath))
2099
2124
  skillCount++;
2100
2125
  }
2101
2126
  // Nested layout: <entry>/skills/<stem>/SKILL.md
@@ -2124,7 +2149,7 @@ function buildSkillManifest(cwd, skillsDir = null) {
2124
2149
  // Use forward-slash separator explicitly so manifest paths are posix-style
2125
2150
  // on all platforms, matching the flat-layout behaviour above.
2126
2151
  const relPath = `${entry.name}/skills/${nested.name}/SKILL.md`;
2127
- if (pushSkillEntry(relPath, nestedContent))
2152
+ if (pushSkillEntry(relPath, nestedContent, nestedSkillMd))
2128
2153
  skillCount++;
2129
2154
  }
2130
2155
  }
@@ -115,6 +115,7 @@ function convertClaudeCommandToKiloSkill(content, skillName) {
115
115
  const SKILLS_CONVERTER_REGISTRY = {
116
116
  convertClaudeCommandToOpencodeSkill,
117
117
  convertClaudeCommandToKiloSkill,
118
+ convertClaudeCommandToKimiCodeSkill: runtimeArtifactConversion.convertClaudeCommandToKimiCodeSkill,
118
119
  };
119
120
  // ---------------------------------------------------------------------------
120
121
  // User-artifact preservation helpers
@@ -282,8 +282,8 @@ function cmdRequirementsReadyIds(cwd, args, raw) {
282
282
  catch {
283
283
  siblingPlanFiles = [];
284
284
  }
285
- const parseFrontmatterReqIds = (content) => {
286
- const fm = extractFrontmatter(content);
285
+ const parseFrontmatterReqIds = (content, sourcePath) => {
286
+ const fm = extractFrontmatter(content, sourcePath);
287
287
  const fmReq = fm.requirements;
288
288
  if (Array.isArray(fmReq))
289
289
  return fmReq.map((r) => String(r).trim()).filter(Boolean);
@@ -309,7 +309,7 @@ function cmdRequirementsReadyIds(cwd, args, raw) {
309
309
  catch {
310
310
  continue;
311
311
  }
312
- const siblingReqIds = parseFrontmatterReqIds(siblingContent);
312
+ const siblingReqIds = parseFrontmatterReqIds(siblingContent, siblingPath);
313
313
  const siblingDeclaresId = siblingReqIds.some((id) => id.toLowerCase() === reqId.toLowerCase());
314
314
  if (!siblingDeclaresId)
315
315
  continue;
@@ -531,7 +531,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
531
531
  for (const s of summaries) {
532
532
  try {
533
533
  const content = node_fs_1.default.readFileSync(node_path_1.default.join(phasesDir, dir, s), 'utf-8');
534
- const fm = extractFrontmatter(content);
534
+ const fm = extractFrontmatter(content, node_path_1.default.join(phasesDir, dir, s));
535
535
  const rawOneLiner = fm['one-liner'];
536
536
  const oneLiner = (typeof rawOneLiner === 'string' ? rawOneLiner : '') || extractOneLinerFromBody(content);
537
537
  if (oneLiner) {
@@ -668,7 +668,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
668
668
  kind: 'milestoneComplete',
669
669
  version,
670
670
  nextMilestoneCommand: (0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd)),
671
- }, { clock: clock_cjs_1.realClock, progressProvider: () => null });
671
+ }, { clock: clock_cjs_1.realClock, progressProvider: () => null, sourcePath: statePath });
672
672
  writeStateMd(statePath, result.content, cwd);
673
673
  }
674
674
  // Archive phase directories if requested
@@ -10,10 +10,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
13
+ exports.RUNTIMES_WITH_FAST_MODE = exports.EFFORT_ARGV = exports.EFFORT_RENDERING = exports.KNOWN_PROVIDERS = exports.PROVIDER_PRESETS = exports.RUNTIMES_WITH_REASONING_EFFORT = exports.KNOWN_RUNTIMES = exports.RUNTIME_PROFILE_MAP = exports.MODEL_ALIAS_MAP = exports.AGENT_DEFAULT_TIERS = exports.AGENT_TO_PHASE_TYPE = exports.MODEL_PROFILES = exports.ADAPTIVE_TIER_VALUES = exports.VALID_TIERS = exports.VALID_AGENT_TIERS = exports.VALID_PHASE_TYPES = exports.VALID_PROFILES = exports.catalog = void 0;
14
14
  exports.nextTier = nextTier;
15
15
  exports.formatAgentToModelMapAsTable = formatAgentToModelMapAsTable;
16
16
  exports.getAgentToModelMapForProfile = getAgentToModelMapForProfile;
17
+ exports.renderEffortArgv = renderEffortArgv;
17
18
  exports.renderEffortForRuntime = renderEffortForRuntime;
18
19
  const node_path_1 = __importDefault(require("node:path"));
19
20
  // In .cts (CommonJS output) files, `require` is available as a global;
@@ -151,6 +152,55 @@ exports.EFFORT_RENDERING = {
151
152
  },
152
153
  },
153
154
  };
155
+ exports.EFFORT_ARGV = {
156
+ // Verified against `claude --help`: `--effort <level>`.
157
+ claude: {
158
+ render: (level) => ['--effort', level],
159
+ supported: new Set(['low', 'medium', 'high', 'xhigh', 'max']),
160
+ clamp: (level) => (level === 'minimal' ? 'low' : level),
161
+ },
162
+ // Verified against `opencode run --help`: `--variant` — "model variant
163
+ // (provider-specific reasoning effort, e.g., high, max, minimal)".
164
+ opencode: {
165
+ render: (level) => ['--variant', level],
166
+ supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']),
167
+ clamp: (level) => level,
168
+ },
169
+ // First-party Codex docs: `model_reasoning_effort` is a config-only key with no
170
+ // dedicated flag, so the generic `-c key=value` override is the only argv route.
171
+ codex: {
172
+ render: (level) => ['-c', `model_reasoning_effort=${level}`],
173
+ supported: new Set(['minimal', 'low', 'medium', 'high', 'xhigh']),
174
+ clamp: (level) => (level === 'max' ? 'xhigh' : level),
175
+ },
176
+ };
177
+ /**
178
+ * Render the invocation-time effort argument for a host.
179
+ *
180
+ * `effortSurface` is the host's negotiated axis value. Only `argv` produces an
181
+ * argument; `none`, `undocumented`, and anything unrecognised produce nothing.
182
+ * Never throws.
183
+ */
184
+ function renderEffortArgv(host, universalEffort, effortSurface) {
185
+ const empty = { argv: [], value: null, host };
186
+ if (effortSurface !== 'argv')
187
+ return empty;
188
+ // Own-property lookup only. A plain `EFFORT_ARGV[host]` resolves `__proto__`
189
+ // (and `constructor`/`toString`) to inherited members, which are truthy but
190
+ // carry no `clamp`/`render` — a hostile host id would throw instead of
191
+ // degrading. The host id reaches here from a descriptor, i.e. untrusted JSON.
192
+ if (typeof host !== 'string' || !Object.prototype.hasOwnProperty.call(exports.EFFORT_ARGV, host))
193
+ return empty;
194
+ const spec = exports.EFFORT_ARGV[host];
195
+ if (!spec || typeof spec.clamp !== 'function' || typeof spec.render !== 'function')
196
+ return empty;
197
+ if (typeof universalEffort !== 'string' || universalEffort.length === 0)
198
+ return empty;
199
+ const clamped = spec.clamp(universalEffort);
200
+ if (!spec.supported.has(clamped))
201
+ return empty;
202
+ return { argv: spec.render(clamped), value: clamped, host };
203
+ }
154
204
  /**
155
205
  * Render a universal effort string for a specific runtime.
156
206
  */
@@ -41,7 +41,12 @@ function _safeStringify(value) {
41
41
  }
42
42
  }
43
43
  /**
44
- * Determine whether the audit file should be written to.
44
+ * Determine whether observability is opt-in enabled — via the GSD_AUDIT env var
45
+ * or config.audit.enabled. Exported (#2620) so the live dispatch seam can decide
46
+ * whether to inject the reference logger at all: when observability is off we
47
+ * inject nothing (the Hub stays byte-for-byte silent, preserving the default
48
+ * dispatch output contract, incl. --json-errors); when on, the caller injects
49
+ * createDefaultLogger and gets the stderr-on-error line + opt-in file audit.
45
50
  */
46
51
  function _isAuditEnabled(config) {
47
52
  if (process.env['GSD_AUDIT'] === '1')
@@ -143,4 +148,4 @@ function createDefaultLogger({ cwd = process.cwd(), config } = {}) {
143
148
  },
144
149
  };
145
150
  }
146
- module.exports = { createDefaultLogger, createNoOpLogger };
151
+ module.exports = { createDefaultLogger, createNoOpLogger, isAuditEnabled: _isAuditEnabled };
@@ -20,6 +20,12 @@ const command_aliases_cjs_1 = require("./command-aliases.cjs");
20
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
21
  const commandRoutingHub = require("./command-routing-hub.cjs");
22
22
  const { createHub, ERROR_KINDS, makeInvalidArgs } = commandRoutingHub;
23
+ // #2620 (ADR-0174 §6): inject the reference DispatchLogger on the live phase
24
+ // dispatch path, but only when observability is opt-in enabled; otherwise the
25
+ // Hub stays byte-for-byte silent via its no-op fallback.
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const observabilityLogger = require("./observability/logger.cjs");
28
+ const { createDefaultLogger, isAuditEnabled } = observabilityLogger;
23
29
  // ─── Implementation ───────────────────────────────────────────────────────────
24
30
  function routePhaseCommand({ phase, args, cwd, raw, error }) {
25
31
  // ── Unsupported subcommands ─────────────────────────────────────────────────
@@ -229,7 +235,10 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
229
235
  const manifest = { phase: manifestSubcommands };
230
236
  // ── Construct hub ──────────────────────────────────────────────────────────
231
237
  // #175: Hub is CJS-only — no mode param, no sdkLoader.
232
- const hub = createHub({ cjsRegistry, manifest });
238
+ // #2620: wire the reference logger (ADR-0174 §6) only when observability is
239
+ // opt-in enabled; otherwise leave it unset so the Hub stays byte-for-byte
240
+ // silent via its no-op fallback.
241
+ const hub = createHub({ cjsRegistry, manifest, logger: isAuditEnabled() ? createDefaultLogger({ cwd }) : undefined });
233
242
  // ── Dispatch ────────────────────────────────────────────────────────────────
234
243
  const result = hub.dispatch({
235
244
  family: 'phase',