@opengsd/gsd-core 1.13.0 → 1.14.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 (257) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +85 -0
  4. package/agents/gsd-ai-researcher.compact.md +96 -0
  5. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  6. package/agents/gsd-code-fixer.compact.md +458 -0
  7. package/agents/gsd-code-fixer.md +5 -5
  8. package/agents/gsd-code-reviewer.compact.md +269 -0
  9. package/agents/gsd-code-reviewer.md +15 -3
  10. package/agents/gsd-codebase-mapper.compact.md +760 -0
  11. package/agents/gsd-debug-session-manager.compact.md +345 -0
  12. package/agents/gsd-doc-classifier.compact.md +192 -0
  13. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  14. package/agents/gsd-doc-verifier.compact.md +143 -0
  15. package/agents/gsd-doc-writer.compact.md +440 -0
  16. package/agents/gsd-dom-verifier.compact.md +138 -0
  17. package/agents/gsd-domain-researcher.compact.md +141 -0
  18. package/agents/gsd-eval-auditor.compact.md +160 -0
  19. package/agents/gsd-eval-planner.compact.md +137 -0
  20. package/agents/gsd-framework-selector.compact.md +82 -0
  21. package/agents/gsd-integration-checker.compact.md +245 -0
  22. package/agents/gsd-intel-updater.compact.md +226 -0
  23. package/agents/gsd-mempalace-curator.compact.md +45 -0
  24. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  25. package/agents/gsd-pattern-mapper.compact.md +275 -0
  26. package/agents/gsd-project-researcher.compact.md +587 -0
  27. package/agents/gsd-research-synthesizer.compact.md +212 -0
  28. package/agents/gsd-roadmapper.compact.md +454 -0
  29. package/agents/gsd-roadmapper.md +13 -0
  30. package/agents/gsd-security-auditor.compact.md +162 -0
  31. package/agents/gsd-ui-auditor.compact.md +404 -0
  32. package/agents/gsd-ui-checker.compact.md +277 -0
  33. package/agents/gsd-ui-researcher.compact.md +282 -0
  34. package/agents/gsd-user-profiler.compact.md +108 -0
  35. package/bin/install.js +206 -68
  36. package/commands/gsd/cleanup.md +1 -0
  37. package/commands/gsd/code-review.md +2 -1
  38. package/commands/gsd/complete-milestone.md +1 -0
  39. package/commands/gsd/config.md +1 -0
  40. package/commands/gsd/debug.md +1 -0
  41. package/commands/gsd/graphify.md +1 -0
  42. package/commands/gsd/health.md +1 -0
  43. package/commands/gsd/mempalace-capture.md +1 -0
  44. package/commands/gsd/mempalace-recall.md +1 -0
  45. package/commands/gsd/new-milestone.md +1 -0
  46. package/commands/gsd/new-project.md +1 -0
  47. package/commands/gsd/next.md +1 -0
  48. package/commands/gsd/pause-work.md +1 -0
  49. package/commands/gsd/phase.md +1 -0
  50. package/commands/gsd/pr-branch.md +1 -0
  51. package/commands/gsd/resume-work.md +1 -0
  52. package/commands/gsd/review-backlog.md +1 -0
  53. package/commands/gsd/settings.md +2 -1
  54. package/commands/gsd/stats.md +1 -0
  55. package/commands/gsd/thread.md +1 -0
  56. package/commands/gsd/workspace.md +1 -0
  57. package/commands/gsd/workstreams.md +1 -0
  58. package/gsd-core/bin/check-latest-version.cjs +8 -3
  59. package/gsd-core/bin/gsd-tools.cjs +338 -125
  60. package/gsd-core/bin/lib/adr-parser.cjs +1 -1
  61. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  62. package/gsd-core/bin/lib/audit.cjs +39 -22
  63. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  64. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  65. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  66. package/gsd-core/bin/lib/capability-registry.cjs +79 -67
  67. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  68. package/gsd-core/bin/lib/capability-validator.cjs +14 -1
  69. package/gsd-core/bin/lib/check-command-router.cjs +113 -36
  70. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  71. package/gsd-core/bin/lib/commands.cjs +650 -72
  72. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  73. package/gsd-core/bin/lib/config.cjs +153 -38
  74. package/gsd-core/bin/lib/coverage.cjs +1 -1
  75. package/gsd-core/bin/lib/decisions.cjs +137 -34
  76. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  77. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  78. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  79. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  80. package/gsd-core/bin/lib/init.cjs +409 -47
  81. package/gsd-core/bin/lib/install-engine.cjs +16 -3
  82. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  83. package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
  84. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  85. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  86. package/gsd-core/bin/lib/milestone.cjs +19 -8
  87. package/gsd-core/bin/lib/model-resolver.cjs +101 -10
  88. package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +161 -22
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  91. package/gsd-core/bin/lib/phase.cjs +167 -63
  92. package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
  93. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  94. package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
  95. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  96. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  97. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  98. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  99. package/gsd-core/bin/lib/research-store.cjs +11 -12
  100. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  101. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  102. package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
  103. package/gsd-core/bin/lib/roadmap.cjs +108 -14
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
  105. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
  106. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
  108. package/gsd-core/bin/lib/security.cjs +126 -7
  109. package/gsd-core/bin/lib/state-document.cjs +130 -28
  110. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  111. package/gsd-core/bin/lib/state-transition.cjs +142 -28
  112. package/gsd-core/bin/lib/state.cjs +223 -27
  113. package/gsd-core/bin/lib/surface.cjs +60 -2
  114. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  115. package/gsd-core/bin/lib/uat.cjs +1 -1
  116. package/gsd-core/bin/lib/update-context.cjs +30 -24
  117. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  118. package/gsd-core/bin/lib/verification.cjs +47 -15
  119. package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
  120. package/gsd-core/bin/lib/verify.cjs +188 -23
  121. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  122. package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
  123. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  124. package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
  125. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  126. package/gsd-core/references/compact-content-gate.md +66 -0
  127. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  128. package/gsd-core/references/model-profiles.md +12 -3
  129. package/gsd-core/references/planning-config.md +3 -0
  130. package/gsd-core/references/tdd.md +5 -2
  131. package/gsd-core/references/thinking-models-planning.md +18 -2
  132. package/gsd-core/references/verification-patterns.md +17 -4
  133. package/gsd-core/references/worktree-path-safety.md +112 -2
  134. package/gsd-core/templates/README.md +7 -1
  135. package/gsd-core/templates/state.md +6 -3
  136. package/gsd-core/templates/summary.compact.md +212 -0
  137. package/gsd-core/templates/user-setup.compact.md +199 -0
  138. package/gsd-core/templates/user-setup.md +0 -9
  139. package/gsd-core/workflows/add-todo.md +3 -2
  140. package/gsd-core/workflows/autonomous.md +13 -10
  141. package/gsd-core/workflows/check-todos.md +4 -2
  142. package/gsd-core/workflows/cleanup.md +3 -1
  143. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
  144. package/gsd-core/workflows/code-review-fix.md +3 -3
  145. package/gsd-core/workflows/code-review.md +156 -30
  146. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  147. package/gsd-core/workflows/complete-milestone.md +39 -262
  148. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  149. package/gsd-core/workflows/docs-update.md +14 -155
  150. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  151. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
  152. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  153. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
  154. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  155. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  156. package/gsd-core/workflows/execute-phase.md +53 -152
  157. package/gsd-core/workflows/execute-plan.md +20 -7
  158. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  159. package/gsd-core/workflows/help.md +1 -1
  160. package/gsd-core/workflows/map-codebase.md +50 -3
  161. package/gsd-core/workflows/new-milestone.md +54 -12
  162. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  163. package/gsd-core/workflows/new-project.md +32 -202
  164. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  165. package/gsd-core/workflows/plan-phase.md +22 -181
  166. package/gsd-core/workflows/pr-branch.md +19 -7
  167. package/gsd-core/workflows/quick.md +8 -1
  168. package/gsd-core/workflows/reapply-patches.md +77 -3
  169. package/gsd-core/workflows/settings.md +18 -5
  170. package/gsd-core/workflows/update.md +7 -5
  171. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  172. package/gsd-core/workflows/verify-work.md +20 -180
  173. package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
  174. package/hooks/dist/gsd-context-monitor.js +88 -15
  175. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  176. package/hooks/dist/gsd-secret-read-guard.js +44 -18
  177. package/hooks/dist/gsd-statusline.js +11 -7
  178. package/hooks/dist/gsd-validate-commit.sh +34 -4
  179. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  180. package/hooks/dist/gsd-write-guard.js +46 -1
  181. package/hooks/dist/lib/dispatch-identity.js +187 -0
  182. package/hooks/dist/lib/filename-classification.js +64 -0
  183. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  184. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  185. package/hooks/gsd-agent-isolation-guard.js +42 -16
  186. package/hooks/gsd-context-monitor.js +88 -15
  187. package/hooks/gsd-cursor-subagent-start.js +34 -14
  188. package/hooks/gsd-secret-read-guard.js +44 -18
  189. package/hooks/gsd-statusline.js +11 -7
  190. package/hooks/gsd-validate-commit.sh +34 -4
  191. package/hooks/gsd-worktree-path-guard.js +25 -14
  192. package/hooks/gsd-write-guard.js +46 -1
  193. package/hooks/lib/dispatch-identity.js +187 -0
  194. package/hooks/lib/filename-classification.js +64 -0
  195. package/hooks/lib/isolation-deny-reason.js +53 -1
  196. package/hooks/lib/isolation-sentinel.js +58 -19
  197. package/package.json +10 -6
  198. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  199. package/scripts/benchmark-compact-content.cjs +368 -0
  200. package/scripts/check-contract-drift.cjs +4 -1
  201. package/scripts/check-env.cjs +36 -8
  202. package/scripts/check-glossary-refs.cjs +25 -21
  203. package/scripts/ci-next-health.cjs +271 -0
  204. package/scripts/ci-prepare-test-scope.cjs +7 -7
  205. package/scripts/ci-test-scope.cjs +126 -20
  206. package/scripts/ci-timeout-report.cjs +1 -1
  207. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  208. package/scripts/docs-guard-registry.cjs +7 -2
  209. package/scripts/gen-adr-index.cjs +8 -2
  210. package/scripts/gen-inventory-manifest.cjs +12 -0
  211. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  212. package/scripts/lib/drift-scan.cjs +1 -1
  213. package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
  214. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  215. package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
  216. package/scripts/lib/suite-detection.cjs +32 -0
  217. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  218. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
  219. package/scripts/lint-phase-id-drift.cjs +338 -13
  220. package/scripts/lint-response-language-coverage.cjs +9 -3
  221. package/scripts/lint-source-test-name-collision.cjs +1 -1
  222. package/scripts/lint-test-file-count.allowlist.json +1 -0
  223. package/scripts/lint-vendored-deps.cjs +128 -17
  224. package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
  225. package/scripts/prompt-injection-scan.sh +14 -0
  226. package/scripts/workflow-size.cjs +139 -0
  227. package/skills/gsd-cleanup/SKILL.md +1 -0
  228. package/skills/gsd-code-review/SKILL.md +2 -1
  229. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  230. package/skills/gsd-config/SKILL.md +1 -0
  231. package/skills/gsd-debug/SKILL.md +1 -0
  232. package/skills/gsd-graphify/SKILL.md +1 -0
  233. package/skills/gsd-health/SKILL.md +1 -0
  234. package/skills/gsd-mempalace-capture/SKILL.md +1 -0
  235. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  236. package/skills/gsd-new-milestone/SKILL.md +1 -0
  237. package/skills/gsd-new-project/SKILL.md +1 -0
  238. package/skills/gsd-next/SKILL.md +1 -0
  239. package/skills/gsd-pause-work/SKILL.md +1 -0
  240. package/skills/gsd-phase/SKILL.md +1 -0
  241. package/skills/gsd-pr-branch/SKILL.md +1 -0
  242. package/skills/gsd-resume-work/SKILL.md +1 -0
  243. package/skills/gsd-review-backlog/SKILL.md +1 -0
  244. package/skills/gsd-settings/SKILL.md +2 -1
  245. package/skills/gsd-stats/SKILL.md +1 -0
  246. package/skills/gsd-thread/SKILL.md +1 -0
  247. package/skills/gsd-workspace/SKILL.md +1 -0
  248. package/skills/gsd-workstreams/SKILL.md +1 -0
  249. package/vscode/package.json +1 -1
  250. package/gsd-core/templates/claude-md.md +0 -145
  251. package/gsd-core/templates/codebase/concerns.md +0 -310
  252. package/gsd-core/templates/codebase/conventions.md +0 -307
  253. package/gsd-core/templates/codebase/integrations.md +0 -280
  254. package/gsd-core/templates/codebase/structure.md +0 -285
  255. package/gsd-core/templates/codebase/testing.md +0 -480
  256. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  257. package/gsd-core/templates/discovery.md +0 -146
