@opengsd/gsd-core 1.14.0 → 1.15.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-code-fixer.compact.md +7 -6
  8. package/agents/gsd-code-fixer.md +9 -8
  9. package/agents/gsd-debug-session-manager.compact.md +17 -2
  10. package/agents/gsd-debug-session-manager.md +17 -2
  11. package/agents/gsd-debugger.md +2 -2
  12. package/agents/gsd-eval-auditor.compact.md +1 -1
  13. package/agents/gsd-eval-auditor.md +1 -1
  14. package/agents/gsd-executor.md +13 -8
  15. package/agents/gsd-intel-updater.compact.md +1 -1
  16. package/agents/gsd-intel-updater.md +1 -1
  17. package/agents/gsd-phase-researcher.md +19 -11
  18. package/agents/gsd-plan-checker.md +8 -7
  19. package/agents/gsd-planner.md +12 -8
  20. package/agents/gsd-project-researcher.compact.md +1 -1
  21. package/agents/gsd-project-researcher.md +1 -1
  22. package/agents/gsd-research-synthesizer.compact.md +1 -1
  23. package/agents/gsd-research-synthesizer.md +1 -1
  24. package/agents/gsd-ui-auditor.md +155 -17
  25. package/agents/gsd-ui-researcher.compact.md +1 -1
  26. package/agents/gsd-ui-researcher.md +1 -1
  27. package/agents/gsd-verifier.md +10 -9
  28. package/bin/install.js +642 -95
  29. package/commands/gsd/autonomous.md +2 -2
  30. package/commands/gsd/capture.md +1 -1
  31. package/commands/gsd/mempalace-capture.md +7 -3
  32. package/commands/gsd/plan-review-convergence.md +6 -6
  33. package/commands/gsd/progress.md +1 -1
  34. package/commands/gsd/quick-batch.md +1 -1
  35. package/commands/gsd/review.md +2 -3
  36. package/gsd-core/bin/gsd-tools.cjs +335 -22
  37. package/gsd-core/bin/lib/adr-parser.cjs +3 -1
  38. package/gsd-core/bin/lib/audit.cjs +81 -13
  39. package/gsd-core/bin/lib/capability-registry.cjs +82 -187
  40. package/gsd-core/bin/lib/capability-validator.cjs +0 -1
  41. package/gsd-core/bin/lib/check-command-router.cjs +101 -14
  42. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  43. package/gsd-core/bin/lib/commands.cjs +175 -42
  44. package/gsd-core/bin/lib/config-loader.cjs +65 -4
  45. package/gsd-core/bin/lib/config.cjs +33 -7
  46. package/gsd-core/bin/lib/decisions.cjs +30 -14
  47. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  48. package/gsd-core/bin/lib/graphify.cjs +10 -2
  49. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  50. package/gsd-core/bin/lib/init.cjs +207 -41
  51. package/gsd-core/bin/lib/install-engine.cjs +13 -0
  52. package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone.cjs +18 -5
  54. package/gsd-core/bin/lib/model-resolver.cjs +159 -50
  55. package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
  56. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  57. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  58. package/gsd-core/bin/lib/phase-id.cjs +109 -7
  59. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  60. package/gsd-core/bin/lib/phase.cjs +227 -26
  61. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  62. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  63. package/gsd-core/bin/lib/planning-inspect.cjs +18 -1
  64. package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
  65. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  66. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  67. package/gsd-core/bin/lib/project-root.cjs +41 -2
  68. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  69. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  70. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  71. package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
  72. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  73. package/gsd-core/bin/lib/roadmap.cjs +251 -31
  74. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
  75. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
  76. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  77. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
  78. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  79. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  80. package/gsd-core/bin/lib/state-transition.cjs +39 -2
  81. package/gsd-core/bin/lib/state.cjs +42 -0
  82. package/gsd-core/bin/lib/surface.cjs +17 -1
  83. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  84. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  85. package/gsd-core/bin/lib/uat.cjs +8 -0
  86. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  87. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  88. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  89. package/gsd-core/bin/lib/verification.cjs +268 -15
  90. package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
  91. package/gsd-core/bin/lib/verify.cjs +132 -25
  92. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  93. package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
  94. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  95. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  96. package/gsd-core/references/checkpoints.md +5 -3
  97. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  98. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  99. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  100. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  101. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  102. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  103. package/gsd-core/references/edge-probe.md +195 -21
  104. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  105. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  106. package/gsd-core/references/gsd-run-resolver.md +1 -1
  107. package/gsd-core/references/model-profiles.md +1 -1
  108. package/gsd-core/references/phase-argument-parsing.md +9 -7
  109. package/gsd-core/references/phase-id-convention.md +28 -0
  110. package/gsd-core/references/planner-gap-closure.md +2 -0
  111. package/gsd-core/references/planner-load-graph-context.md +24 -13
  112. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  113. package/gsd-core/references/planning-config.md +11 -2
  114. package/gsd-core/references/tdd.md +27 -4
  115. package/gsd-core/references/ui-consideration-probe.md +10 -5
  116. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  117. package/gsd-core/references/worktree-path-safety.md +321 -0
  118. package/gsd-core/templates/verification-report.md +1 -1
  119. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  120. package/gsd-core/workflows/add-backlog.md +1 -1
  121. package/gsd-core/workflows/add-phase.md +1 -1
  122. package/gsd-core/workflows/add-tests.md +2 -2
  123. package/gsd-core/workflows/add-todo.md +3 -3
  124. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  125. package/gsd-core/workflows/audit-fix.md +1 -1
  126. package/gsd-core/workflows/audit-milestone.md +1 -1
  127. package/gsd-core/workflows/audit-uat.md +1 -1
  128. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  129. package/gsd-core/workflows/autonomous.md +16 -6
  130. package/gsd-core/workflows/check-todos.md +2 -2
  131. package/gsd-core/workflows/cleanup.md +2 -2
  132. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  133. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
  134. package/gsd-core/workflows/code-review-fix.md +108 -22
  135. package/gsd-core/workflows/code-review.md +63 -46
  136. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
  137. package/gsd-core/workflows/complete-milestone.md +2 -2
  138. package/gsd-core/workflows/debug.md +3 -3
  139. package/gsd-core/workflows/diagnose-issues.md +1 -1
  140. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  141. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  142. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  143. package/gsd-core/workflows/discuss-phase.md +1 -1
  144. package/gsd-core/workflows/do.md +2 -2
  145. package/gsd-core/workflows/docs-update.md +3 -3
  146. package/gsd-core/workflows/edit-phase.md +1 -1
  147. package/gsd-core/workflows/eval-review.md +10 -3
  148. package/gsd-core/workflows/execute-phase/detail/elaboration.md +2 -2
  149. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  150. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  151. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
  152. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
  153. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  154. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  155. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  156. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  157. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  158. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  159. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  160. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  161. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  162. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  163. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  164. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  165. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  166. package/gsd-core/workflows/execute-phase.md +36 -26
  167. package/gsd-core/workflows/execute-plan.md +5 -4
  168. package/gsd-core/workflows/explore.md +3 -3
  169. package/gsd-core/workflows/extract-learnings.md +2 -1
  170. package/gsd-core/workflows/fast.md +1 -1
  171. package/gsd-core/workflows/forensics.md +1 -1
  172. package/gsd-core/workflows/graduation.md +1 -1
  173. package/gsd-core/workflows/health.md +2 -2
  174. package/gsd-core/workflows/help/modes/full.compact.md +3 -3
  175. package/gsd-core/workflows/help/modes/full.md +5 -5
  176. package/gsd-core/workflows/help/modes/topic.md +15 -5
  177. package/gsd-core/workflows/import.md +2 -2
  178. package/gsd-core/workflows/inbox.md +2 -2
  179. package/gsd-core/workflows/ingest-docs.md +3 -3
  180. package/gsd-core/workflows/insert-phase.md +1 -1
  181. package/gsd-core/workflows/list-seeds.md +1 -1
  182. package/gsd-core/workflows/list-workspaces.md +1 -1
  183. package/gsd-core/workflows/manager.md +2 -2
  184. package/gsd-core/workflows/map-codebase.md +2 -2
  185. package/gsd-core/workflows/milestone-summary.md +1 -1
  186. package/gsd-core/workflows/mvp-phase.md +1 -1
  187. package/gsd-core/workflows/new-milestone.md +2 -2
  188. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  189. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  190. package/gsd-core/workflows/new-project.md +7 -7
  191. package/gsd-core/workflows/new-workspace.md +2 -2
  192. package/gsd-core/workflows/next.md +1 -1
  193. package/gsd-core/workflows/note.md +1 -1
  194. package/gsd-core/workflows/onboard.md +1 -1
  195. package/gsd-core/workflows/pause-work.md +1 -1
  196. package/gsd-core/workflows/plan-phase/detail/elaboration.md +1 -1
  197. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  198. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  199. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  200. package/gsd-core/workflows/plan-phase.md +24 -7
  201. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  202. package/gsd-core/workflows/plant-seed.md +62 -20
  203. package/gsd-core/workflows/pr-branch.md +113 -13
  204. package/gsd-core/workflows/profile-user.md +2 -2
  205. package/gsd-core/workflows/progress.md +1 -1
  206. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  207. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  208. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  209. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  210. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  211. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  212. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  213. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  214. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  215. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  216. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  217. package/gsd-core/workflows/quick-batch.md +1 -1
  218. package/gsd-core/workflows/quick.md +21 -9
  219. package/gsd-core/workflows/reapply-patches.md +9 -3
  220. package/gsd-core/workflows/remove-phase.md +1 -1
  221. package/gsd-core/workflows/remove-workspace.md +2 -2
  222. package/gsd-core/workflows/resume-project.md +1 -1
  223. package/gsd-core/workflows/review.md +31 -16
  224. package/gsd-core/workflows/scan.md +1 -1
  225. package/gsd-core/workflows/secure-phase.md +3 -2
  226. package/gsd-core/workflows/settings-advanced.md +30 -10
  227. package/gsd-core/workflows/settings-integrations.md +2 -3
  228. package/gsd-core/workflows/settings.md +4 -4
  229. package/gsd-core/workflows/ship.md +3 -2
  230. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  231. package/gsd-core/workflows/sketch.md +1 -1
  232. package/gsd-core/workflows/smart-entry.md +2 -2
  233. package/gsd-core/workflows/spec-phase.md +15 -5
  234. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  235. package/gsd-core/workflows/spike.md +1 -1
  236. package/gsd-core/workflows/stats.md +1 -1
  237. package/gsd-core/workflows/sync-skills.md +5 -5
  238. package/gsd-core/workflows/thread.md +1 -1
  239. package/gsd-core/workflows/transition.md +1 -1
  240. package/gsd-core/workflows/ui-phase.md +44 -8
  241. package/gsd-core/workflows/ui-review.md +18 -4
  242. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  243. package/gsd-core/workflows/undo.md +339 -20
  244. package/gsd-core/workflows/update.md +7 -7
  245. package/gsd-core/workflows/validate-phase.md +3 -2
  246. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  247. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  248. package/gsd-core/workflows/verify-work.md +81 -16
  249. package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
  250. package/hooks/dist/gsd-secret-read-guard.js +27 -1
  251. package/hooks/dist/gsd-statusline.js +70 -13
  252. package/hooks/dist/gsd-validate-commit.sh +63 -4
  253. package/hooks/gsd-agent-isolation-guard.js +24 -0
  254. package/hooks/gsd-secret-read-guard.js +27 -1
  255. package/hooks/gsd-statusline.js +70 -13
  256. package/hooks/gsd-validate-commit.sh +63 -4
  257. package/package.json +3 -2
  258. package/scripts/build-hooks.js +15 -6
  259. package/scripts/check-contract-drift.cjs +127 -11
  260. package/scripts/command-contract-helpers.cjs +3 -0
  261. package/scripts/docs-guard-registry.cjs +28 -0
  262. package/scripts/gen-loop-host-contract.cjs +69 -0
  263. package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
  264. package/scripts/lib/ndjson-reporter.cjs +3 -2
  265. package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
  266. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
  267. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  268. package/scripts/lint-phase-id-drift.cjs +290 -5
  269. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  270. package/scripts/lint-retired-runtime-name.cjs +619 -0
  271. package/scripts/lint-state-write-path-drift.cjs +93 -0
  272. package/scripts/lint-test-file-count.allowlist.json +28 -9
  273. package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
  274. package/scripts/prompt-injection-scan.sh +4 -0
  275. package/scripts/release-tarball-smoke.cjs +194 -1
  276. package/skills/gsd-autonomous/SKILL.md +2 -2
  277. package/skills/gsd-capture/SKILL.md +1 -1
  278. package/skills/gsd-mempalace-capture/SKILL.md +7 -3
  279. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  280. package/skills/gsd-progress/SKILL.md +1 -1
  281. package/skills/gsd-quick-batch/SKILL.md +1 -1
  282. package/skills/gsd-review/SKILL.md +2 -3
  283. package/vscode/package.json +1 -1
