@opengsd/gsd-core 1.16.0 → 1.18.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -2
  3. package/.opencode/plugins/gsd-core.js +251 -1
  4. package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
  5. package/GEMINI.md +1 -1
  6. package/agents/gsd-advisor-researcher.compact.md +1 -1
  7. package/agents/gsd-advisor-researcher.md +1 -1
  8. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  9. package/agents/gsd-assumptions-analyzer.md +1 -1
  10. package/agents/gsd-code-fixer.compact.md +1 -1
  11. package/agents/gsd-code-fixer.md +1 -1
  12. package/agents/gsd-code-reviewer.compact.md +1 -1
  13. package/agents/gsd-code-reviewer.md +1 -1
  14. package/agents/gsd-codebase-mapper.compact.md +1 -1
  15. package/agents/gsd-codebase-mapper.md +1 -1
  16. package/agents/gsd-debug-session-manager.compact.md +6 -0
  17. package/agents/gsd-debug-session-manager.md +6 -0
  18. package/agents/gsd-debugger.md +1 -1
  19. package/agents/gsd-doc-verifier.compact.md +4 -4
  20. package/agents/gsd-doc-verifier.md +4 -3
  21. package/agents/gsd-doc-writer.compact.md +23 -5
  22. package/agents/gsd-doc-writer.md +19 -6
  23. package/agents/gsd-eval-auditor.compact.md +1 -1
  24. package/agents/gsd-eval-auditor.md +1 -1
  25. package/agents/gsd-executor.md +7 -3
  26. package/agents/gsd-integration-checker.compact.md +1 -1
  27. package/agents/gsd-integration-checker.md +1 -1
  28. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  29. package/agents/gsd-nyquist-auditor.md +1 -1
  30. package/agents/gsd-phase-researcher.md +1 -1
  31. package/agents/gsd-plan-checker.md +2 -1
  32. package/agents/gsd-planner.md +5 -4
  33. package/agents/gsd-project-researcher.compact.md +1 -1
  34. package/agents/gsd-project-researcher.md +1 -1
  35. package/agents/gsd-research-synthesizer.compact.md +5 -5
  36. package/agents/gsd-research-synthesizer.md +5 -5
  37. package/agents/gsd-roadmapper.compact.md +1 -1
  38. package/agents/gsd-roadmapper.md +1 -1
  39. package/agents/gsd-security-auditor.compact.md +1 -1
  40. package/agents/gsd-security-auditor.md +1 -1
  41. package/agents/gsd-ui-auditor.compact.md +1 -1
  42. package/agents/gsd-ui-auditor.md +1 -1
  43. package/agents/gsd-ui-checker.compact.md +1 -1
  44. package/agents/gsd-ui-checker.md +1 -1
  45. package/agents/gsd-ui-researcher.compact.md +13 -1
  46. package/agents/gsd-ui-researcher.md +28 -1
  47. package/agents/gsd-verifier.md +1 -1
  48. package/bin/gsd-mcp-server.js +3 -1
  49. package/bin/install.js +398 -95
  50. package/commands/gsd/code-review.md +8 -0
  51. package/commands/gsd/pause-work.md +1 -1
  52. package/gsd-core/bin/gsd-tools.cjs +34 -12
  53. package/gsd-core/bin/lib/adr-parser.cjs +37 -5
  54. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  55. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  56. package/gsd-core/bin/lib/capability-registry.cjs +138 -80
  57. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  58. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  59. package/gsd-core/bin/lib/capability-validator.cjs +8 -5
  60. package/gsd-core/bin/lib/capability-writer.cjs +2 -1
  61. package/gsd-core/bin/lib/commands.cjs +33 -9
  62. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  63. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  64. package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
  65. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  66. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  67. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  68. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  69. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  70. package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
  71. package/gsd-core/bin/lib/graphify.cjs +3 -2
  72. package/gsd-core/bin/lib/init.cjs +43 -16
  73. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  74. package/gsd-core/bin/lib/install-engine.cjs +11 -0
  75. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  76. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  77. package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
  78. package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
  79. package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
  80. package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
  81. package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
  82. package/gsd-core/bin/lib/mcp-server.cjs +5 -1
  83. package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
  84. package/gsd-core/bin/lib/opencode-version.cjs +486 -0
  85. package/gsd-core/bin/lib/phase.cjs +141 -11
  86. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
  87. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  88. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  89. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  90. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  91. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
  92. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  93. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
  94. package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
  95. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  96. package/gsd-core/bin/lib/smart-entry.cjs +100 -53
  97. package/gsd-core/bin/lib/state-document.cjs +37 -3
  98. package/gsd-core/bin/lib/state-transition.cjs +98 -19
  99. package/gsd-core/bin/lib/state.cjs +544 -126
  100. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  101. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  102. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  103. package/gsd-core/bin/lib/update-context.cjs +18 -1
  104. package/gsd-core/bin/lib/verification.cjs +107 -17
  105. package/gsd-core/bin/lib/verify.cjs +36 -1
  106. package/gsd-core/references/agent-contracts.md +1 -1
  107. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  108. package/gsd-core/references/git-integration.md +3 -3
  109. package/gsd-core/references/planner-reversibility.md +18 -4
  110. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  111. package/gsd-core/references/tdd.md +4 -3
  112. package/gsd-core/references/verifier-phase-gates.md +8 -1
  113. package/gsd-core/workflows/add-backlog.md +10 -2
  114. package/gsd-core/workflows/add-phase.md +5 -1
  115. package/gsd-core/workflows/add-tests.md +5 -1
  116. package/gsd-core/workflows/add-todo.md +5 -1
  117. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  118. package/gsd-core/workflows/audit-milestone.md +21 -9
  119. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  120. package/gsd-core/workflows/autonomous.md +52 -64
  121. package/gsd-core/workflows/check-todos.md +5 -1
  122. package/gsd-core/workflows/code-review-fix.md +5 -1
  123. package/gsd-core/workflows/code-review.md +21 -2
  124. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  125. package/gsd-core/workflows/complete-milestone.md +25 -5
  126. package/gsd-core/workflows/debug.md +5 -1
  127. package/gsd-core/workflows/diagnose-issues.md +5 -1
  128. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  129. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  130. package/gsd-core/workflows/discuss-phase.md +5 -1
  131. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  132. package/gsd-core/workflows/docs-update.md +27 -5
  133. package/gsd-core/workflows/edit-phase.md +5 -1
  134. package/gsd-core/workflows/eval-review.md +11 -3
  135. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  136. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
  137. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  138. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  139. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  140. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  141. package/gsd-core/workflows/execute-phase.md +40 -7
  142. package/gsd-core/workflows/execute-plan.md +8 -4
  143. package/gsd-core/workflows/extract-learnings.md +6 -2
  144. package/gsd-core/workflows/forensics.md +5 -1
  145. package/gsd-core/workflows/import.md +5 -1
  146. package/gsd-core/workflows/insert-phase.md +5 -1
  147. package/gsd-core/workflows/manager.md +10 -2
  148. package/gsd-core/workflows/map-codebase.md +5 -1
  149. package/gsd-core/workflows/milestone-summary.md +10 -2
  150. package/gsd-core/workflows/new-milestone.md +13 -5
  151. package/gsd-core/workflows/new-project.md +9 -5
  152. package/gsd-core/workflows/next.md +8 -4
  153. package/gsd-core/workflows/pause-work.md +3 -3
  154. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  155. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  156. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  157. package/gsd-core/workflows/plan-phase.md +41 -73
  158. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  159. package/gsd-core/workflows/plant-seed.md +8 -4
  160. package/gsd-core/workflows/progress.md +6 -2
  161. package/gsd-core/workflows/quick-batch.md +5 -1
  162. package/gsd-core/workflows/quick.md +22 -7
  163. package/gsd-core/workflows/remove-phase.md +5 -1
  164. package/gsd-core/workflows/resume-project.md +19 -11
  165. package/gsd-core/workflows/review.md +6 -2
  166. package/gsd-core/workflows/scan.md +5 -1
  167. package/gsd-core/workflows/secure-phase.md +6 -2
  168. package/gsd-core/workflows/settings.md +3 -3
  169. package/gsd-core/workflows/ship.md +13 -10
  170. package/gsd-core/workflows/thread.md +10 -2
  171. package/gsd-core/workflows/transition.md +5 -1
  172. package/gsd-core/workflows/ui-phase.md +5 -1
  173. package/gsd-core/workflows/ui-review.md +6 -2
  174. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  175. package/gsd-core/workflows/update.md +5 -1
  176. package/gsd-core/workflows/validate-phase.md +6 -2
  177. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  178. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  179. package/gsd-core/workflows/verify-work.md +11 -3
  180. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  181. package/hooks/dist/gsd-check-update.js +26 -20
  182. package/hooks/dist/gsd-context-monitor.js +14 -5
  183. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  184. package/hooks/dist/gsd-secret-read-guard.js +189 -15
  185. package/hooks/dist/gsd-statusline.js +321 -22
  186. package/hooks/dist/gsd-validate-commit.sh +27 -5
  187. package/hooks/gsd-agent-isolation-guard.js +3 -1
  188. package/hooks/gsd-check-update.js +26 -20
  189. package/hooks/gsd-context-monitor.js +14 -5
  190. package/hooks/gsd-cursor-subagent-start.js +2 -1
  191. package/hooks/gsd-secret-read-guard.js +189 -15
  192. package/hooks/gsd-statusline.js +321 -22
  193. package/hooks/gsd-validate-commit.sh +27 -5
  194. package/package.json +1 -1
  195. package/scripts/audit-workflow-script-paths.cjs +1 -1
  196. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  197. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  198. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  199. package/scripts/docs-guard-registry.cjs +2 -0
  200. package/scripts/gen-capability-registry.cjs +7 -1
  201. package/scripts/gen-loop-host-contract.cjs +4 -1
  202. package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
  203. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
  204. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  205. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  206. package/scripts/mutation-matrix.cjs +3 -1
  207. package/scripts/release-tarball-smoke.cjs +15 -7
  208. package/scripts/xtrace-phase-timing.cjs +205 -0
  209. package/skills/gsd-code-review/SKILL.md +8 -0
  210. package/skills/gsd-pause-work/SKILL.md +1 -1
  211. package/vscode/package.json +1 -1