@@ -15,6 +15,7 @@ const node_os_1 = __importDefault(require("node:os"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const clock_cjs_1 = require("./clock.cjs");
17
17
  const pattern_cjs_1 = require("./pattern.cjs");
18
+ const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
18
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
19
20
  const io = require("./io.cjs");
20
21
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
@@ -86,9 +87,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
86
87
  const { findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs } = phaseLocator;
87
88
  const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
88
89
  const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
89
- const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase } = phaseId;
90
+ const { comparePhaseNum, normalizePhaseName, matchPhaseDirs, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName } = phaseId;
90
91
  const { pruneOrphanedWorktrees } = worktreeSafety;
91
- const { planningPaths, planningDir, planningRoot, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
92
+ const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, } = planningWorkspace;
92
93
  const { determinePhaseStatus } = commandsMod;
93
94
  const { extractFrontmatter } = frontmatterMod;
94
95
  const { isPhaseComplete, resolveVerificationFile, resolveUatFile } = verificationMod;
@@ -257,7 +258,15 @@ function withProjectRoot(cwd, result) {
257
258
  if (config.project_code) {
258
259
  result['project_code'] = config.project_code;
259
260
  }
260
- const projectMdPath = node_path_1.default.join(planningDir(cwd), 'PROJECT.md');
261
+ // #4455 follow-up (self-discovered): PROJECT.md is shared across a
262
+ // project's own workstreams (never cloned per workstream) but DOES
263
+ // respect the separate GSD_PROJECT multi-project namespace (#3749) — see
264
+ // cmdInitCompleteMilestone's projectPath comment for the full evidence.
265
+ // `ws` explicitly nulled, `project` left to default from GSD_PROJECT.
266
+ // Reading via the workstream-aware planningDir(cwd) meant every init.*
267
+ // call's project_title silently vanished whenever a workstream was
268
+ // active, since no PROJECT.md ever exists at the workstream path.
269
+ const projectMdPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
261
270
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(projectMdPath);
262
271
  if (content) {
263
272
  const h1Match = content.match(/^#\s+(.+)$/m);
@@ -477,6 +486,16 @@ function readConfigJsonBoolean(cwd, keyPath) {
477
486
  return false;
478
487
  }
479
488
  }
489
+ /** Reads `filePath`; returns its content, or `null` when missing/unreadable/empty. */
490
+ function readNonEmptyFileOrNull(filePath) {
491
+ try {
492
+ const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
493
+ return content && content.length > 0 ? content : null;
494
+ }
495
+ catch {
496
+ return null;
497
+ }
498
+ }
480
499
  /**
481
500
  * Bounded, non-throwing read of a dotted key path from `.planning/config.json`,
482
501
  * returning the raw resolved value (any JSON type) or `undefined` on any
@@ -828,10 +847,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
828
847
  runnable_plans: phaseInfo?.['runnable_plans'] || [],
829
848
  runnable_count: phaseInfo?.['runnable_plans']?.length || 0,
830
849
  branch_name: config.branching_strategy === 'phase' && phaseInfo
831
- ? config.phase_branch_template
832
- .replace('{project}', config.project_code || '')
833
- .replace('{phase}', normalizePhaseName(phaseInfo['phase_number']))
834
- .replace('{slug}', phaseInfo['phase_slug'] || 'phase')
850
+ ? renderPhaseBranchName(config.phase_branch_template.replace('{project}', config.project_code || ''), phaseInfo['phase_number'], phaseInfo['phase_slug'])
835
851
  : config.branching_strategy === 'milestone'
836
852
  ? config.milestone_branch_template
837
853
  .replace('{milestone}', milestone['version'] ?? '')
@@ -847,6 +863,17 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
847
863
  // #3188: null when the file is absent (parity with patterns_path/context_path).
848
864
  state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
849
865
  roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
866
+ // #4456 correction: an isolated review pass initially "fixed" this to
867
+ // planningDir(cwd, null) on the assumption that config.json is shared
868
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
869
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
870
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
871
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
872
+ // caught the regression immediately. The diagram is stale for
873
+ // config.json specifically (same class of staleness already found for
874
+ // `milestones/` during the #4455 follow-up) — reverted to the
875
+ // workstream-aware planningDir(cwd), matching the established,
876
+ // ADR-governed, tested contract.
850
877
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
851
878
  // #2376: execute-phase.md's verify_phase_goal step reads this instead of
852
879
  // hardcoding '.planning/REQUIREMENTS.md' into the gsd-verifier spawn prompt.
@@ -1130,7 +1157,13 @@ function buildInitCompletenessFields(cwd) {
1130
1157
  const planningExists = node_fs_1.default.existsSync(dir);
1131
1158
  const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'REQUIREMENTS.md'));
1132
1159
  const milestonesExists = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'MILESTONES.md'));
1133
- const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(dir, 'PROJECT.md')) &&
1160
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1161
+ // workstreams (see cmdInitCompleteMilestone's projectPath comment for the
1162
+ // full evidence) — checked at planningRoot(cwd), never the workstream-scoped
1163
+ // `dir`, so a workstream whose own REQUIREMENTS/ROADMAP/STATE are all
1164
+ // present isn't wrongly reported incomplete just because the shared
1165
+ // PROJECT.md isn't ALSO duplicated under its own directory.
1166
+ const coreComplete = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')) &&
1134
1167
  requirementsExists &&
1135
1168
  node_fs_1.default.existsSync(node_path_1.default.join(dir, 'ROADMAP.md')) &&
1136
1169
  node_fs_1.default.existsSync(node_path_1.default.join(dir, 'STATE.md'));
@@ -1165,23 +1198,46 @@ function cmdInitNewProject(cwd, raw, options = {}) {
1165
1198
  // root-scoped (`pathExistsInternal(cwd, '.planning')`) semantics for that
1166
1199
  // one key stay byte-identical for existing consumers.
1167
1200
  ...buildInitCompletenessFields(cwd),
1168
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1201
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1202
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1203
+ // the full evidence.
1204
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1169
1205
  has_codebase_map: hasCodebaseMap,
1170
1206
  planning_exists: pathExistsInternal(cwd, '.planning'),
1171
1207
  has_existing_code: hasCode,
1172
1208
  has_package_file: hasPackageFile,
1173
1209
  is_brownfield: isBrownfield,
1210
+ // #4458: new-project.md's Step 5.1 (Sub-Repo Detection) used to run its own
1211
+ // narrower `find ... -exec test -d "{}/.git"` predicate, which requires
1212
+ // .git to be a DIRECTORY and so silently excluded linked git worktree
1213
+ // children (.git is a FILE there). detectSubRepos already handled this
1214
+ // correctly (fs.existsSync, not isDirectory) but had zero callers anywhere
1215
+ // in the codebase — reused here instead of leaving the workflow to
1216
+ // maintain its own duplicate, narrower detection logic.
1217
+ sub_repos_detected: coreUtils.detectSubRepos(cwd),
1174
1218
  needs_codebase_map: isBrownfield && !hasCodebaseMap,
1175
1219
  ...getInitGitState(cwd),
1176
1220
  brave_search_available: hasBraveSearch,
1177
1221
  firecrawl_available: hasFirecrawl,
1178
1222
  exa_search_available: hasExaSearch,
1179
1223
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1180
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1224
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1225
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1181
1226
  // #2376: new-project.md's research-synthesizer/roadmapper spawn prompts
1182
1227
  // read these instead of hardcoding '.planning/...' literals.
1183
1228
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1184
1229
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1230
+ // #4456 correction: an isolated review pass initially "fixed" this to
1231
+ // planningDir(cwd, null) on the assumption that config.json is shared
1232
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1233
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1234
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1235
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1236
+ // caught the regression immediately. The diagram is stale for
1237
+ // config.json specifically (same class of staleness already found for
1238
+ // `milestones/` during the #4455 follow-up) — reverted to the
1239
+ // workstream-aware planningDir(cwd), matching the established,
1240
+ // ADR-governed, tested contract.
1185
1241
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1186
1242
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1187
1243
  };
@@ -1223,18 +1279,41 @@ function cmdInitNewMilestone(cwd, raw, options = {}) {
1223
1279
  phase_archive_path: latestCompleted
1224
1280
  ? toPosixPath(node_path_1.default.join(planningRoot(cwd), 'milestones', `${latestCompleted.version}-phases`))
1225
1281
  : null,
1226
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1282
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1283
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1284
+ // the full evidence.
1285
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1227
1286
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1228
1287
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1229
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1288
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1289
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1230
1290
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1231
1291
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1232
1292
  // #2376: new-milestone.md's research-synthesizer/roadmapper spawn prompts
1233
1293
  // read these instead of hardcoding '.planning/...' literals.
1234
1294
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1295
+ // #4456 correction: an isolated review pass initially "fixed" this to
1296
+ // planningDir(cwd, null) on the assumption that config.json is shared
1297
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
1298
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
1299
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
1300
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
1301
+ // caught the regression immediately. The diagram is stale for
1302
+ // config.json specifically (same class of staleness already found for
1303
+ // `milestones/` during the #4455 follow-up) — reverted to the
1304
+ // workstream-aware planningDir(cwd), matching the established,
1305
+ // ADR-governed, tested contract.
1235
1306
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
1236
1307
  research_dir: toPosixPath(node_path_1.default.join(planningRoot(cwd), 'research')),
1237
1308
  milestones_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'MILESTONES.md')),
1309
+ // #4456: new-milestone.md's Step 6 stages the phase-archive move
1310
+ // (`git add .planning/milestones/ .planning/phases/`) — both
1311
+ // workstream-scoped (phases_dir mirrors the phasesDir local above;
1312
+ // archive_dir mirrors cmdInitCompleteMilestone's own field of the same
1313
+ // name), so a literal root `git add` misses the actual files
1314
+ // phases.clear just moved under an active workstream.
1315
+ phases_dir: toPosixPath(phasesDir),
1316
+ archive_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'milestones')),
1238
1317
  };
1239
1318
  // `state:flat-mode` (#2994): whether NO workstream is active — the inverse
1240
1319
  // of `state:workstream-active` (introduced for `cmdInitTransition` below).
@@ -1372,7 +1451,10 @@ function cmdInitQuickBatch(cwd, raw, options = {}) {
1372
1451
  function cmdInitIngestDocs(cwd, raw) {
1373
1452
  const config = loadConfig(cwd);
1374
1453
  const result = {
1375
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1454
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1455
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1456
+ // the full evidence.
1457
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1376
1458
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1377
1459
  ...getInitGitState(cwd),
1378
1460
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The
@@ -1381,7 +1463,8 @@ function cmdInitIngestDocs(cwd, raw) {
1381
1463
  // hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc.
1382
1464
  // literals into their Agent(prompt=...) blocks; those now interpolate
1383
1465
  // these fields instead.
1384
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1466
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1467
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1385
1468
  requirements_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md')),
1386
1469
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1387
1470
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
@@ -1417,12 +1500,16 @@ function cmdInitResume(cwd, raw) {
1417
1500
  ...buildInitCompletenessFields(cwd),
1418
1501
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1419
1502
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1420
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
1503
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
1504
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
1505
+ // the full evidence.
1506
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
1421
1507
  planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
1422
1508
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1423
1509
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
1424
1510
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
1425
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
1511
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
1512
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
1426
1513
  has_interrupted_agent: !!interruptedAgentId,
1427
1514
  interrupted_agent_id: interruptedAgentId,
1428
1515
  commit_docs: config.commit_docs,
@@ -1898,13 +1985,156 @@ function cmdInitPhaseOp(cwd, phase, raw) {
1898
1985
  }
1899
1986
  output(withProjectRoot(cwd, result), raw);
1900
1987
  }
1988
+ // #2618: bullet-cap and title-floor for renderPendingTodosMarkdown below.
1989
+ // 240 matches the bound already vetted by maintainer review on the prior
1990
+ // attempt at this issue (PR #2662) — re-deriving a different number would be
1991
+ // pure bikeshedding, not a correctness improvement. See
1992
+ // .gsd/phase/feat-2618-compact-todo-pointers/40-design.md.
1993
+ const PENDING_TODO_BULLET_MAX_CHARS = 240;
1994
+ const PENDING_TODO_TITLE_FLOOR = 15;
1995
+ const PENDING_TODO_AREA_FLOOR = 3;
1996
+ function sanitizePendingTodoInline(value) {
1997
+ // Defensive: the regex captures that populate title/area/needs can only
1998
+ // ever match a single line, so this is belt-and-suspenders against any
1999
+ // future non-regex-sourced input, not a reachable case today.
2000
+ return value.replace(/[\r\n]+/g, ' ').trim();
2001
+ }
2002
+ function truncatePendingTodoText(value, maxLen) {
2003
+ if (value.length <= maxLen)
2004
+ return value;
2005
+ if (maxLen <= 1)
2006
+ return value.slice(0, Math.max(0, maxLen));
2007
+ return `${value.slice(0, maxLen - 1)}…`;
2008
+ }
2009
+ /**
2010
+ * #2618: pure renderer for STATE.md's "### Pending Todos" section BODY (not
2011
+ * the heading). One bullet per todo, each capped at
2012
+ * PENDING_TODO_BULLET_MAX_CHARS. `gsd-core/workflows/add-todo.md` and
2013
+ * `check-todos.md` splice this string in verbatim instead of free-hand
2014
+ * editing STATE.md — see the design doc for why this is real, unit-tested
2015
+ * code rather than a prose algorithm (DEFECT.GENERATIVE-FIX: a prose
2016
+ * algorithm duplicated as a test oracle is exactly the divergence class
2017
+ * this avoids).
2018
+ *
2019
+ * #4384 regression fix: the optional `projectRoot` makes the bullet's
2020
+ * `[todo file](…)` link repo-relative (see pendingTodoLinkTarget) so the cap
2021
+ * is deterministic w.r.t. where the repo is checked out. Omitting it keeps
2022
+ * the legacy absolute-link behavior for existing direct callers.
2023
+ */
2024
+ function renderPendingTodosMarkdown(todos, projectRoot) {
2025
+ if (!Array.isArray(todos) || todos.length === 0) {
2026
+ return 'None yet.';
2027
+ }
2028
+ return todos.map((todo) => renderPendingTodoBullet(todo, projectRoot)).join('\n');
2029
+ }
2030
+ function pendingTodoFieldAsString(value, fallback) {
2031
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
2032
+ }
2033
+ /**
2034
+ * #4384 regression fix: the bullet's markdown link target, rendered
2035
+ * repo-relative when `projectRoot` is given and the todo's `path` is
2036
+ * absolute. The JSON `todos[].path` field stays absolute (#2376 contract);
2037
+ * only the rendered display link changes — embedding the machine-variable
2038
+ * absolute base let macOS's /private/var/folders/… temp paths consume the
2039
+ * 240-char budget and drop the "Needs <solution>" clause on long-path
2040
+ * machines only (next's own macos CI shard went red on exactly this, run
2041
+ * 34038716700). Repo-relative links also resolve correctly from STATE.md at
2042
+ * the repo root and survive repo moves.
2043
+ */
2044
+ function pendingTodoLinkTarget(todo, projectRoot) {
2045
+ const raw = pendingTodoFieldAsString(todo['path'], '');
2046
+ if (typeof projectRoot !== 'string' || projectRoot.length === 0 || !node_path_1.default.isAbsolute(raw)) {
2047
+ return raw;
2048
+ }
2049
+ const rel = toPosixPath(node_path_1.default.relative(projectRoot, raw));
2050
+ if (rel.length === 0 || node_path_1.default.isAbsolute(rel)) {
2051
+ // Degenerate (path === projectRoot) or Windows cross-drive fallback:
2052
+ // keep the raw target rather than emitting an empty or incorrect link.
2053
+ return raw;
2054
+ }
2055
+ return rel;
2056
+ }
2057
+ /**
2058
+ * #4439: the stored `created:` frontmatter (and the JSON `todos[].created`
2059
+ * field it round-trips through) is always a full ISO-8601 timestamp, by
2060
+ * design — this is the display-only seam that reformats it to the
2061
+ * date-only `[date]` bullet documented in docs/reference/state-md.md and
2062
+ * docs/COMMANDS.md. A value that doesn't start with a well-formed
2063
+ * `YYYY-MM-DD` (the 'unknown' fallback, or any other non-conforming
2064
+ * string) passes through unchanged rather than being mangled.
2065
+ */
2066
+ function pendingTodoDateOnly(value) {
2067
+ const match = value.match(/^\d{4}-\d{2}-\d{2}/);
2068
+ return match ? match[0] : value;
2069
+ }
2070
+ function renderPendingTodoBullet(todo, projectRoot) {
2071
+ const date = pendingTodoDateOnly(sanitizePendingTodoInline(pendingTodoFieldAsString(todo['created'], 'unknown')));
2072
+ let area = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['area'], 'general'));
2073
+ let title = sanitizePendingTodoInline(pendingTodoFieldAsString(todo['title'], 'Untitled'));
2074
+ // Strip trailing "." so the fixed "Needs ....` template below never
2075
+ // produces a doubled period when the source text already ended in one.
2076
+ let needs = typeof todo['needs'] === 'string'
2077
+ ? sanitizePendingTodoInline(todo['needs']).replace(/\.+$/, '')
2078
+ : '';
2079
+ const link = `[todo file](${pendingTodoLinkTarget(todo, projectRoot)})`;
2080
+ const assemble = () => {
2081
+ const needsClause = needs ? ` — Needs ${needs}.` : '';
2082
+ return `- [${date}] [${area}] ${title} — ${link}${needsClause}`;
2083
+ };
2084
+ let line = assemble();
2085
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2086
+ return line;
2087
+ // 1) Drop the needs clause entirely first — date/area/title/link untouched.
2088
+ needs = '';
2089
+ line = assemble();
2090
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2091
+ return line;
2092
+ // 2) Shorten the title next, down to a floor — date/area/link untouched.
2093
+ const titleOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2094
+ const targetTitleLen = Math.max(PENDING_TODO_TITLE_FLOOR, title.length - titleOverage);
2095
+ if (targetTitleLen < title.length) {
2096
+ title = truncatePendingTodoText(title, targetTitleLen);
2097
+ line = assemble();
2098
+ }
2099
+ if (line.length <= PENDING_TODO_BULLET_MAX_CHARS)
2100
+ return line;
2101
+ // 3) Shorten area as a last resort — date and the markdown link are never
2102
+ // altered (link correctness > strict cap; see design doc "Known limits").
2103
+ const areaOverage = line.length - PENDING_TODO_BULLET_MAX_CHARS;
2104
+ const targetAreaLen = Math.max(PENDING_TODO_AREA_FLOOR, area.length - areaOverage);
2105
+ if (targetAreaLen < area.length) {
2106
+ area = truncatePendingTodoText(area, targetAreaLen);
2107
+ line = assemble();
2108
+ }
2109
+ return line;
2110
+ }
1901
2111
  function cmdInitTodos(cwd, area, raw) {
1902
2112
  const config = loadConfig(cwd);
1903
- const pendingDir = node_path_1.default.join(planningDir(cwd), 'todos', 'pending');
2113
+ // #4256: todos are root-scoped shared state (migrateToWorkstreams keeps
2114
+ // them at .planning/todos/ and every workflow writer writes that literal
2115
+ // path), so this read resolves via todosDir(cwd) — NOT planningDir(cwd),
2116
+ // which would look in .planning/workstreams/<ws>/todos/ under a workstream
2117
+ // (a directory nothing creates) and report existing todos as absent.
2118
+ const todosRoot = todosDir(cwd);
2119
+ const pendingDir = node_path_1.default.join(todosRoot, 'pending');
1904
2120
  let count = 0;
1905
2121
  const todos = [];
2122
+ // #2618: distinct from "genuinely zero pending todos" — false only when
2123
+ // readdirSync itself failed for a reason OTHER than the directory simply
2124
+ // not existing yet (ENOENT), mirroring the ENOENT-vs-other-errno split
2125
+ // already used above in this file (#3885, ADR-3473 §8.5). Without this,
2126
+ // a real I/O/permission error on the pending dir would look identical to
2127
+ // "no pending todos" and could wipe an existing, non-empty Pending Todos
2128
+ // section in STATE.md on refresh — the fail-safe requirement for #2618.
2129
+ let pendingReadOk = true;
1906
2130
  try {
1907
- const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md'));
2131
+ // #2618: sorted so pending_todos_markdown's bullet order is stable across
2132
+ // runs — readdirSync's order is filesystem-dependent, not contractually
2133
+ // stable, and an unstable order would reorder every bullet on an
2134
+ // unrelated re-render, turning a one-line git diff into a full-section
2135
+ // rewrite (must-have #3). Filenames are `YYYY-MM-DD-slug.md`, so this
2136
+ // also yields a sensible chronological order as a side effect.
2137
+ const files = node_fs_1.default.readdirSync(pendingDir).filter((f) => f.endsWith('.md')).sort();
1908
2138
  for (const file of files) {
1909
2139
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(pendingDir, file));
1910
2140
  if (content === null)
@@ -1916,6 +2146,21 @@ function cmdInitTodos(cwd, area, raw) {
1916
2146
  // #2337: kept in parity with cmdListTodos — surface severity when
1917
2147
  // present, omit the key entirely for todos with no severity line.
1918
2148
  const severityMatch = content.match(/^severity:\s*(.+)$/m);
2149
+ // #2618: first non-empty line of the `## Solution` body, used as the
2150
+ // bullet's "Needs ..." clause. "TBD" (the create_file template's own
2151
+ // placeholder for an unresolved solution) renders no clause at all
2152
+ // rather than the useless literal "Needs TBD.".
2153
+ const solutionSection = (0, markdown_sectionizer_cjs_1.collectSection)(content, (h) => h.level === 2 && h.text.trim() === 'Solution');
2154
+ let needs;
2155
+ if (solutionSection) {
2156
+ const firstLine = solutionSection.body
2157
+ .split('\n')
2158
+ .map((l) => l.trim())
2159
+ .find((l) => l.length > 0);
2160
+ if (firstLine && firstLine.toUpperCase() !== 'TBD') {
2161
+ needs = firstLine;
2162
+ }
2163
+ }
1919
2164
  const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
1920
2165
  if (area && todoArea !== area)
1921
2166
  continue;
@@ -1926,8 +2171,9 @@ function cmdInitTodos(cwd, area, raw) {
1926
2171
  title: titleMatch ? titleMatch[1].trim() : 'Untitled',
1927
2172
  area: todoArea,
1928
2173
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1929
- path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending', file)),
2174
+ path: toPosixPath(node_path_1.default.join(pendingDir, file)),
1930
2175
  ...(severityMatch ? { severity: severityMatch[1].trim() } : {}),
2176
+ ...(needs ? { needs } : {}),
1931
2177
  });
1932
2178
  }
1933
2179
  catch {
@@ -1935,8 +2181,11 @@ function cmdInitTodos(cwd, area, raw) {
1935
2181
  }
1936
2182
  }
1937
2183
  }
1938
- catch {
1939
- /* intentionally empty */
2184
+ catch (err) {
2185
+ const code = err?.code;
2186
+ if (code !== 'ENOENT') {
2187
+ pendingReadOk = false;
2188
+ }
1940
2189
  }
1941
2190
  const result = {
1942
2191
  commit_docs: config.commit_docs,
@@ -1946,11 +2195,22 @@ function cmdInitTodos(cwd, area, raw) {
1946
2195
  todos,
1947
2196
  area_filter: area || null,
1948
2197
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
1949
- pending_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
1950
- completed_dir: toPosixPath(node_path_1.default.join(planningDir(cwd), 'todos', 'completed')),
2198
+ // #4256: both dir fields probe the ROOT todos tree via todosDir(cwd).
2199
+ pending_dir: toPosixPath(pendingDir),
2200
+ completed_dir: toPosixPath(node_path_1.default.join(todosRoot, 'completed')),
2201
+ // planning_exists intentionally stays workstream/project-scoped — it
2202
+ // answers "does the ACTIVE planning dir exist", not a todos question.
1951
2203
  planning_exists: node_fs_1.default.existsSync(planningDir(cwd)),
1952
- todos_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos')),
1953
- pending_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'todos', 'pending')),
2204
+ todos_dir_exists: node_fs_1.default.existsSync(todosRoot),
2205
+ pending_dir_exists: node_fs_1.default.existsSync(pendingDir),
2206
+ // #2618: see PENDING_TODO_BULLET_MAX_CHARS comment / design doc. Consumed
2207
+ // by add-todo.md / check-todos.md's update_state step; omitted entirely
2208
+ // (rather than emitted with possibly-wrong data) when pendingReadOk is
2209
+ // false, so the workflow's fail-safe check can key off field presence.
2210
+ pending_read_ok: pendingReadOk,
2211
+ // #4384 fix: pass cwd as projectRoot so the bullet link renders
2212
+ // repo-relative — see pendingTodoLinkTarget.
2213
+ ...(pendingReadOk ? { pending_todos_markdown: renderPendingTodosMarkdown(todos, cwd) } : {}),
1954
2214
  };
1955
2215
  output(withProjectRoot(cwd, result), raw);
1956
2216
  }