@@ -28,6 +28,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
28
28
  const node_os_1 = __importDefault(require("node:os"));
29
29
  const node_path_1 = __importDefault(require("node:path"));
30
30
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
31
+ // #4717: runtime-identity fill — env rung + per-install marker rung.
32
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
33
+ const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
31
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
35
  const planningWorkspace = require("./planning-workspace.cjs");
33
36
  const { planningDir, planningRoot } = planningWorkspace;
@@ -150,8 +153,21 @@ const CONFIG_DEFAULTS = {
150
153
  research_before_questions: _getNestedConfigDefault('workflow', 'research_before_questions'), // #3894
151
154
  smart_zone_tokens: _getNestedConfigDefault('workflow', 'smart_zone_tokens'),
152
155
  inline_plan_threshold: _getNestedConfigDefault('workflow', 'inline_plan_threshold'), // #3801
156
+ planner_stall_detection_enabled: _getNestedConfigDefault('planner', 'stall_detection_enabled'),
153
157
  max_prompt_tokens: _getNestedConfigDefault('review', 'max_prompt_tokens'),
154
158
  };
159
+ /**
160
+ * Resolve the planner watchdog policy from hand-edited configuration.
161
+ * Only a real JSON boolean may override the default; strings/numbers/null
162
+ * fail safe to the manifest-owned default-on behavior (#4570).
163
+ */
164
+ function resolvePlannerStallDetectionEnabled(value) {
165
+ if (typeof value === 'boolean')
166
+ return value;
167
+ // A missing or skewed manifest must never convert an absent override into
168
+ // permission to disable the watchdog. The documented contract is default-on.
169
+ return true;
170
+ }
155
171
  /**
156
172
  * Deep-merge two plain config objects. `overlay` wins on key conflict.
157
173
  * Explicit `null` in overlay overrides base (null means "unset this key").
@@ -601,7 +617,7 @@ function _warnUnusableConfig(fault) {
601
617
  * cannot dirty the working tree; the ~30 callers that omit it keep persisting, so
602
618
  * a legacy config is still migrated exactly once by ordinary use.
603
619
  */