@@ -32,6 +32,8 @@ const node_os_1 = __importDefault(require("node:os"));
32
32
  const node_https_1 = __importDefault(require("node:https"));
33
33
  const node_crypto_1 = __importDefault(require("node:crypto"));
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const { resolveGsdHome } = require('./runtime-homes.cjs');
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
37
  const shellSeam = require('./shell-command-projection.cjs');
36
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
39
  const capValidator = require('./capability-validator.cjs');
@@ -984,7 +986,7 @@ async function resolveTarball(parsed, opts, gsdHome, hostVersion) {
984
986
  async function resolveCapabilitySource(spec, opts = {}) {
985
987
  const parsed = parseSpec(spec);
986
988
  const hostVersion = opts.hostVersion ?? readHostVersion();
987
- const gsdHome = opts.gsdHome ?? process.env['GSD_HOME'] ?? node_os_1.default.homedir();
989
+ const gsdHome = opts.gsdHome || resolveGsdHome();
988
990
  switch (parsed.kind) {
989
991
  case 'local':
990
992
  return resolveLocal(parsed, opts, gsdHome, hostVersion);
@@ -47,6 +47,8 @@ const { loadConfig } = configLoaderMod;
47
47
  const installProfilesMod = require("./install-profiles.cjs");
48
48
  const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires, parseCallsAgents, workflowAgentRefs } = installProfilesMod;
49
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ const ledgerMod = require("./capability-ledger.cjs");
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
52
  const surfaceMod = require("./surface.cjs");
51
53
  const { resolveSurface } = surfaceMod;
52
54
  // ─── Prototype-pollution guard (inline literal, CodeQL barrier) ───────────────
@@ -265,16 +267,17 @@ function _loadInstalledSkillsManifest(configDir) {
265
267
  // SKILL.md must NOT register the stem — otherwise the capability would be
266
268
  // wrongly reported surfaced/enabled and a verify:post hook would render
267
269
  // for a skill that cannot run.
268
- if (!node_fs_1.default.existsSync(skillMdPath))
269
- continue;
270
+ // #5121: bounded read — a missing, non-regular (FIFO/device) or oversized SKILL.md registers no stem.
270
271
  let content = '';
271
272
  try {
272
- content = node_fs_1.default.readFileSync(skillMdPath, 'utf8');
273
+ content = ledgerMod.readSkillFile(skillMdPath);
273
274
  }
274
275
  catch {
275
276
  // SKILL.md present but unreadable — register with no deps (parity with
276
277
  // loadSkillsManifest's readFileSync catch branch).
277
278
  }
279
+ if (content === null)
280
+ continue;
278
281
  // Parse requires: via the SAME shared parser loadSkillsManifest uses, so
279
282
  // installed-runtime dependency resolution can never silently diverge from
280
283
  // the source-tree path (single source of truth — no duplicated regex).
@@ -1899,6 +1899,10 @@ const REVIEWER_EFFORT_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhi
1899
1899
  * silently rot away from reality (DEFECT.GENERATIVE-FIX).
1900
1900
  */
1901
1901
  const KNOWN_HOST_BEHAVIORS = new Set([
1902
+ // #2586: declares runtime-level feature axes GSD does not/cannot support on
1903
+ // omitting the key on every other runtime carries no inverted meaning.
1904
+ // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1905
+ // — present-and-populated / absent-is-unsupported-empty convention, so
1902
1906
  'agentFileExtension',
1903
1907
  'agentFrontmatterExtensions',
1904
1908
  'agentManifestStyle',
@@ -1916,6 +1920,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1916
1920
  'combinedFamilyInstall',
1917
1921
  'commandBodyConverter',
1918
1922
  'contentRewriteProfile',
1923
+ 'defaultHomeIncludeRoot',
1919
1924
  'doneBannerStyle',
1920
1925
  'flatCommandDir',
1921
1926
  'frontmatterDialect',
@@ -1936,6 +1941,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1936
1941
  'managedHookEvents',
1937
1942
  'mcpCompanion',
1938
1943
  'namedSubagentsSupported',
1944
+ 'nativeCliPlugin',
1939
1945
  'nativeModelAliases',
1940
1946
  'nativePlugin',
1941
1947
  'noPathRewrite',
@@ -1952,6 +1958,7 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1952
1958
  'requiresSubagentPair',
1953
1959
  'restoreAtRefTildeInAgents',
1954
1960
  'restoreAtRefTildeInSpecTree',
1961
+ 'retargetsGsdCoreProsePath',
1955
1962
  'retiredArtifacts',
1956
1963
  'rewriteClaudeAtIncludes',
1957
1964
  'settingsFileByScope',
@@ -1971,10 +1978,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1971
1978
  'specRootSkillPass',
1972
1979
  'tomlConfigInstall',
1973
1980
  'trackCategoryDescription',
1974
- // #2586: declares runtime-level feature axes GSD does not/cannot support on
1975
- // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1976
- // — present-and-populated / absent-is-unsupported-empty convention, so
1977
- // omitting the key on every other runtime carries no inverted meaning.
1978
1981
  'unsupportedFeatures',
1979
1982
  'verificationStyle',
1980
1983
  'writeCategoryDescription',
@@ -2572,7 +2575,7 @@ function validateLaneProbe(ctx, probe) {
2572
2575
  if (!isPositiveIntegerMs(probe.timeoutMs)) {
2573
2576
  errors.push(
2574
2577
  ctx + ' reviewer.probe.timeoutMs must be a positive integer of milliseconds for kind "' +
2575
- probe.kind + '" — an unbounded probe hangs every /gsd:review invocation ' +
2578
+ probe.kind + '" — an unbounded probe hangs every /gsd-review invocation ' +
2576
2579
  '(got: ' + describeValue(probe.timeoutMs) + ')',
2577
2580
  );
2578
2581
  }
@@ -50,6 +50,7 @@ const { planningDir } = planningWorkspaceMod;
50
50
  const nodefs = require("fs");
51
51
  // eslint-disable-next-line @typescript-eslint/no-require-imports
52
52
  const nodepath = require("path");
53
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
53
54
  // ─── Implementation ───────────────────────────────────────────────────────────
54
55
  /**
55
56
  * Write-side capability state mutator.
@@ -305,7 +306,7 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
305
306
  if (entry.enabled === true) {
306
307
  const afterCap = after.capabilities.find((c) => c.id === entry.id);
307
308
  if (afterCap && afterCap.enabled !== true) {
308
- errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use /gsd:surface enable)`);
309
+ errors.push(`cannot enable "${entry.id}": not in the active surface/profile (widen the profile or use ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'surface enable')})`);
309
310
  }
310
311
  }
311
312
  // Fix B: desired enabled=false — assert it is actually disabled after write.
@@ -1233,6 +1233,24 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1233
1233
  // null — it would leave an unresolvable `variant: null` key on disk
1234
1234
  // forever. Test presence with its own regex, and only compare values
1235
1235
  // once presence is known.
1236
+ // Native OpenCode 2.x form (ADR-5331 D5): the effort rides on the model reference as
1237
+ // `provider/model#variant`, with no separate `variant:` key. When the agent's `model:` carries a
1238
+ // `#` suffix that suffix IS the effort, so the sync edits the model instead of adding a second,
1239
+ // conflicting `variant:` line.
1240
+ const modelLineMatch = /^model:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1241
+ const modelValue = modelLineMatch ? modelLineMatch[1].replace(/\r$/, '').replace(/^(["'])(.*)\1$/, '$2') : null;
1242
+ const hashAt = modelValue === null ? -1 : modelValue.lastIndexOf('#');
1243
+ let modelFormContent = null;
1244
+ let modelFormFrom = null;
1245
+ if (modelValue !== null && hashAt > 0) {
1246
+ const base = modelValue.slice(0, hashAt);
1247
+ modelFormFrom = modelValue.slice(hashAt + 1);
1248
+ if (target !== null && modelFormFrom === target) {
1249
+ skipped++;
1250
+ continue;
1251
+ }
1252
+ modelFormContent = setFrontmatterKeyLine(content, 'model', target === null ? base : `${base}#${target}`);
1253
+ }
1236
1254
  const variantPresent = /^variant:/m.test(fmMatch.body);
1237
1255
  const variantMatch = /^variant:[ \t]*(.+?)[ \t]*$/m.exec(fmMatch.body);
1238
1256
  // `null` (key absent) and `''` (key present, value empty) are distinct
@@ -1241,17 +1259,19 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1241
1259
  // "empty key" indistinguishable in the sync output, even though only one
1242
1260
  // of them actually has a `variant:` line to remove.
1243
1261
  const currentVariant = variantPresent ? (variantMatch ? variantMatch[1] : '') : null;
1244
- if (target === null) {
1245
- if (!variantPresent) {
1262
+ if (modelFormContent === null) {
1263
+ if (target === null) {
1264
+ if (!variantPresent) {
1265
+ skipped++;
1266
+ continue;
1267
+ }
1268
+ }
1269
+ else if (variantPresent && currentVariant === target) {
1246
1270
  skipped++;
1247
1271
  continue;
1248
1272
  }
1249
1273
  }
1250
- else if (variantPresent && currentVariant === target) {
1251
- skipped++;
1252
- continue;
1253
- }
1254
- changes.push({ agent: agentName, from: currentVariant, to: target });
1274
+ changes.push({ agent: agentName, from: modelFormContent === null ? currentVariant : modelFormFrom, to: target });
1255
1275
  synced++;
1256
1276
  if (!dryRun) {
1257
1277
  // Atomic publish AND mode preservation, same discipline as
@@ -1279,7 +1299,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
1279
1299
  }
1280
1300
  catch { /* non-fatal: fall back to writing without an explicit mode */ }
1281
1301
  try {
1282
- node_fs_1.default.writeFileSync(tmpPath, target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target), originalMode !== undefined ? { mode: originalMode } : undefined);
1302
+ node_fs_1.default.writeFileSync(tmpPath, modelFormContent ?? (target === null ? removeFrontmatterKeyLine(content, 'variant') : setFrontmatterKeyLine(content, 'variant', target)), originalMode !== undefined ? { mode: originalMode } : undefined);
1283
1303
  // Not redundant with the `mode` option above: `mode` only applies
1284
1304
  // when the file is actually created (O_CREAT). A leftover tmp file
1285
1305
  // from an earlier crashed run would be reused (truncated) at its OLD
@@ -2966,6 +2986,8 @@ async function cmdWebsearch(query, options, raw) {
2966
2986
  'Accept': 'application/json',
2967
2987
  'X-Subscription-Token': apiKey
2968
2988
  },
2989
+ // never follow redirects: X-Subscription-Token must not leave api.search.brave.com (#5409)
2990
+ redirect: 'manual',
2969
2991
  signal: ac.signal
2970
2992
  });
2971
2993
  }
@@ -2991,7 +3013,9 @@ async function cmdWebsearch(query, options, raw) {
2991
3013
  const status = response.status;
2992
3014
  const isRetryable = status === 429 || status >= 500;
2993
3015
  if (!isRetryable) {
2994
- // Non-retryable 4xx — fail immediately, no attempts field
3016
+ // Non-retryable 4xx and every 3xx (redirect: 'manual' hands them back as-is, #5409) —
3017
+ // fail immediately, no attempts field. Keep 3xx out of isRetryable.
3018
+ response.body?.cancel().catch(() => { });
2995
3019
  output({ available: false, error: `API error: ${status}` }, raw, '');
2996
3020
  return;
2997
3021
  }
@@ -25,11 +25,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
25
25
  return (mod && mod.__esModule) ? mod : { "default": mod };
26
26
  };
27
27
  const node_fs_1 = __importDefault(require("node:fs"));
28
- const node_os_1 = __importDefault(require("node:os"));
29
28
  const node_path_1 = __importDefault(require("node:path"));
30
29
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
31
30
  // #4717: runtime-identity fill — env rung + per-install marker rung.
32
31
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
32
+ const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
33
33
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -935,7 +935,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
935
935
  // untouched. Faults in the global file stay silent in this branch (the
936
936
  // project config governs; the nearer file is the actionable one).
937
937
  try {
938
- const shadowHome = process.env['GSD_HOME'] || node_os_1.default.homedir();
938
+ const shadowHome = (0, runtime_homes_cjs_1.resolveGsdHome)();
939
939
  const shadowPath = node_path_1.default.join(shadowHome, '.gsd', 'defaults.json');
940
940
  const shadowRead = _readConfigFile(shadowPath);
941
941
  if (shadowRead.kind === 'ok') {
@@ -997,7 +997,7 @@ function loadConfigResolvedInternal(cwd, options = {}) {
997
997
  }
998
998
  // Branch D or E: no .planning/
999
999
  try {
1000
- const home = process.env['GSD_HOME'] || node_os_1.default.homedir();
1000
+ const home = (0, runtime_homes_cjs_1.resolveGsdHome)();
1001
1001
  const globalDefaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
1002
1002
  const globalRead = _readConfigFile(globalDefaultsPath);
1003
1003
  if (globalRead.kind === 'fault') {
@@ -21,6 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.decisionMentioned = decisionMentioned;
24
+ exports.phaseDirProblem = phaseDirProblem;
24
25
  exports.loadPlanContents = loadPlanContents;
25
26
  exports.loadSummaryContents = loadSummaryContents;
26
27
  exports.loadDecisionExtraction = loadDecisionExtraction;
@@ -70,6 +71,24 @@ function decisionMentioned(haystack, decision) {
70
71
  return phrase ? normalizePhrase(haystack).includes(phrase) : false;
71
72
  }
72
73
  // ─── File reading ─────────────────────────────────────────────────────────────
74
+ // #4939: loadPlanContents answers no files for a path that is not there, and that reads
75
+ // as "no plan cites anything" — so a phase-dir argument that names no directory
76
+ // (the phase NUMBER in the phase-dir slot) must be told apart BEFORE the scan.
77
+ // Answers null for a directory, else what is wrong with the path and what to do
78
+ // about it. Like the plan gate's contextKind (#4794), the message names the real
79
+ // stat outcome: a permission or I/O failure (EACCES, ELOOP, EIO) is reported as
80
+ // unreadable, not as a missing directory. Only ENOENT/ENOTDIR mean the path is not there.
81
+ function phaseDirProblem(dirPath) {
82
+ const passDirectory = 'Pass the phase DIRECTORY (e.g. .planning/phases/01-slug), not the phase number.';
83
+ const st = (0, gate_evidence_cjs_1.statEvidence)(dirPath);
84
+ if (st.kind === 'none')
85
+ return { reason: 'phase directory not found', what: 'does not exist', hint: passDirectory, unreadable: false };
86
+ if (st.kind === 'unreadable')
87
+ return { reason: 'phase directory unreadable', what: `could not be read (${st.reason})`, hint: 'Check that the directory is readable.', unreadable: true };
88
+ if (st.value.isDirectory())
89
+ return null;
90
+ return { reason: 'phase path is not a directory', what: st.value.isFile() ? 'is a file, not a directory' : 'is not a directory', hint: passDirectory, unreadable: false };
91
+ }
73
92
  /**
74
93
  * The contents of the phase files `pick` selects (#5170, ADR-5057 §4). An ABSENT phase directory
75
94
  * is "no files" (`found []`); a directory or file that exists but cannot be read is `unreadable` —
@@ -28,6 +28,7 @@ const node_path_1 = __importDefault(require("node:path"));
28
28
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
29
29
  const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
30
30
  const gate_config_cjs_1 = require("./gate-config.cjs");
31
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
31
32
  const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
32
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
33
34
  const planningWorkspace = require("./planning-workspace.cjs");
@@ -45,13 +46,13 @@ const { defaultPhaseCleanCommitTimesMs } = verificationMod;
45
46
  function computeContextDrift(contextEffectiveMs, entries) {
46
47
  return entries.filter((e) => e.effectiveMs < contextEffectiveMs).map((e) => e.file);
47
48
  }
48
- function buildContextDriftMessage(staleArtifacts, phaseArg) {
49
+ function buildContextDriftMessage(staleArtifacts, phaseArg, projectDir) {
49
50
  const parts = [`CONTEXT.md decisions are newer than: ${staleArtifacts.join(', ')}.`];
50
51
  if (staleArtifacts.some((f) => f.endsWith('-RESEARCH.md'))) {
51
- parts.push(`Regenerate research: /gsd:plan-phase ${phaseArg} --research.`);
52
+ parts.push(`Regenerate research: ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `plan-phase ${phaseArg} --research`)}.`);
52
53
  }
53
54
  if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
54
- parts.push('Regenerate patterns: delete the PATTERNS.md file, then re-run /gsd:plan-phase.');
55
+ parts.push(`Regenerate patterns: delete the PATTERNS.md file, then re-run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, 'plan-phase')}.`);
55
56
  }
56
57
  if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
57
58
  parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
@@ -114,7 +115,7 @@ function runContextDriftGate(cwd, phaseArg) {
114
115
  // missing or malformed config is "key absent", nothing is printed).
115
116
  const action = (0, gate_config_cjs_1.readWorkflowConfigValue)(cwd, 'workflow.context_drift_action').value === 'block' ? 'block' : 'warn';
116
117
  const block = staleArtifacts.length > 0 && action === 'block';
117
- const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg) : '';
118
+ const message = staleArtifacts.length > 0 ? buildContextDriftMessage(staleArtifacts, phaseArg, cwd) : '';
118
119
  return (0, gate_verdict_cjs_1.gateVerdict)(block ? 'block' : (staleArtifacts.length > 0 ? 'advisory' : 'pass'), block, {
119
120
  block,
120
121
  skipped: false,
@@ -145,9 +145,24 @@ function evaluateDecisionCoveragePlan(input) {
145
145
  if (decisions.length === 0) {
146
146
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' });
147
147
  }
148
+ // #4939: a phase-dir argument that was GIVEN but is not a directory is a caller
149
+ // error like #2770's empty context argument and #4794's non-file context path:
150
+ // no plan was scanned, so nothing was measured. Fail closed with total/covered
151
+ // null and `uncovered` omitted. An OMITTED phase dir is left to the scan below —
152
+ // the #4130 `--context <path>` alone tests pin that as a coverage gap.
153
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
154
+ if (phaseDirIssue) {
155
+ const phaseDirResult = { passed: false, skipped: false, reason: phaseDirIssue.reason, total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" ${phaseDirIssue.what}, so no plans were scanned. ${phaseDirIssue.hint}` };
156
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(true, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('block', true, phaseDirResult);
157
+ }
148
158
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
149
159
  if (planContents.kind === 'unreadable')
150
160
  return unreadableDecisionGate(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
161
+ // #4939: a phase directory that holds no PLAN file is the same caller error: zero plans
162
+ // loaded is not zero coverage.
163
+ if (phaseDir && planContents.value.length === 0) {
164
+ return (0, gate_verdict_cjs_1.gateVerdict)('block', true, { passed: false, skipped: false, reason: 'phase directory holds no plans', total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" holds no PLAN files, so no plans were scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
165
+ }
151
166
  const sections = planContents.value.map(decision_coverage_support_cjs_1.extractPlanDesignatedSections);
152
167
  const uncovered = [];
153
168
  let covered = 0;
@@ -90,6 +90,14 @@ function evaluateDecisionCoverageVerify(input) {
90
90
  if (decisions.length === 0) {
91
91
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' });
92
92
  }
93
+ // #4939: mirror the plan gate. A phase-dir argument that names no directory
94
+ // means no plan or summary was scanned, so no decision can be reported
95
+ // not-honored. Warn (verify stays non-blocking) rather than list every decision.
96
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
97
+ if (phaseDirIssue) {
98
+ const phaseDirResult = { skipped: false, blocking: false, reason: phaseDirIssue.reason, total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" ${phaseDirIssue.what}, so no plans or summaries were scanned. ${phaseDirIssue.hint}` };
99
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(false, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, phaseDirResult);
100
+ }
93
101
  // Every file the haystack is built from is typed evidence (#5170): one that exists but cannot be
94
102
  // read is `unreadable`, never an empty slice of the haystack that makes a decision look unhonored.
95
103
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
@@ -98,6 +106,11 @@ function evaluateDecisionCoverageVerify(input) {
98
106
  const summaryParts = (0, decision_coverage_support_cjs_1.loadSummaryContents)(phaseDir);
99
107
  if (summaryParts.kind === 'unreadable')
100
108
  return unreadableVerify(`${summaryParts.span ?? phaseDir}: ${summaryParts.reason}`);
109
+ // #4939: mirror the plan gate. A phase directory with no PLAN and no SUMMARY file gave the
110
+ // verifier nothing to read, which is a caller error, not a phase whose decisions were all missed.
111
+ if (phaseDir && planContents.value.length === 0 && summaryParts.value.length === 0) {
112
+ return (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, { skipped: false, blocking: false, reason: 'phase directory holds no plans', total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" holds no PLAN or SUMMARY files, so nothing was scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
113
+ }
101
114
  const modifiedFiles = (0, decision_coverage_support_cjs_1.readModifiedFilesContent)(projectDir, summaryParts.value);
102
115
  if (modifiedFiles.kind === 'unreadable')
103
116
  return unreadableVerify(`${modifiedFiles.span ?? 'files_modified'}: ${modifiedFiles.reason}`);
@@ -26,6 +26,10 @@
26
26
  * This is a leaf pure module: no fs, no child_process, no config — the subprocess
27
27
  * seam is injected so the evaluator is trivially testable without spawning.
28
28
  */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_path_1 = __importDefault(require("node:path"));
29
33
  // ─── Public constants ─────────────────────────────────────────────────────────
30
34
  /** Default command timeout for `command-exit-zero` (30s). Matches execTool default. */
31
35
  const COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS = 30_000;
@@ -116,15 +120,31 @@ function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
116
120
  throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
117
121
  }
118
122
  const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
119
- const filePath = deps.findPhaseArtifact(targetDir, artifactSuffix);
120
- if (!filePath) {
123
+ // Plain `.sort()` (no locale) so the outcome never depends on directory listing order (#5313).
124
+ // Set: finders return realpaths, so a symlink to the same file is one artifact, not two.
125
+ const found = deps.findPhaseArtifact(targetDir, artifactSuffix);
126
+ // A pre-#5313 dep returned `string | null`; a silent null -> "not found" block or per-character spread would mislead.
127
+ if (!Array.isArray(found)) {
128
+ throw new Error('predicate deps "findPhaseArtifact" must return an array of paths');
129
+ }
130
+ const candidates = [...new Set(found)].sort();
131
+ if (candidates.length === 0) {
121
132
  return {
122
133
  block: true,
123
134
  message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
124
135
  details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
125
136
  };
126
137
  }
127
- const fm = deps.readFrontmatter(filePath);
138
+ // #5313: never pick one of several matches (listing order is OS-dependent); a throw would fail open via onError.
139
+ if (candidates.length > 1) {
140
+ const names = candidates.map((c) => node_path_1.default.relative(targetDir, c).split(node_path_1.default.sep).join('/')).join(', ');
141
+ return {
142
+ block: true,
143
+ message: `Multiple artifacts match ${artifactSuffix} in ${targetDir}: ${names}; remove or rename all but one`,
144
+ details: { kind: 'artifact-frontmatter-equals', ambiguous: true, candidates },
145
+ };
146
+ }
147
+ const fm = deps.readFrontmatter(candidates[0]);
128
148
  const actualValue = fm[field];
129
149
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
130
150
  const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
@@ -80,31 +80,27 @@ function buildPredicateDeps() {
80
80
  if (phaseDirStat.kind === 'unreadable')
81
81
  return unreadable(phaseDir, phaseDirStat.reason);
82
82
  if (phaseDirStat.kind === 'none')
83
- return null;
83
+ return [];
84
84
  if (artifactSuffix === '.' ||
85
85
  artifactSuffix === '..' ||
86
86
  artifactSuffix.includes('\0') ||
87
87
  node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
88
88
  node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
89
- return null;
90
- }
91
- const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
92
- if (directContained !== null) {
93
- const direct = regularFileOrNull(directContained);
94
- if (direct !== null)
95
- return direct;
89
+ return [];
96
90
  }
91
+ // #5313: collect EVERY distinct match; the evaluator fails closed on >1 instead of trusting listing order.
92
+ const found = [];
97
93
  const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
98
94
  if (planningContained !== null) {
99
95
  const planning = regularFileOrNull(planningContained);
100
96
  if (planning !== null)
101
- return planning;
97
+ found.push(planning);
102
98
  }
103
99
  const listing = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
104
100
  if (listing.kind === 'unreadable')
105
101
  return unreadable(phaseDir, listing.reason);
106
102
  if (listing.kind === 'none')
107
- return null;
103
+ return [];
108
104
  for (const f of listing.value) {
109
105
  if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
110
106
  const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
@@ -112,10 +108,10 @@ function buildPredicateDeps() {
112
108
  continue;
113
109
  const candidate = regularFileOrNull(candidateContained);
114
110
  if (candidate !== null)
115
- return candidate;
111
+ found.push(candidate);
116
112
  }
117
113
  }
118
- return null;
114
+ return found;
119
115
  },
120
116
  readFrontmatter(filePath) {
121
117
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
@@ -17,6 +17,12 @@
17
17
  * tolerant. A plan id that is not `<phase>-<plan>` (a plan named `.*-PLAN.md`) matches nothing,
18
18
  * and a git failure is "no commits" exactly as before.
19
19
  *
20
+ * Cross-plan GREEN (#4867): a gap closure is planned as a RED-only probe plan plus the plan that
21
+ * `depends_on` it and carries the fix. A `type: tdd` plan with its own RED but no GREEN is credited
22
+ * with a `feat(...)` commit under any plan that lists it in `depends_on`, directly or through a
23
+ * chain; the row carries `greenFrom` and the report names the supplier. Its own RED is still
24
+ * required, and a plan nothing depends on is scored on its own commits alone.
25
+ *
20
26
  * Argv after the verb: `<phase>` (a number; an unresolvable phase reports zero plans).
21
27
  */
22
28
  var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -31,7 +37,7 @@ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
31
37
  const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
32
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
39
  const frontmatterMod = require("./frontmatter.cjs");
34
- const { frontmatterKeyHasValue } = frontmatterMod;
40
+ const { frontmatterKeyHasValue, extractFrontmatter } = frontmatterMod;
35
41
  /**
36
42
  * True when the plan's frontmatter declares `type: tdd` (CRLF included, #2449). The block is the
37
43
  * one fence owner's, read as RAW text — not through the YAML parser — so a block the parser refuses
@@ -42,6 +48,35 @@ const { frontmatterKeyHasValue } = frontmatterMod;
42
48
  function isTddPlan(content) {
43
49
  return frontmatterKeyHasValue(content, 'type', 'tdd');
44
50
  }
51
+ /** A `depends_on` token as a plan id: `01.1-20`, `01.1-20-PLAN` and `01.1-20-PLAN.md` all name plan `01.1-20`. */
52
+ function toPlanId(token) {
53
+ return token.trim().replace(/-PLAN(?:\.md)?$/i, '');
54
+ }
55
+ /**
56
+ * The plan ids a plan lists in `depends_on` (#4867). A plan whose frontmatter does not parse, or has
57
+ * no `depends_on`, depends on nothing: the pairing below only ever ADDS credit, so an unreadable
58
+ * link can never turn a failing plan into a passing one.
59
+ */
60
+ function dependsOnOf(content) {
61
+ // `extractFrontmatter` does not throw: an unparseable block is an empty result, so `depends_on` is absent.
62
+ const value = extractFrontmatter(content)['depends_on'];
63
+ const tokens = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];
64
+ return tokens.map(toPlanId).filter((id) => id !== '');
65
+ }
66
+ /** Every plan that depends on `planId`, directly or through a chain (#4867); a cycle is walked once. */
67
+ function transitiveDependents(planId, dependents) {
68
+ const seen = new Set();
69
+ const queue = [planId];
70
+ while (queue.length > 0) {
71
+ for (const next of dependents.get(queue.shift()) ?? []) {
72
+ if (next !== planId && !seen.has(next)) {
73
+ seen.add(next);
74
+ queue.push(next);
75
+ }
76
+ }
77
+ }
78
+ return [...seen].sort();
79
+ }
45
80
  /**
46
81
  * The commit types (`test`, `feat`, `refactor`, …) among the plan's own commits — those that
47
82
  * touched at least one path (`pathspecs: ['.']`, as the old `git log -- .` lookup did). An
@@ -91,6 +126,9 @@ function evaluateTddReviewCheckpoint(input) {
91
126
  const phaseDir = located.kind === 'found' ? located.value : '';
92
127
  // Find all PLAN.md files with type: tdd in frontmatter
93
128
  const tddPlanFiles = [];
129
+ // #4867: plan id -> the plans that list it in `depends_on`, over EVERY plan of the phase (a
130
+ // dependent GREEN plan is usually `type: execute`, not `tdd`).
131
+ const dependents = new Map();
94
132
  const unreadable = [];
95
133
  if (phaseDir) {
96
134
  const entries = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
@@ -110,8 +148,13 @@ function evaluateTddReviewCheckpoint(input) {
110
148
  const plan = (0, gate_evidence_cjs_1.readTextEvidence)(planPath);
111
149
  if (plan.kind === 'unreadable')
112
150
  unreadable.push({ source: planPath, reason: plan.reason });
113
- else if (plan.kind === 'found' && isTddPlan(plan.value))
114
- tddPlanFiles.push(planPath);
151
+ else if (plan.kind === 'found') {
152
+ if (isTddPlan(plan.value))
153
+ tddPlanFiles.push(planPath);
154
+ const planId = node_path_1.default.basename(planPath, '-PLAN.md');
155
+ for (const dep of dependsOnOf(plan.value))
156
+ dependents.set(dep, [...(dependents.get(dep) ?? []), planId]);
157
+ }
115
158
  }
116
159
  }
117
160
  }
@@ -136,15 +179,24 @@ function evaluateTddReviewCheckpoint(input) {
136
179
  const planId = node_path_1.default.basename(planPath, '-PLAN.md');
137
180
  const kinds = planCommitKinds(projectDir, planId);
138
181
  const red = kinds.has('test');
139
- const green = kinds.has('feat');
140
182
  const refactor = kinds.has('refactor');
183
+ // #4867: a RED-only probe plan is satisfied by the GREEN of a plan that (transitively) declares
184
+ // `depends_on` it. Only a real `feat(...)` commit under such a plan counts; the plan's own RED is
185
+ // still required, and a plan nothing depends on is scored on its own commits alone.
186
+ const ownGreen = kinds.has('feat');
187
+ const greenFrom = ownGreen
188
+ ? []
189
+ : transitiveDependents(planId, dependents).filter((dependentId) => planCommitKinds(projectDir, dependentId).has('feat'));
190
+ const green = ownGreen || greenFrom.length > 0;
141
191
  const missing = [];
142
192
  if (!red)
143
193
  missing.push('RED');
144
194
  if (!green)
145
195
  missing.push('GREEN');
146
196
  const status = missing.length === 0 ? 'Pass' : 'FAIL';
147
- rows.push({ planId, red, green, refactor, status, missing });
197
+ rows.push(greenFrom.length > 0
198
+ ? { planId, red, green, refactor, status, missing, greenFrom }
199
+ : { planId, red, green, refactor, status, missing });
148
200
  }
149
201
  const violations = rows.filter(r => r.status === 'FAIL').length;
150
202
  // Build review table
@@ -160,6 +212,9 @@ function evaluateTddReviewCheckpoint(input) {
160
212
  tableDivider,
161
213
  ...tableRows,
162
214
  ].join('\n');
215
+ for (const r of rows.filter(row => row.greenFrom)) {
216
+ table += `\n\nPlan ${r.planId} GREEN is supplied by dependent plan ${(r.greenFrom ?? []).join(', ')}.`;
217
+ }
163
218
  if (violations > 0) {
164
219
  table += '\n\n⚠ Gate violations are advisory — review before advancing.';
165
220
  for (const r of rows.filter(row => row.status === 'FAIL')) {
@@ -19,6 +19,7 @@ exports.evaluateUiSafetyGate = evaluateUiSafetyGate;
19
19
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
20
20
  const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
21
21
  const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
22
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
22
23
  const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
23
24
  const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
24
25
  const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
@@ -52,7 +53,7 @@ function computeUiSafetyGate(projectDir, phase) {
52
53
  const result = { frontend, hasUiFiles, hasUiSpec, block };
53
54
  if (block) {
54
55
  result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
55
- `Run /gsd:ui-phase ${phase} to generate the design contract before continuing.`;
56
+ `Run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `ui-phase ${phase}`)} to generate the design contract before continuing.`;
56
57
  }
57
58
  if (phaseLookupFailed)
58
59
  result.phaseLookupFailed = true;
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  const node_fs_1 = __importDefault(require("node:fs"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
16
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports
17
18
  const capabilityStateMod = require("./capability-state.cjs");
18
19
  const { isCapabilityActive } = capabilityStateMod;
@@ -408,7 +409,7 @@ function graphifyQuery(cwd, term, options = {}) {
408
409
  const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
409
410
  if (!node_fs_1.default.existsSync(graphPath)) {
410
411
  return { error: configured
411
- ? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
412
+ ? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
412
413
  : 'No graph built yet. Run graphify build first.' };
413
414
  }
414
415
  const graph = safeReadJson(graphPath);
@@ -446,7 +447,7 @@ function graphifyStatus(cwd) {
446
447
  const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
447
448
  if (!node_fs_1.default.existsSync(graphPath)) {
448
449
  return { exists: false, graph_path: graphPath, message: configured
449
- ? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
450
+ ? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run ${(0, runtime_slash_cjs_1.runtimeCommand)(cwd, 'graphify build')}.`
450
451
  : 'No graph built yet. Run graphify build to create one.' };
451
452
  }
452
453
  const stat = node_fs_1.default.statSync(graphPath);