@@ -2065,7 +2325,10 @@ function cmdInitMilestoneOp(cwd, raw) {
2065
2325
  all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
2066
2326
  archived_milestones: archivedMilestones,
2067
2327
  archive_count: archivedMilestones.length,
2068
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2328
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2329
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2330
+ // the full evidence.
2331
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2069
2332
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2070
2333
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2071
2334
  archive_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'archive')),
@@ -2447,10 +2710,23 @@ function cmdInitManager(cwd, raw) {
2447
2710
  recommended_actions: filteredActions,
2448
2711
  waiting_signal: waitingSignal,
2449
2712
  all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0,
2450
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
2713
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
2714
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
2715
+ // the full evidence.
2716
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2451
2717
  roadmap_exists: true,
2452
2718
  state_exists: true,
2453
2719
  manager_flags: managerFlags,
2720
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths — same
2721
+ // pattern cmdInitPlanPhase already uses (existence-checked, toPosixPath'd,
2722
+ // null when absent) plus the archive dir composition milestone.cts's
2723
+ // `cmdMilestoneComplete` uses (#1911: planningPaths(cwd).planning +
2724
+ // 'milestones', workstream-aware). autonomous.md's discover_phases/
2725
+ // iterate/lifecycle steps consume these instead of hardcoding
2726
+ // `.planning/STATE.md` / `.planning/milestones/...`.
2727
+ state_path: node_fs_1.default.existsSync(paths.state) ? toPosixPath(paths.state) : null,
2728
+ roadmap_path: node_fs_1.default.existsSync(paths.roadmap) ? toPosixPath(paths.roadmap) : null,
2729
+ archive_dir: toPosixPath(node_path_1.default.join(paths.planning, 'milestones')),
2454
2730
  };