604
- function loadConfigResolved(cwd, options = {}) {
620
+ function loadConfigResolvedInternal(cwd, options = {}) {
605
621
  // Opt-OUT, not opt-in: omitting the option must preserve the historical
606
622
  // write-back for every existing caller.
607
623
  const persist = options['persist'] !== false;
@@ -614,7 +630,7 @@ function loadConfigResolved(cwd, options = {}) {
614
630
  : (options['workstreamContext'] && Object.prototype.hasOwnProperty.call(options['workstreamContext'], 'ws'))
615
631
  ? options['workstreamContext']['ws']
616
632
  : (process.env['GSD_WORKSTREAM'] || null);
617
- const ws = typeof activeWorkstream === 'string' ? activeWorkstream : (activeWorkstream === null ? null : null);
633
+ const ws = typeof activeWorkstream === 'string' ? activeWorkstream.trim() || null : null;
618
634
  // wsRequested: true when caller explicitly requested a non-empty workstream.
619
635
  // Used for source labeling (Fix 4) and early absent-dir intercept (Fix 2).
620
636
  const wsRequested = ws != null && ws !== '';
@@ -858,6 +874,7 @@ function loadConfigResolved(cwd, options = {}) {
858
874
  phase_naming: get('phase_naming') ?? defaults.phase_naming,
859
875
  project_code: get('project_code') ?? defaults.project_code,
860
876
  subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
877
+ planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(getNested('planner', 'stall_detection_enabled')),
861
878
  model_overrides: (parsed['model_overrides']) || null,
862
879
  agent_tools: (parsed['agent_tools']) || null,
863
880
  models: (parsed['models']) || null,
@@ -959,7 +976,7 @@ function loadConfigResolved(cwd, options = {}) {
959
976
  // `workstream: null` still wins the `hasOwnProperty` check at the top of this
960
977
  // function, so spreading cannot let `workstreamContext` reintroduce a workstream.
961
978
  if (wsRequested && rootParsed) {
962
- const fb = loadConfigResolved(cwd, { ...options, workstream: null });
979
+ const fb = loadConfigResolvedInternal(cwd, { ...options, workstream: null });
963
980
  return fallback({ config: fb.config, source: 'root', degraded: true });
964
981
  }
965
982
  // Branch B, C, D, E
@@ -967,7 +984,7 @@ function loadConfigResolved(cwd, options = {}) {
967
984
  if (rootParsed) {
968
985
  // Branch B: workstream requested but ws config.json absent; root config present.
969
986
  // (Only reached when wsRequested is false — e.g. ws='' with .planning/workstreams//config.json)
970
- const fb = loadConfigResolved(cwd, { ...options, workstream: null });
987
+ const fb = loadConfigResolvedInternal(cwd, { ...options, workstream: null });
971
988
  return fallback({ config: fb.config, source: 'root', degraded: true });
972
989
  }
973
990
  // Branch C: .planning/ exists but no config.json and no root config — federated/builtin defaults
@@ -1016,6 +1033,7 @@ function loadConfigResolved(cwd, options = {}) {
1016
1033
  resolve_model_ids: (globalDefaults['resolve_model_ids']) ?? defaults.resolve_model_ids,
1017
1034
  context_window: (globalDefaults['context_window']) ?? defaults.context_window,
1018
1035
  subagent_timeout: (globalDefaults['subagent_timeout']) ?? defaults.subagent_timeout,
1036
+ planner_stall_detection_enabled: resolvePlannerStallDetectionEnabled(globalDefaults['planner']?.['stall_detection_enabled']),
1019
1037
  model_overrides: (globalDefaults['model_overrides']) || null,
1020
1038
  models: (globalDefaults['models']) || null,
1021
1039
  granularity: (globalDefaults['granularity']) !== undefined ? globalDefaults['granularity'] : null,
@@ -1054,6 +1072,48 @@ function loadConfigResolved(cwd, options = {}) {
1054
1072
  }
1055
1073
  }
1056
1074
  }
1075
+ /**
1076
+ * #4717 — fill an empty `runtime` from the environment, then the per-install
1077
+ * marker. writeNonClaudeDefaults stamps `runtime` into the SHARED
1078
+ * ~/.gsd/defaults.json with whichever non-Claude runtime installed first, so
1079
+ * direct readers of config.runtime saw another runtime's identity (or
1080
+ * nothing) on a multi-runtime machine. Copy-on-write: the builtin-defaults
1081
+ * branch returns a shared object, so never assign into it. An explicit
1082
+ * config.runtime is never overridden.
1083
+ */
1084
+ function fillRuntimeIdentity(resolved) {
1085
+ const cfg = resolved?.config;
1086
+ if (!cfg)
1087
+ return resolved;
1088
+ const runtime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], (0, runtime_slash_cjs_1.readInstallRuntimeMarker)());
1089
+ // Only a runtime the name policy can canonicalize is an identity. Unknown
1090
+ // tokens pass THROUGH resolveRuntimeNameFromCandidates (future-runtime
1091
+ // tolerance) and must not be materialized into config.runtime, where ~30
1092
+ // consumers would read them — fail safe to no identity (#4717 review).
1093
+ const canonicalRuntime = runtime ? (0, runtime_name_policy_cjs_1.canonicalizeRuntimeName)(runtime) : null;
1094
+ if (!canonicalRuntime)
1095
+ return resolved;
1096
+ // Rung 1 — empty runtime: materialize THIS install's identity (env, then
1097
+ // the per-install marker). An explicit runtime is never overridden.
1098
+ if (!cfg['runtime']) {
1099
+ return { ...resolved, config: { ...cfg, runtime: canonicalRuntime } };
1100
+ }
1101
+ // Rung 2 (#4717 stamped-defaults leg): the global-defaults branch forwards
1102
+ // the SHARED ~/.gsd/defaults.json's runtime verbatim — whichever non-Claude
1103
+ // runtime installed FIRST stamped that machine-wide file, and every other
1104
+ // runtime's resolution inherited its identity (the issue's second failure
1105
+ // shape). When THIS install carries its own identity (GSD_RUNTIME or the
1106
+ // marker), the stamp — not the operator — is speaking: correct it. Project
1107
+ // and workstream configs are explicit operator intent and are never touched;
1108
+ // with no install identity of its own the stamped value stays (status quo).
1109
+ if (resolved.source === 'global-defaults') {
1110
+ return { ...resolved, config: { ...cfg, runtime: canonicalRuntime } };
1111
+ }
1112
+ return resolved;
1113
+ }
1114
+ function loadConfigResolved(cwd, options = {}) {
1115
+ return fillRuntimeIdentity(loadConfigResolvedInternal(cwd, options));
1116
+ }
1057
1117
  /**
1058
1118
  * loadConfig — backwards-compatible config loading, now a thin wrapper over loadConfigResolved.
1059
1119
  * Returns the config object only; for provenance metadata use loadConfigResolved.
@@ -1072,6 +1132,7 @@ module.exports = {
1072
1132
  _getNestedConfigDefault,
1073
1133
  _getConfigValue,
1074
1134
  _getConfigNested,
1135
+ resolvePlannerStallDetectionEnabled,
1075
1136
  _deepMergeConfig,
1076
1137
  _warnedUnknownConfigKeys,
1077
1138
  _warnedShadowedGlobalKeys,
@@ -20,11 +20,11 @@ const cliExitMod = require("./cli-exit.cjs");
20
20
  const { ExitError } = cliExitMod;
21
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports
22
22
  const configLoader = require("./config-loader.cjs");
23
- const { CONFIG_DEFAULTS } = configLoader;
23
+ const { CONFIG_DEFAULTS, resolvePlannerStallDetectionEnabled } = configLoader;
24
24
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
25
25
  // eslint-disable-next-line @typescript-eslint/no-require-imports
26
26
  const planningWorkspace = require("./planning-workspace.cjs");
27
- const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
27
+ const { planningDir, planningRoot, resolveEnvWorkstream, withPlanningLock } = planningWorkspace;
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const modelProfiles = require("./model-profiles.cjs");
30
30
  const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTable } = modelProfiles;
@@ -66,6 +66,14 @@ const SHIP_PR_BODY_TEMPLATE_TOKENS = new Set([
66
66
  'padded_phase',
67
67
  ]);
68
68
  const SHIP_PR_BODY_SOURCE_RE = /^(ROADMAP|PLAN|SUMMARY|VERIFICATION|STATE|REQUIREMENTS|CONTEXT)\.md\s+##\s+[^\r\n#][^\r\n]*$/;
69
+ // ADR-612 PR-5: configuration accepts every convention the runtime can read.
70
+ // Keep this distinct from roadmap-upgrade's supported target set: sequential
71
+ // is valid project configuration but is not a migration destination.
72
+ const VALID_PHASE_ID_CONVENTIONS = Object.freeze([
73
+ 'sequential',
74
+ 'milestone-prefixed',
75
+ 'bracket',
76
+ ]);
69
77
  /**
70
78
  * Schema-level defaults for well-known config keys.
71
79
  * When a key is absent from config.json and no --default flag was supplied,
@@ -75,6 +83,7 @@ const SCHEMA_DEFAULTS = {
75
83
  'context_window': 200000,
76
84
  'executor.stall_detect_interval_minutes': 5,
77
85
  'executor.stall_threshold_minutes': 10,
86
+ 'planner.stall_detection_enabled': CONFIG_DEFAULTS.planner_stall_detection_enabled,
78
87
  'planner.stall_detect_interval_minutes': 5,
79
88
  'planner.stall_threshold_minutes': 10,
80
89
  'git.create_tag': true,
@@ -152,12 +161,15 @@ function resolveSchemaDefault(cwd, kp) {
152
161
  * Centralizing emission here means masking can't be missed at a call site.
153
162
  */
154
163
  function emitResolvedDefault(kp, value, raw) {
164
+ const resolvedValue = kp === 'planner.stall_detection_enabled'
165
+ ? resolvePlannerStallDetectionEnabled(value)
166
+ : value;
155
167
  if ((0, secrets_cjs_1.isSecretKey)(kp)) {
156
- const masked = (0, secrets_cjs_1.maskSecret)(value);
168
+ const masked = (0, secrets_cjs_1.maskSecret)(resolvedValue);
157
169
  output(masked, raw, masked);
158
170
  return;
159
171
  }
160
- output(value, raw, String(value));
172
+ output(resolvedValue, raw, String(resolvedValue));
161
173
  }
162
174
  // ─── Validation helpers ───────────────────────────────────────────────────────
163
175
  function validateKnownConfigKeyPath(keyPath) {
@@ -542,7 +554,7 @@ function _setNestedValue(config, keyPath, parsedValue) {
542
554
  }
543
555
  /**
544
556
  * Deletes a value from the config object, allowing nested values via dot
545
- * notation (e.g., "review.models.gemini"). Mirrors `_setNestedValue`'s
557
+ * notation (e.g., "review.models.codex"). Mirrors `_setNestedValue`'s
546
558
  * prototype-pollution guard on every path segment (including intermediates).
547
559
  *
548
560
  * Unlike `_setNestedValue`, this NEVER creates missing intermediate objects —
@@ -806,6 +818,9 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
806
818
  const VALID_CONTEXT_VALUES = ['dev', 'research', 'review'];
807
819
  if (kp === 'context')
808
820
  assertEnumValue(parsedValue, val, VALID_CONTEXT_VALUES, 'context value');
821
+ if (kp === 'phase_id_convention') {
822
+ assertEnumValue(parsedValue, val, VALID_PHASE_ID_CONVENTIONS, 'phase_id_convention');
823
+ }
809
824
  // Codebase drift detector (#2003)
810
825
  const VALID_DRIFT_ACTIONS = ['warn', 'auto-remap'];
811
826
  if (kp === 'workflow.drift_action')
@@ -853,6 +868,13 @@ function cmdConfigSet(cwd, keyPath, value, raw, options = {}) {
853
868
  error(`Invalid workflow.agent_hint_routing '${val}'. Must be a boolean (true or false).`);
854
869
  }
855
870
  }
871
+ // Planner watchdog opt-out (#4570) — only a real boolean may change the
872
+ // default-on policy. In particular, string "false" must not disable it.
873
+ if (kp === 'planner.stall_detection_enabled') {
874
+ if (typeof parsedValue !== 'boolean') {
875
+ error(`Invalid planner.stall_detection_enabled '${val}'. Must be a boolean (true or false).`);
876
+ }
877
+ }
856
878
  // #3086 — git.create_tag: boolean only
857
879
  if (kp === 'git.create_tag') {
858
880
  if (typeof parsedValue !== 'boolean') {
@@ -1161,6 +1183,9 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
1161
1183
  }
1162
1184
  error(`Key not found: ${kp}`, ERROR_REASON.CONFIG_KEY_NOT_FOUND);
1163
1185
  }
1186
+ if (kp === 'planner.stall_detection_enabled') {
1187
+ current = resolvePlannerStallDetectionEnabled(current);
1188
+ }
1164
1189
  // Never echo plaintext for sensitive keys via config-get. Plaintext lives
1165
1190
  // in config.json on disk; the CLI surface always shows the masked form.
1166
1191
  if ((0, secrets_cjs_1.isSecretKey)(kp)) {
@@ -1190,7 +1215,7 @@ function resolveFromRootConfig(cwd, kp) {
1190
1215
  // diverges from planningRoot without a workstream and loadConfigResolved does NOT
1191
1216
  // inherit root — matching the runtime's own `if (ws)` gate keeps the two surfaces
1192
1217
  // from diverging on the project-scoped (non-workstream) case.
1193
- if (!process.env['GSD_WORKSTREAM'])
1218
+ if (!resolveEnvWorkstream())
1194
1219
  return { found: false, value: undefined };
1195
1220
  const root = planningRoot(cwd);
1196
1221
  const rootConfigPath = node_path_1.default.join(root, 'config.json');
@@ -1297,7 +1322,7 @@ function cmdConfigPath(cwd, _raw, workstreamContext = null) {
1297
1322
  * (caller uses `await` which is safe on a sync return value).
1298
1323
  */
1299
1324
  function cmdMigrateConfig(cwd, raw) {
1300
- const ws = process.env['GSD_WORKSTREAM'] || null;
1325
+ const ws = resolveEnvWorkstream();
1301
1326
  // #3749: resolve the migration target through the project-aware resolver so
1302
1327
  // GSD_PROJECT scopes the write; migrateOnDisk itself cannot (see its
1303
1328
  // configPathOverride note).
@@ -1345,6 +1370,7 @@ function cmdMigrateConfig(cwd, raw) {
1345
1370
  }
1346
1371
  module.exports = {
1347
1372
  VALID_CONFIG_KEYS,
1373
+ VALID_PHASE_ID_CONVENTIONS,
1348
1374
  cmdConfigEnsureSection,
1349
1375
  cmdConfigSet,
1350
1376
  cmdConfigGet,
@@ -85,14 +85,14 @@ const DECISION_ID_SOURCE = 'D[0-9]*-[A-Za-z0-9][A-Za-z0-9_-]*';
85
85
  const ID_ATTEMPT_SOURCE = 'D(?:[0-9][A-Za-z0-9]*)?-';
86
86
  /**
87
87
  * Colon form: `- **D[phase]-NN[ [tags]]:** text`
88
- * (#1343: `[^:*]*` subsumes any pre-colon prose, stops at `:**`)
88
+ * (#1343: the pre-colon run subsumes any pre-colon prose, stops at `:**`; #4788: the run is code-span-aware — a backticked span is consumed whole, so a `:` or `*` INSIDE it is data, never grammar)
89
89
  * Group 1 captures the FULL id including any phase prefix (#4130).
90
90
  * The ID is consumed atomically `(?=(…))\1` — see the hardening note above
91
91
  * the constants (#4130 follow-up); with the tail unable to give back, the
92
92
  * remaining `[^:*]*:` scan has a single viable split and the whole match is
93
93
  * linear in line length.
94
94
  */
95
- const bulletColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:\\*\\*\\s*(.*)$`);
95
+ const bulletColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|[^:*\\u0060])*:\\*\\*\\s*(.*)$`);
96
96
  /**
97
97
  * Em-dash form: `- **D[phase]-NN[ [tags]] — title** body`
98
98
  * The em-dash (U+2014) or its lookalike separates the ID+tags group from a title
@@ -112,22 +112,32 @@ const bulletColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1
112
112
  * candidate and `**` succeeds or fails identically; no capture involves the
113
113
  * dash position. The ID is atomic like the other forms (driver #1).
114
114
  */
115
- const bulletEmDashRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^*—–]*[—–][^*]*\\*\\*\\s*(.*)$`);
115
+ const bulletEmDashRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|[^*—–\\u0060])*[—–](?:\\u0060[^\\u0060]*\\u0060|[^*\\u0060])*\\*\\*\\s*(.*)$`);
116
116
  /**
117
117
  * Titled-colon form: `- **D[phase]-NN[ [tags]]: Title.** body`
118
118
  * A title sits between the colon and the closing `**` (so the `:**` anchor of
119
119
  * bulletColonRe fails, and there is no em-dash for bulletEmDashRe). This is a strict
120
120
  * superset of the colon-immediate form, so it MUST be checked AFTER bulletColonRe and
121
- * bulletEmDashRe — it only catches bullets those two miss. The title run is `[^:*]*` (no
122
- * colon, no `*`) so a genuinely-malformed bullet with a colon in the pre-separator run
123
- * (e.g. `D-07 ratio 3:1:**`) still fails the anchor and falls through to the parse-miss
124
- * guard — matching bulletColonRe's `[^:*]*` discipline that the separator colon is the
125
- * only colon permitted before `**`. (#1639)
121
+ * bulletEmDashRe — it only catches bullets those two miss. The title runs are code-span-aware
122
+ * (#4788) but still exclude `*`, so a genuinely-malformed bullet with no bare colon at all
123
+ * before `**` still fails the anchor and falls through to the parse-miss guard — the #1639
124
+ * fail-loud discipline: the separator must be a BARE colon before `**`. A code span's
125
+ * `:`/`*` is data, never grammar; an UNTERMINATED backtick now fails loud (previously it was
126
+ * an ordinary character) — the deliberate cost of span opacity.
127
+ *
128
+ * #4793/#4958: the pre-separator run tolerates a bare colon as prose ONLY when it is
129
+ * immediately followed by whitespace (`:(?=\s)`), e.g. a natural sentence colon in
130
+ * `D-02: Two managers … same pair: the second is rejected.`. A colon with no following
131
+ * whitespace (a compact/ratio-style colon like `3:1`) is never consumed by this branch, so
132
+ * it still hard-caps the pre-separator run and the #1639/#4130/#1343 negative-space fixtures
133
+ * (e.g. `D-07 ratio 3:1:**`) still fail loud. The post-separator branch is unchanged and
134
+ * still excludes colons, so normal greedy backtracking selects the LAST valid split (a bare
135
+ * colon followed by whitespace, or the anchor colon itself) before the closing `**`.
126
136
  *
127
137
  * The ID is consumed atomically `(?=(…))\1` like the other forms — the
128
138
  * hardening note above the constants explains why (#4130 follow-up).
129
139
  */
130
- const bulletTitledColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?[^:*]*:[^:*]*\\*\\*\\s*(.*)$`);
140
+ const bulletTitledColonRe = new RegExp(`^\\s*-\\s+\\*\\*(?=(${DECISION_ID_SOURCE}))\\1(?:\\s*\\[([^\\]]+)\\])?(?:\\u0060[^\\u0060]*\\u0060|:(?=\\s)|[^:*\\u0060])*:(?:\\u0060[^\\u0060]*\\u0060|[^:*\\u0060])*\\*\\*\\s*(.*)$`);
131
141
  /**
132
142
  * #4130: the parse-miss guard's probe — a line whose bold lead-in ATTEMPTS the
133
143
  * ID grammar (see `ID_ATTEMPT_SOURCE`) but failed all three bullet patterns
@@ -403,6 +413,7 @@ function parseDecisionLines(block) {
403
413
  let current = null;
404
414
  let openIndent = null;
405
415
  let parseMisses = 0;
416
+ const unreadableIds = [];
406
417
  const flush = () => {
407
418
  if (current) {
408
419
  current.text = current.text.trim();
@@ -501,6 +512,11 @@ function parseDecisionLines(block) {
501
512
  if (parseMissGuardRe.test(line)) {
502
513
  flush();
503
514
  parseMisses += 1;
515
+ // #4794: carry the attempted id so a caller capturing stdout (a JSON gate)
516
+ // can name the decision to fix — today it exists only in this stderr warn.
517
+ const attempted = line.match(/\*\*\s*(D(?:[0-9][A-Za-z0-9]*)?-[A-Za-z0-9_-]*)/);
518
+ if (attempted && !unreadableIds.includes(attempted[1]))
519
+ unreadableIds.push(attempted[1]);
504
520
  console.warn(`parseDecisions: ignored unparseable decision bullet: ${trimmed}`);
505
521
  continue;
506
522
  }
@@ -516,7 +532,7 @@ function parseDecisionLines(block) {
516
532
  }
517
533
  }
518
534
  flush();
519
- return { decisions: out, parseMisses };
535
+ return { decisions: out, parseMisses, unreadableIds };
520
536
  }
521
537
  // ─── Primary entry point: extractDecisions ────────────────────────────────────
522
538
  /**
@@ -541,13 +557,13 @@ function extractDecisions(content) {
541
557
  const taggedBlocks = (0, markdown_sectionizer_cjs_1.extractTaggedBlocks)(stripped, 'decisions');
542
558
  if (taggedBlocks.length > 0) {
543
559
  const combined = taggedBlocks.join('\n\n');
544
- const { decisions, parseMisses } = parseDecisionLines(combined);
560
+ const { decisions, parseMisses, unreadableIds } = parseDecisionLines(combined);
545
561
  if (decisions.length > 0 && parseMisses === 0) {
546
562
  return { decisions, outcome: 'parsed' };
547
563
  }
548
564
  // FIX B: parse-misses present — could-not-parse even if some decisions extracted.
549
565
  if (parseMisses > 0) {
550
- return { decisions, outcome: 'could-not-parse' };
566
+ return { decisions, outcome: 'could-not-parse', unreadableIds };
551
567
  }
552
568
  // FIX A: Block present but 0 extracted and no parse-misses.
553
569
  // Only report could-not-parse when there is genuine evidence of real decisions
@@ -568,13 +584,13 @@ function extractDecisions(content) {
568
584
  // stripFences:true → inner fences inside the section body are stripped.
569
585
  const section = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => /decisions?\b/i.test(h.text), { levelBounded: true, stripFences: true });
570
586
  if (section !== null) {
571
- const { decisions, parseMisses } = parseDecisionLines(section.body);
587
+ const { decisions, parseMisses, unreadableIds } = parseDecisionLines(section.body);
572
588
  if (decisions.length > 0 && parseMisses === 0) {
573
589
  return { decisions, outcome: 'parsed' };
574
590
  }
575
591
  // FIX B: parse-misses present — could-not-parse even if some decisions extracted.
576
592
  if (parseMisses > 0) {
577
- return { decisions, outcome: 'could-not-parse' };
593
+ return { decisions, outcome: 'could-not-parse', unreadableIds };
578
594
  }
579
595
  // FIX A: Heading found but 0 extracted and no parse-misses.
580
596
  // Report could-not-parse when the section body holds a decision-entry-shaped
@@ -1394,6 +1394,14 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
1394
1394
  // Pass the resolved path so a truncated file is named in the diagnostic and deduplicated
1395
1395
  // per file rather than per content digest (#1882, ADR-1411 wiring clause).
1396
1396
  const fm = extractFrontmatter(content, fullPath);
1397
+ // #4806: an unparseable frontmatter block is a distinct outcome — the file
1398
+ // HAS a frontmatter block but its YAML failed to parse. Reporting
1399
+ // "Field not found" tells the caller the key is absent, which is
1400
+ // indistinguishable from a file that genuinely lacks it.
1401
+ if (fm[FRONTMATTER_UNPARSEABLE] === true) {
1402
+ output({ error: 'Frontmatter is not parseable YAML — fix the syntax error in the frontmatter block', path: filePath }, raw, undefined);
1403
+ return;
1404
+ }
1397
1405
  if (field) {
1398
1406
  const value = fm[field];
1399
1407
  if (value === undefined) {
@@ -1649,4 +1657,9 @@ module.exports = {
1649
1657
  cmdFrontmatterMerge,
1650
1658
  cmdFrontmatterValidate,
1651
1659
  propagateCommentChannel,
1660
+ // #4917 / ADR-4910 Decision 1: additive-only export so `planning-document.cts`
1661
+ // can COMPOSE this seam's fence-detection grammar (byte-0 rule, BOM strip,
1662
+ // CR handling) instead of reimplementing it. No behavior change — same
1663
+ // function `extractFrontmatter`/`frontmatterListEntries` already call.
1664
+ frontmatterRegion,
1652
1665
  };
@@ -445,14 +445,17 @@ function graphifyStatus(cwd) {
445
445
  return disabledResponse();
446
446
  const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
447
447
  if (!node_fs_1.default.existsSync(graphPath)) {
448
- return { exists: false, message: configured
448
+ return { exists: false, graph_path: graphPath, message: configured
449
449
  ? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
450
450
  : 'No graph built yet. Run graphify build to create one.' };
451
451
  }
452
452
  const stat = node_fs_1.default.statSync(graphPath);
453
453
  const graph = safeReadJson(graphPath);
454
454
  if (!graph) {
455
- return { error: 'Failed to parse graph.json' };
455
+ // Still surface graph_path (#4836 Minor 1): callers gate on `exists`, not
456
+ // on this outcome, so without it they'd fall through to the CLI-first
457
+ // branch with no --graph value to substitute for the <graph> placeholder.
458
+ return { error: 'Failed to parse graph.json', graph_path: graphPath };
456
459
  }
457
460
  const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
458
461
  const age = Date.now() - stat.mtimeMs;
@@ -482,6 +485,11 @@ function graphifyStatus(cwd) {
482
485
  (lastBuildAutoUpdate.status === 'failed' || lastBuildAutoUpdate.status === 'running');
483
486
  return {
484
487
  exists: true,
488
+ // The absolute location the whole graphify surface reads, already resolved
489
+ // through `graphify.graph_path` (#1825). Callers that shell out to the
490
+ // graphify CLI pass this as `--graph` so the umbrella override is honoured
491
+ // there too, instead of re-deriving `.planning/graphs/graph.json` (#4836).
492
+ graph_path: graphPath,
485
493
  last_build: stat.mtime.toISOString(),
486
494
  node_count: (graph.nodes || []).length,
487
495
  edge_count: (graph.edges || graph.links || []).length,
@@ -24,6 +24,7 @@ exports.resolveReportedRuntime = resolveReportedRuntime;
24
24
  const node_fs_1 = __importDefault(require("node:fs"));
25
25
  const node_path_1 = __importDefault(require("node:path"));
26
26
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
27
+ const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
27
28
  const update_context_cjs_1 = require("./update-context.cjs");
28
29
  Object.defineProperty(exports, "CODEX_CONFIG_MARKER", { enumerable: true, get: function () { return update_context_cjs_1.CODEX_CONFIG_MARKER; } });
29
30
  // Codex sandbox env vars set by the shell tool / Seatbelt child-process spawn.
@@ -127,6 +128,14 @@ function resolveReportedRuntimeUnsafe(projectDir, deps) {
127
128
  const explicit = (0, runtime_slash_cjs_1.resolveExplicitRuntime)(projectDir, deps?.env ?? process.env);
128
129
  if (explicit)
129
130
  return explicit;
131
+ // #4717: the per-install marker names the runtime that owns THIS tree — a
132
+ // stronger signal than host sniffing, which misreports every session on a
133
+ // multi-runtime machine (e.g. a globally exported CODEX_HOME makes a Claude
134
+ // Code session read as codex). Marker-less trees (dev/source, pre-#2297
135
+ // installs) fall through to host detection unchanged.
136
+ const marker = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)((0, runtime_slash_cjs_1.readInstallRuntimeMarker)());
137
+ if (marker)
138
+ return marker;
130
139
  const detected = detectHostRuntime(deps);
131
140
  if (detected.runtime)
132
141
  return detected.runtime;