2455
2731
  output(withProjectRoot(cwd, result), raw);
2456
2732
  }
@@ -2469,10 +2745,57 @@ function cmdInitManager(cwd, raw) {
2469
2745
  */
2470
2746
  function cmdInitCompleteMilestone(cwd, raw, options = {}) {
2471
2747
  const gitCreateTag = detectGitCreateTag(cwd);
2748
+ // #4455: workstream-scoped STATE/ROADMAP/milestone-archive paths for
2749
+ // complete-milestone.md's reorganize_roadmap_and_delete_originals step —
2750
+ // same pattern cmdInitPlanPhase already uses, mirrored here since this is
2751
+ // that workflow's own dedicated init entry point.
2752
+ const planningBase = planningDir(cwd);
2753
+ const statePath = node_path_1.default.join(planningBase, 'STATE.md');
2754
+ const roadmapPath = node_path_1.default.join(planningBase, 'ROADMAP.md');
2755
+ const archiveDir = node_path_1.default.join(planningBase, 'milestones');
2756
+ // #4455 follow-up (code-review finding): MILESTONES.md is workstream-scoped
2757
+ // too — cmdMilestoneComplete (src/milestone.cts) writes it via
2758
+ // planningPaths(cwd).planning (the workstream base, not root; #1911). It is
2759
+ // not the deliberately-root-scoped exception `todos` is (#4256) — an
2760
+ // earlier version of this fix wrongly treated it as a shared root file,
2761
+ // which would have made the safety commit below silently miss the actual
2762
+ // file milestone.complete just wrote under an active workstream.
2763
+ const milestonesPath = node_path_1.default.join(planningBase, 'MILESTONES.md');
2764
+ // #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike
2765
+ // MILESTONES.md, is genuinely SHARED across a project's own workstreams —
2766
+ // never cloned per workstream. gsd-core/references/workstream-flag.md's
2767
+ // directory diagram marks it `# Shared`; new-milestone.md states it
2768
+ // outright ("PROJECT.md is shared across workstreams") and explicitly
2769
+ // SKIPS writing its `## Current Milestone` heading under an active
2770
+ // workstream specifically to avoid clobbering the one shared file (#2308);
2771
+ // cmdWorkstreamCreate (src/workstream.cts) never creates a PROJECT.md
2772
+ // under a workstream directory. The first version of this #4455 follow-up
2773
+ // wrongly generalized from planningPaths()'s structural shape (which
2774
+ // composes `project` under the workstream base) without checking an
2775
+ // actual PROJECT.md write path — resolved against planningRoot(cwd)
2776
+ // (round 2), but that ALSO ignores the separate GSD_PROJECT dimension
2777
+ // (multi-project namespacing, #3749: PROJECT.md legitimately lives at
2778
+ // `.planning/<project>/PROJECT.md` when GSD_PROJECT is set — a real,
2779
+ // tested, pre-existing feature planningRoot's blanket root-only read
2780
+ // broke), caught by gsd-test on this fix's own first push. `planningDir`
2781
+ // with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but
2782
+ // `project` left to default from GSD_PROJECT is the correct middle
2783
+ // ground: respects project-namespacing, ignores workstream-namespacing.
2784
+ const projectPath = node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md');
2785
+ // REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements,
2786
+ // src/planning-workspace.cts) — the git-rm-after-archive step needs the
2787
+ // resolved path too, not the literal root file.
2788
+ const requirementsPath = node_path_1.default.join(planningBase, 'REQUIREMENTS.md');
2472
2789
  const result = {
2473
2790
  // #2994: hoisted from complete-milestone.md's git_tag step
2474
2791
  // <config-check> resolver (git.create_tag, fail-open default true).
2475
2792
  git_create_tag: gitCreateTag,
2793
+ state_path: node_fs_1.default.existsSync(statePath) ? toPosixPath(statePath) : null,
2794
+ roadmap_path: node_fs_1.default.existsSync(roadmapPath) ? toPosixPath(roadmapPath) : null,
2795
+ archive_dir: toPosixPath(archiveDir),
2796
+ milestones_path: node_fs_1.default.existsSync(milestonesPath) ? toPosixPath(milestonesPath) : null,
2797
+ project_path: node_fs_1.default.existsSync(projectPath) ? toPosixPath(projectPath) : null,
2798
+ requirements_path: node_fs_1.default.existsSync(requirementsPath) ? toPosixPath(requirementsPath) : null,
2476
2799
  };
2477
2800
  result['section_manifest'] = buildSectionManifestField(cwd, null, options, 'complete-milestone', {
2478
2801
  gitCreateTag,
@@ -2880,7 +3203,10 @@ function cmdInitProgress(cwd, raw, options = {}) {
2880
3203
  paused_at: pausedAt,
2881
3204
  has_work_in_progress: !!currentPhase,
2882
3205
  phase_mvp_mode: phaseMvpMode,
2883
- project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'PROJECT.md')))),
3206
+ // #4455 follow-up (code-review finding): PROJECT.md is shared across
3207
+ // workstreams — see cmdInitCompleteMilestone's projectPath comment for
3208
+ // the full evidence.
3209
+ project_exists: pathExistsInternal(cwd, toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')))),
2884
3210
  roadmap_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2885
3211
  state_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2886
3212
  // #4040: partial-init discriminator (see buildInitCompletenessFields) —
@@ -2890,7 +3216,19 @@ function cmdInitProgress(cwd, raw, options = {}) {
2890
3216
  // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase.
2891
3217
  state_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'STATE.md')),
2892
3218
  roadmap_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md')),
2893
- project_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'PROJECT.md')),
3219
+ // #4455 follow-up: PROJECT.md is shared across workstreams.
3220
+ project_path: toPosixPath(node_path_1.default.join(planningDir(cwd, null), 'PROJECT.md')),
3221
+ // #4456 correction: an isolated review pass initially "fixed" this to
3222
+ // planningDir(cwd, null) on the assumption that config.json is shared
3223
+ // like PROJECT.md (workstream-flag.md's directory diagram marks it
3224
+ // `# Shared`) — but ADR-0006's own tests (tests/init.test.cjs, "init
3225
+ // handlers honor GSD_WORKSTREAM") assert config_path IS workstream-scoped
3226
+ // for execute-phase/new-project/new-milestone/progress, and gsd-test
3227
+ // caught the regression immediately. The diagram is stale for
3228
+ // config.json specifically (same class of staleness already found for
3229
+ // `milestones/` during the #4455 follow-up) — reverted to the
3230
+ // workstream-aware planningDir(cwd), matching the established,
3231
+ // ADR-governed, tested contract.
2894
3232
  config_path: toPosixPath(node_path_1.default.join(planningDir(cwd), 'config.json')),
2895
3233
  };
2896
3234
  // #2992 (Phase 6.1): additive, optional field — degrades to null, never throws.
@@ -3125,12 +3463,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3125
3463
  warn(`[agent-skills] WARNING: Global skill not found at "${displayPath}/SKILL.md" — skipping\n`);
3126
3464
  continue;
3127
3465
  }
3128
- const pathCheck = (0, security_cjs_1.validatePath)(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
3129
- if (!pathCheck['safe']) {
3130
- const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => {
3131
- const rootCheck = (0, security_cjs_1.validatePath)(globalSkillMd, root, { allowAbsolute: true });
3132
- return Boolean(rootCheck['safe']);
3133
- });
3466
+ const globalSkillMdContained = (0, security_cjs_1.tryWithinRoot)(globalSkillMd, globalSkillsBase, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
3467
+ if (globalSkillMdContained === null) {
3468
+ const acceptedViaTrustedRoot = trustedGlobalRoots.some((root) => (0, security_cjs_1.tryWithinRoot)(globalSkillMd, root, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) !== null);
3134
3469
  if (!acceptedViaTrustedRoot) {
3135
3470
  warn(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
3136
3471
  continue;
@@ -3139,15 +3474,23 @@ function buildAgentSkillsBlock(config, agentType, projectRoot, diagnostics) {
3139
3474
  // trace, not a skip, so it must not land in the diagnostics warnings[].
3140
3475
  process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
3141
3476
  }
3477
+ // `ref` is an emitted display token, not a path anything reads or writes
3478
+ // through — the containment check above is a gate, not a path producer.
3479
+ // Emitting the validated (realpath-resolved, platform-separator) value
3480
+ // instead of this literal broke symlinked skill dirs and Windows output.
3481
+ // The only filesystem read here (existsSync above) already ran on the
3482
+ // lexical path before containment was checked, so ADR-4650's "use the
3483
+ // validated value" rule doesn't apply to this emission.
3142
3484
  validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
3143
3485
  continue;
3144
3486
  }
3145
- const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
3146
- if (!pathCheck['safe']) {
3147
- warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck['error']}\n`);
3487
+ const skillPathContained = (0, security_cjs_1.tryWithinRoot)(skillPath, projectRoot);
3488
+ if (skillPathContained === null) {
3489
+ warn(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": not confined to the project directory\n`);
3148
3490
  continue;
3149
3491
  }
3150
- const skillMdPath = node_path_1.default.join(projectRoot, skillPath, 'SKILL.md');
3492
+ // ADR-4650: the validated value is the value used — never re-derive from raw input.
3493
+ const skillMdPath = node_path_1.default.join(skillPathContained, 'SKILL.md');
3151
3494
  if (!node_fs_1.default.existsSync(skillMdPath)) {
3152
3495
  // #2941: if the bare name matches a global skill, hint at the global: prefix.
3153
3496
  // The bare name resolves as project-relative (which doesn't exist), but the
@@ -3203,20 +3546,37 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3203
3546
  // persona fallback. Triggering the fallback for claude would change the
3204
3547
  // documented "unconfigured → empty block" contract that agent-skills tests
3205
3548
  // pin.
3549
+ //
3550
+ // #4407 (ADR-4139 stream 2): this is the one place GSD's own agent-persona
3551
+ // content is served through a real code seam rather than an eagerly
3552
+ // @-included file, so the compact/canonical choice is made here in code
3553
+ // (a real exit code) instead of a prose config-get gate. Compact is tried
3554
+ // first when requested; a missing compact sibling falls back to canonical
3555
+ // with the fallback disclosed in the payload itself, never a silent switch.
3556
+ let agentPayloadVariant = null;
3206
3557
  if (!block) {
3207
3558
  const runtime = (config && config['runtime']) || process.env['GSD_RUNTIME'] || 'claude';
3208
3559
  if (runtime !== 'claude') {
3209
3560
  const agentCheck = checkAgentsInstalled(runtime, projectRoot);
3210
3561
  const agentsDir = agentCheck?.agents_dir;
3211
3562
  if (typeof agentsDir === 'string' && agentsDir.length > 0) {
3212
- const agentFile = node_path_1.default.join(agentsDir, `${agentType}.md`);
3213
- try {
3214
- const content = (0, shell_command_projection_cjs_1.platformReadSync)(agentFile);
3215
- if (content && content.length > 0) {
3216
- block = content;
3563
+ const compactRequested = readConfigJsonBoolean(projectRoot, ['workflow', 'compact_content']);
3564
+ const compactContent = compactRequested
3565
+ ? readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.compact.md`))
3566
+ : null;
3567
+ if (compactContent !== null) {
3568
+ block = compactContent;
3569
+ agentPayloadVariant = 'compact';
3570
+ }
3571
+ else {
3572
+ const canonicalContent = readNonEmptyFileOrNull(node_path_1.default.join(agentsDir, `${agentType}.md`));
3573
+ if (canonicalContent !== null) {
3574
+ block = compactRequested
3575
+ ? `<!-- gsd: no compact payload registered for ${agentType}; serving canonical -->\n\n${canonicalContent}`
3576
+ : canonicalContent;
3577
+ agentPayloadVariant = 'canonical';
3217
3578
  }
3218
3579
  }
3219
- catch { /* agent file not found — fall through to empty block */ }
3220
3580
  }
3221
3581
  }
3222
3582
  }
@@ -3263,8 +3623,8 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3263
3623
  if (jsonMode) {
3264
3624
  // Build the Resolution<AgentSkillsValue> envelope and embed .value additively.
3265
3625
  // Flat fields are retained unchanged for back-compat; value formalises the
3266
- // Resolution convention (ADR-1411 P3, #1416). source/degraded remain
3267
- // config-provenance extras, outside the Resolution<T> envelope.
3626
+ // Resolution convention (ADR-1411 P3, #1416). source/degraded/agent_payload_variant
3627
+ // remain config-provenance extras, outside the Resolution<T> envelope.
3268
3628
  const resolution = (0, resolution_cjs_1.makeResolution)({ block: block || '', skills_count: normalizedPaths.length }, { configured, reason, warnings: diagnostics.warnings });
3269
3629
  output({
3270
3630
  agent_type: agentType,
@@ -3275,6 +3635,7 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
3275
3635
  reason,
3276
3636
  source,
3277
3637
  degraded,
3638
+ agent_payload_variant: agentPayloadVariant,
3278
3639
  value: resolution.value,
3279
3640
  }, raw);
3280
3641
  return;
@@ -3563,4 +3924,5 @@ module.exports = {
3563
3924
  cmdAgentSkills,
3564
3925
  buildSkillManifest,
3565
3926
  cmdSkillManifest,
3927
+ renderPendingTodosMarkdown,
3566
3928
  };