@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
@@ -48,6 +48,7 @@ const coreUtilsMod = require("./core-utils.cjs");
48
48
  const planningScopeMod = require("./planning-scope.cjs");
49
49
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
50
50
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
51
+ const security_cjs_1 = require("./security.cjs");
51
52
  const { output, error } = io;
52
53
  const { extractPhaseToken, scopeToPhase } = phaseId;
53
54
  const { extractFrontmatter } = frontmatterMod;
@@ -251,8 +252,11 @@ function computeCoveredDigest(projectRoot, coveredFiles) {
251
252
  // confinement check above is not enough. realpathSync resolves the
252
253
  // actual target; re-confining against realRoot closes that gap.
253
254
  const real = node_fs_1.default.realpathSync(resolved);
254
- const realRel = node_path_1.default.relative(realRoot, real);
255
- if (realRel === '' || realRel === '..' || realRel.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(realRel)) {
255
+ // Both operands are already realpath-resolved (this fn's own realpathSync calls
256
+ // above), so the shared containment comparison applies directly (ADR-4650) —
257
+ // no re-resolution through assertWithinRoot/tryWithinRoot, which would redo work
258
+ // this function already owns for its exists-vs-escaped tri-state.
259
+ if (!(0, security_cjs_1.isContainedIn)(real, realRoot)) {
256
260
  return null;
257
261
  }
258
262
  const st = node_fs_1.default.statSync(real);
@@ -471,7 +475,7 @@ function resolvePhaseArtifactFile(entries, bareName, options = {}) {
471
475
  // filtered out, and if that leaves nothing the code falls through to
472
476
  // `allowBare`/`null` deliberately.
473
477
  const scoped = options.phaseDirName
474
- ? scopeToPhase(candidates, options.phaseDirName)
478
+ ? scopeToPhase(candidates, options.phaseDirName, options.convention)
475
479
  : candidates;
476
480
  if (scoped.length > 0)
477
481
  return scoped[0];
@@ -528,7 +532,7 @@ function resolveVerificationFile(entries, options = {}) {
528
532
  function resolveUatFile(entries, options = {}) {
529
533
  return resolvePhaseArtifactFile(entries, 'UAT.md', options);
530
534
  }
531
- function findStaleVerificationSummary(phaseDir, fsImpl = defaultFsImpl, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs) {
535
+ function findStaleVerificationSummary(phaseDir, fsImpl = defaultFsImpl, phaseCleanCommitTimesMs = defaultPhaseCleanCommitTimesMs, convention) {
532
536
  // FS errors (TOCTOU: a SUMMARY listed by scanPhasePlans then removed before statSync;
533
537
  // unreadable dir; broken symlink; file->dir swap) must degrade rather than throw
534
538
  // uncaught into callers that are NOT under the planning lock (init.manager /
@@ -544,9 +548,20 @@ function findStaleVerificationSummary(phaseDir, fsImpl = defaultFsImpl, phaseCle
544
548
  // or sentinel-numbered canonically-shaped file cannot outrank this
545
549
  // phase's own (possibly non-canonical) report. #3511: phaseDirName scopes
546
550
  // the fallback path to this same phase (see resolveVerificationFile docs).
551
+ // #4187: allowBare — this staleness seam must see the same report set the
552
+ // status reader sees, or a bare report could never read `stale` while its
553
+ // dashed twin could (two answers from one verb).
547
554
  const phaseDirName = node_path_1.default.basename(phaseDir);
548
- const phaseToken = extractPhaseToken(phaseDirName);
549
- const verificationFile = resolveVerificationFile(phaseFiles, { phaseToken, phaseDirName });
555
+ // #612: derive the token with the resolved convention so a bracket dir's
556
+ // own token is read behind its `{CODE}.{MM}-` prefix. #4187: keep the bare
557
+ // report tier aligned with the status reader.
558
+ const phaseToken = extractPhaseToken(phaseDirName, convention);
559
+ const verificationFile = resolveVerificationFile(phaseFiles, {
560
+ allowBare: true,
561
+ phaseToken,
562
+ phaseDirName,
563
+ convention,
564
+ });
550
565
  if (!verificationFile)
551
566
  return { determined: true, stale: false };
552
567
  const summaryFiles = scanPhasePlans(phaseDir).summaryFiles
@@ -587,7 +602,9 @@ function findStaleVerificationSummary(phaseDir, fsImpl = defaultFsImpl, phaseCle
587
602
  * 1. Find the phase's verification report via `resolveVerificationFile`
588
603
  * (canonical `<phase-token>-VERIFICATION.md` preferred; falls back to the
589
604
  * alphabetically-first `*-VERIFICATION.md` that belongs to THIS phase when
590
- * none is canonical — #3357/#3511). If none → status 'missing'.
605
+ * none is canonical — #3357/#3511; and, when the directory's only report
606
+ * is a bare `VERIFICATION.md`, that file — #4187, matching
607
+ * `verification.resolve-file`). If none → status 'missing'.
591
608
  * 2. Extract `status` from FRONTMATTER ONLY via the shared extractFrontmatter
592
609
  * parser (DEFECT.FRONTMATTER-SCALAR-BROAD-GREP fix — parser anchors at byte 0).
593
610
  * If no frontmatter block or no `status` key → status 'missing'.
@@ -608,7 +625,12 @@ function readVerificationStatus(phaseDir, opts = {}) {
608
625
  const fsImpl = opts.fs ?? defaultFsImpl;
609
626
  const phaseCleanCommitTimesMs = opts.phaseCleanCommitTimesMs ?? defaultPhaseCleanCommitTimesMs;
610
627
  const runtime = opts.runtime ?? 'claude';
611
- // Phase token for the gaps_found command
628
+ // Phase token for the gaps_found command — deliberately convention-LESS
629
+ // even when `opts.convention` is present: the token becomes a bare COMMAND
630
+ // ARGUMENT below, and a bare bracket phase number is milestone-ambiguous
631
+ // (`02` cannot tell GSD.01-02 from GSD.02-02), so the argument keeps its
632
+ // pre-#612 shape. The convention-aware token is derived separately for
633
+ // FILE RESOLUTION only (`resolutionToken`, at the readdir below).
612
634
  const baseName = node_path_1.default.basename(phaseDir);
613
635
  const phaseToken = extractPhaseToken(baseName);
614
636
  const derivedPhaseNumber = phaseToken.length > 0 ? phaseToken : baseName;
@@ -624,12 +646,21 @@ function readVerificationStatus(phaseDir, opts = {}) {
624
646
  let verificationFile = null;
625
647
  try {
626
648
  const entries = fsImpl.readdirSync(phaseDir);
627
- // #3492: pin selection to THIS phase's own token (already derived above
628
- // for the routed command argument) so a stray cross-phase or
629
- // sentinel-numbered canonically-shaped file cannot outrank this phase's
630
- // own (possibly non-canonical) report. #3511: baseName also scopes the
631
- // fallback path to this same phase (see resolveVerificationFile docs).
632
- verificationFile = resolveVerificationFile(entries, { phaseToken, phaseDirName: baseName });
649
+ // #3492: pin selection to THIS phase's own token so a stray cross-phase
650
+ // or sentinel-numbered canonically-shaped file cannot outrank this phase's
651
+ // own report. #612: derive a separate convention-aware RESOLUTION token
652
+ // for bracket directories while the routed command argument above stays
653
+ // convention-less and milestone-unambiguous. #4187: keep the bare report
654
+ // tier aligned with every other verification reader.
655
+ const resolutionToken = opts.convention === 'bracket'
656
+ ? extractPhaseToken(baseName, opts.convention)
657
+ : phaseToken;
658
+ verificationFile = resolveVerificationFile(entries, {
659
+ allowBare: true,
660
+ phaseToken: resolutionToken,
661
+ phaseDirName: baseName,
662
+ convention: opts.convention,
663
+ });
633
664
  }
634
665
  catch {
635
666
  // Directory unreadable → treat as missing
@@ -716,7 +747,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
716
747
  !allCurrentArtifactsCovered(phaseDir, coveredFilesVal);
717
748
  }
718
749
  else {
719
- const staleCheck = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs);
750
+ const staleCheck = findStaleVerificationSummary(phaseDir, fsImpl, phaseCleanCommitTimesMs, opts.convention);
720
751
  isStale = staleCheck.determined && staleCheck.stale;
721
752
  // staleCheck is either {determined:true, stale:false} (checked; nothing
722
753
  // stale) or {determined:false} (could not check — fs/scan/clock failure).
@@ -800,6 +831,7 @@ function isPhaseComplete(phaseDir, deps = {}) {
800
831
  phaseCleanCommitTimesMs: deps.phaseCleanCommitTimesMs,
801
832
  runtime: deps.runtime,
802
833
  phaseNumber: deps.phaseNumber,
834
+ convention: deps.convention,
803
835
  });
804
836
  return {
805
837
  value: {
@@ -453,7 +453,7 @@ function declaredPathCovers(declaredPaths, norm) {
453
453
  if (typeof p !== 'string')
454
454
  return false;
455
455
  const dp = stripLeadingDotSlash(toSlash(p));
456
- return dp === target || dp.startsWith(target + '/');
456
+ return dp === target || dp.startsWith(target + '/'); // allow-handrolled-containment: declared-path coverage for pending-creation detection, not containment
457
457
  });
458
458
  }
459
459
  /**
@@ -13,6 +13,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_os_1 = __importDefault(require("node:os"));
15
15
  const validate_cjs_1 = require("./validate.cjs");
16
+ const security_cjs_1 = require("./security.cjs");
16
17
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
17
18
  const planningWorkspace = require("./planning-workspace.cjs");
18
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
@@ -37,7 +38,7 @@ const worktreeSafetyMod = require("./worktree-safety.cjs");
37
38
  // codebase-drift --name-status parse loop).
38
39
  const { decodeGitQuotedPath } = worktreeSafetyMod;
39
40
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
40
- const security_cjs_1 = require("./security.cjs");
41
+ const security_cjs_2 = require("./security.cjs");
41
42
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
42
43
  const schema_detect_cjs_1 = require("./schema-detect.cjs");
43
44
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
@@ -63,7 +64,11 @@ const { SEVERITY: HEALTH_SEVERITY, REMEDY_ACTION, REMEDY_RISK, evaluateRules, ev
63
64
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-snapshot.cjs is an export= CommonJS module
64
65
  const planningSnapshotMod = require("./planning-snapshot.cjs");
65
66
  const { buildPlanningSnapshot } = planningSnapshotMod;
66
- const { planningDir } = planningWorkspace;
67
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- onboard-projection.cjs is an export= CommonJS module
68
+ const onboardProjectionMod = require("./onboard-projection.cjs");
69
+ const { REQUIRED_CODEBASE_MAP_FILES } = onboardProjectionMod;
70
+ const clock_cjs_1 = require("./clock.cjs");
71
+ const { planningDir, planningRoot, withPlanningLock } = planningWorkspace;
67
72
  const { defaultPhaseCleanCommitTimesMs } = verificationMod;
68
73
  const { extractFrontmatter, parseMustHavesBlock } = frontmatterMod;
69
74
  const { readStateHeadFreshness } = stateMod;
@@ -152,9 +157,12 @@ function verifySummaryCore(cwd, summaryPath, checkFileCount, opts) {
152
157
  if (firstSegment.indexOf('.') > 0)
153
158
  return false;
154
159
  // Containment guard: a `../`-bearing reference must not turn this advisory
155
- // into a filesystem existence probe outside the project.
156
- const resolved = node_path_1.default.resolve(projectRoot, candidate);
157
- if (resolved !== projectRoot && !resolved.startsWith(projectRoot + node_path_1.default.sep))
160
+ // into a filesystem existence probe outside the project. Lexical (ADR-4650
161
+ // decision 6): the candidate is a string pulled from a SUMMARY document and
162
+ // by construction may not exist yet — existence is what gets probed
163
+ // downstream — and this is a pure string-heuristic filter with no other fs
164
+ // access, so a realpath call would also change its cost profile.
165
+ if ((0, security_cjs_1.tryWithinRootLexical)(candidate, projectRoot) === null)
158
166
  return false;
159
167
  return true;
160
168
  };
@@ -1378,11 +1386,11 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1378
1386
  // project. Leave sourceContent as null so the existing not-found /
1379
1387
  // pending classification below runs unchanged. Note this guard is
1380
1388
  // narrower than it may look: `from: "."` is a non-empty string, so it
1381
- // still reaches validatePath and safeReadFile below, and DOES read the
1389
+ // still reaches tryWithinRoot and safeReadFile below, and DOES read the
1382
1390
  // cwd directory (yielding "Source read failed: EISDIR") — this branch
1383
1391
  // only short-circuits the true empty-string case.
1384
- const fromCheck = (0, security_cjs_1.validatePath)(fromPath, cwd);
1385
- if (!fromCheck.safe) {
1392
+ const fromContained = (0, security_cjs_2.tryWithinRoot)(fromPath, cwd);
1393
+ if (fromContained === null) {
1386
1394
  // Do not echo result.error — it embeds absolute host paths.
1387
1395
  check['path_rejected'] = 'from';
1388
1396
  check['detail'] = 'Source path rejected — resolves outside the project directory';
@@ -1390,7 +1398,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1390
1398
  continue;
1391
1399
  }
1392
1400
  try {
1393
- sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(fromCheck.resolved);
1401
+ sourceContent = (0, shell_command_projection_cjs_1.platformReadSync)(fromContained);
1394
1402
  }
1395
1403
  catch (err) {
1396
1404
  // Report the errno only — never the message or path (untrusted `from:`
@@ -1458,8 +1466,8 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1458
1466
  // An empty/missing `to:` is a malformed plan, not a
1459
1467
  // path-confinement violation — only a non-empty path that
1460
1468
  // actually resolves outside the project is path_rejected.
1461
- const toCheck = (0, security_cjs_1.validatePath)(toPath, cwd);
1462
- if (!toCheck.safe) {
1469
+ const toContained = (0, security_cjs_2.tryWithinRoot)(toPath, cwd);
1470
+ if (toContained === null) {
1463
1471
  // Do not read a rejected `to:` — treat as no target content
1464
1472
  // and do not echo result.error, which embeds absolute host
1465
1473
  // paths.
@@ -1467,7 +1475,7 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
1467
1475
  check['detail'] = `Pattern "${link['pattern']}" not found in source; target path rejected — resolves outside the project directory`;
1468
1476
  }
1469
1477
  else {
1470
- targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toCheck.resolved);
1478
+ targetContent = (0, shell_command_projection_cjs_1.platformReadSync)(toContained);
1471
1479
  }
1472
1480
  }
1473
1481
  if (targetContent && pat.test(targetContent)) {
@@ -1823,9 +1831,9 @@ function resolvePhaseDirByToken(phasesDir, phaseArg) {
1823
1831
  const matched = matchPhaseDirs(dirNames, normalizedPhase).matches[0];
1824
1832
  if (matched)
1825
1833
  return node_path_1.default.join(phasesDir, matched);
1826
- const check = (0, security_cjs_1.validatePath)(phaseArg, phasesDir);
1827
- if (check.safe && node_fs_1.default.existsSync(check.resolved))
1828
- return check.resolved;
1834
+ const contained = (0, security_cjs_2.tryWithinRoot)(phaseArg, phasesDir);
1835
+ if (contained !== null && node_fs_1.default.existsSync(contained))
1836
+ return contained;
1829
1837
  return null;
1830
1838
  }
1831
1839
  /**
@@ -1986,6 +1994,110 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
1986
1994
  skipped: isSkipped,
1987
1995
  }, raw);
1988
1996
  }
1997
+ /**
1998
+ * Stamp `last_mapped_commit` (plus `last_mapped_at`) into the frontmatter of
1999
+ * every codebase-map document that exists on disk, using the current HEAD sha.
2000
+ *
2001
+ * #3418: `drift.cjs` shipped a correct `writeMappedCommit` with no production
2002
+ * caller, so no full `/gsd:map-codebase` run ever wrote the machine-readable
2003
+ * baseline that `cmdVerifyCodebaseDrift` reads. The stamp lives in CODE rather
2004
+ * than in a prose instruction to the mapper agent on purpose: an agent that
2005
+ * decides its work is already done skips a prose step silently, which is the
2006
+ * exact class of failure the stamp exists to detect.
2007
+ *
2008
+ * Only documents that already exist are stamped -- a `--fast` map produces four
2009
+ * of the seven, and this must not conjure the missing three as frontmatter-only
2010
+ * stubs that would then satisfy the seven-file completeness probe. `only`
2011
+ * (`--files a.md,b.md`) narrows further, for a caller that refreshed a subset.
2012
+ *
2013
+ * Non-blocking by contract, like every other drift surface: any failure emits
2014
+ * `skipped` with a reason and exits 0.
2015
+ */
2016
+ function cmdStampCodebaseMap(cwd, raw, only) {
2017
+ // Non-hoisted: load-order matters for circular dep guard
2018
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
2019
+ const drift = require('./drift.cjs');
2020
+ const emit = (payload) => output(payload, raw);
2021
+ const skip = (reason) => ({
2022
+ stamped: [],
2023
+ commit: null,
2024
+ stamped_at: null,
2025
+ skipped: true,
2026
+ reason,
2027
+ });
2028
+ try {
2029
+ // Probed outside the lock on purpose: taking the lock creates `.planning/`
2030
+ // if absent, and a stamp against a project that has no codebase map at all
2031
+ // must not conjure the directory. The per-document existence check inside
2032
+ // the lock is the one that matters -- a map deleted after this probe lands
2033
+ // on `no-codebase-map` there rather than being recreated as stubs.
2034
+ const codebaseDir = node_path_1.default.join(planningDir(cwd), 'codebase');
2035
+ if (!node_fs_1.default.existsSync(codebaseDir)) {
2036
+ emit(skip('no-codebase-dir'));
2037
+ return;
2038
+ }
2039
+ // `--files` restricts the stamp to the documents a caller actually
2040
+ // refreshed. The execute-phase auto-remap path rewrites STRUCTURE.md and
2041
+ // ARCHITECTURE.md only; stamping the other five at HEAD there would claim a
2042
+ // currency they do not have. Membership is checked against the closed
2043
+ // seven-document set, so an unknown name is a fail-loud non-answer rather
2044
+ // than a path this function tries to resolve. An empty value is refused
2045
+ // rather than read as "no filter": a caller that meant to narrow the scope
2046
+ // must not silently widen it to all seven.
2047
+ let candidates = REQUIRED_CODEBASE_MAP_FILES;
2048
+ if (only) {
2049
+ if (only.length === 0) {
2050
+ emit(skip('empty-codebase-map-file-filter'));
2051
+ return;
2052
+ }
2053
+ const unknown = only.filter((file) => !candidates.includes(file));
2054
+ if (unknown.length > 0) {
2055
+ emit(skip('unknown-codebase-map-file: ' + unknown.join(',')));
2056
+ return;
2057
+ }
2058
+ candidates = candidates.filter((file) => only.includes(file));
2059
+ }
2060
+ // Everything the stamp reads is read under the lock it writes under, the
2061
+ // same lock the rest of the .planning/ writers take. Two stampers can run
2062
+ // at once (the full map-codebase run and the execute-phase auto-remap);
2063
+ // one that resolved HEAD or listed the present documents before waiting on
2064
+ // the lock would write its now-stale sha over the newer one, or recreate a
2065
+ // document deleted while it waited as a frontmatter-only stub.
2066
+ const result = withPlanningLock(cwd, () => {
2067
+ const revProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', 'HEAD'], { cwd });
2068
+ if (revProbe.exitCode !== 0)
2069
+ return skip('not-a-git-repo');
2070
+ const commit = revProbe.stdout.trim();
2071
+ if (!/^[0-9a-f]{7,40}$/.test(commit))
2072
+ return skip('unreadable-head');
2073
+ const present = candidates.filter((file) => node_fs_1.default.existsSync(node_path_1.default.join(codebaseDir, file)));
2074
+ if (present.length === 0)
2075
+ return skip('no-codebase-map');
2076
+ // Host-local calendar day, matching the `**Analysis Date:**` line the
2077
+ // mapper agent writes -- the two freshness markers must not disagree by
2078
+ // a timezone.
2079
+ const stampedAt = clock_cjs_1.realClock.localToday();
2080
+ const write = drift['writeMappedCommit'];
2081
+ const stamped = [];
2082
+ const failed = [];
2083
+ for (const file of present) {
2084
+ try {
2085
+ write(node_path_1.default.join(codebaseDir, file), commit, stampedAt);
2086
+ stamped.push(file);
2087
+ }
2088
+ catch (err) {
2089
+ // One unwritable document must not cost the stamp on the other six.
2090
+ failed.push({ file, reason: err instanceof Error ? err.message : String(err) });
2091
+ }
2092
+ }
2093
+ return { stamped, failed, commit, stamped_at: stampedAt, skipped: false, reason: null };
2094
+ });
2095
+ emit(result);
2096
+ }
2097
+ catch (err) {
2098
+ emit(skip('exception: ' + (err instanceof Error ? err.message : String(err))));
2099
+ }
2100
+ }
1989
2101
  function cmdVerifyCodebaseDrift(cwd, raw) {
1990
2102
  // Non-hoisted: load-order matters for circular dep guard
1991
2103
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- drift.cjs is an export= CommonJS module
@@ -2034,16 +2146,45 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
2034
2146
  });
2035
2147
  return;
2036
2148
  }
2037
- const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
2038
- let base = lastMapped;
2039
- if (!base) {
2040
- base = EMPTY_TREE;
2149
+ // #3418: an absent or unresolvable baseline means NO COMPARISON IS POSSIBLE.
2150
+ // It is neither zero drift nor total drift, and reporting it as either is a
2151
+ // lie the consumer cannot detect. The former fallback diffed HEAD against
2152
+ // the empty tree, so every tracked file read as newly added and the gate
2153
+ // reported maximum drift identically on every run -- which made a genuinely
2154
+ // stale map indistinguishable from a fresh one, and let `spawn_mapper` fire
2155
+ // a whole-repo remap while presenting itself as an incremental one.
2156
+ if (!lastMapped) {
2157
+ emit({
2158
+ block: false,
2159
+ skipped: true,
2160
+ reason: 'no-mapped-commit',
2161
+ action_required: false,
2162
+ directive: 'none',
2163
+ elements: [],
2164
+ last_mapped_commit: null,
2165
+ });
2166
+ return;
2041
2167
  }
2042
- else {
2043
- const verify = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', base], { cwd });
2044
- if (verify.exitCode !== 0)
2045
- base = EMPTY_TREE;
2168
+ const baseProbe = (0, shell_command_projection_cjs_1.execGit)(['cat-file', '-t', lastMapped], { cwd });
2169
+ if (baseProbe.exitCode !== 0 || baseProbe.stdout.trim() !== 'commit') {
2170
+ // A stamp git cannot resolve: history rewrite, GC, or a shallow clone.
2171
+ // Distinct reason from 'no-mapped-commit' -- the map claims a baseline,
2172
+ // this repository just cannot see it, which is an operator-actionable
2173
+ // difference (re-map vs. unshallow). A resolvable non-commit (a tree or
2174
+ // blob sha, a ref name) is the same class of bad baseline: git would
2175
+ // happily diff against it and report drift against the wrong object.
2176
+ emit({
2177
+ block: false,
2178
+ skipped: true,
2179
+ reason: 'unresolvable-mapped-commit',
2180
+ action_required: false,
2181
+ directive: 'none',
2182
+ elements: [],
2183
+ last_mapped_commit: lastMapped,
2184
+ });
2185
+ return;
2046
2186
  }
2187
+ const base = lastMapped;
2047
2188
  const diff = (0, shell_command_projection_cjs_1.execGit)(['diff', '--name-status', base, 'HEAD'], { cwd });
2048
2189
  if (diff.exitCode !== 0) {
2049
2190
  emit({
@@ -2056,6 +2197,27 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
2056
2197
  });
2057
2198
  return;
2058
2199
  }
2200
+ // #3418: GSD's own planning artifacts are not codebase structure. A
2201
+ // map-codebase run commits `.planning/codebase/*.md`, so a correctly
2202
+ // stamped baseline would be re-poisoned by the very commit that carries
2203
+ // the stamp -- the next gate invocation would report the map's own seven
2204
+ // documents as seven new directories, back over the default threshold of
2205
+ // three. Derived from planningRoot() rather than a hardcoded literal so a
2206
+ // repoint of the planning root cannot leave this filter behind.
2207
+ //
2208
+ // `git diff --name-status` always prints repo-root-relative paths, so a cwd
2209
+ // below the root needs the `sub/` prefix or the filter matches nothing.
2210
+ // That prefix comes from git (`--show-prefix`: root-relative, forward
2211
+ // slashes, trailing slash, empty at the root). The rejected alternative was
2212
+ // path.relative(`--show-toplevel`, cwd), which mixes two path producers: on
2213
+ // Windows os.tmpdir() hands back the 8.3 short form while git resolves the
2214
+ // long one, so relative() between them yields a `../..` chain that matches
2215
+ // nothing. The `.planning` half below is safe to compute with relative()
2216
+ // because both of its sides are the same cwd string.
2217
+ const prefixProbe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-prefix'], { cwd });
2218
+ const repoPrefix = prefixProbe.exitCode === 0 ? prefixProbe.stdout.trim() : '';
2219
+ const planningPrefix = repoPrefix + node_path_1.default.relative(cwd, planningRoot(cwd)).split(node_path_1.default.sep).join('/') + '/';
2220
+ const isPlanningArtifact = (file) => file.split('\\').join('/').startsWith(planningPrefix);
2059
2221
  const added = [];
2060
2222
  const modified = [];
2061
2223
  const deleted = [];
@@ -2076,6 +2238,8 @@ function cmdVerifyCodebaseDrift(cwd, raw) {
2076
2238
  // ASCII common case — passes through untouched. Both capture groups are
2077
2239
  // decoded: R/C lines carry old AND new paths, either may be quoted.
2078
2240
  const file = decodeGitQuotedPath(m[3] || m[2]);
2241
+ if (isPlanningArtifact(file))
2242
+ continue;
2079
2243
  if (status === 'A' || status === 'R' || status === 'C')
2080
2244
  added.push(file);
2081
2245
  else if (status === 'M')
@@ -2154,5 +2318,6 @@ module.exports = {
2154
2318
  cmdVerifyCodebaseDrift,
2155
2319
  computeContextDrift,
2156
2320
  cmdVerifyContextDrift,
2321
+ cmdStampCodebaseMap,
2157
2322
  STATE_HEAD_ADVISORY_COMMITS,
2158
2323
  };
@@ -733,6 +733,7 @@ module.exports = {
733
733
  inspectWorkstream,
734
734
  isCompletedInventory: workstream_inventory_builder_cjs_1.isCompletedInventory,
735
735
  listWorkstreamInventories,
736
+ readCurrentMilestoneVersion,
736
737
  sortWorkstreamInventories,
737
738
  workstreamsRoot,
738
739
  };
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  const node_fs_1 = __importDefault(require("node:fs"));
15
15
  const node_path_1 = __importDefault(require("node:path"));
16
16
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
17
+ const security_cjs_1 = require("./security.cjs");
17
18
  // Default timeout for worktree-related git subprocess calls.
18
19
  // 10 s is generous enough for normal git operations on large repos while still
19
20
  // providing a deterministic failure path when git stalls (locked index, hung
@@ -578,7 +579,7 @@ function normalizeScopePath(raw) {
578
579
  */
579
580
  function isSummaryArtifactRelPath(relPath) {
580
581
  const normalized = normalizeScopePath(relPath);
581
- return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`)
582
+ return normalized.startsWith(`${SUMMARY_ARTIFACT_DIR}/`) // allow-handrolled-containment: artifact-type classification by a fixed known subdirectory name, not a filesystem root-confinement gate
582
583
  && normalized.endsWith(SUMMARY_ARTIFACT_SUFFIX);
583
584
  }
584
585
  /**
@@ -772,7 +773,8 @@ function planWaveScopeConformance(changedPaths, declaredFiles, branch) {
772
773
  seen.add(changed);
773
774
  if (isSummaryArtifactRelPath(changed))
774
775
  continue;
775
- const covered = prefixes.some((prefix) => (prefix === null || changed === prefix || changed.startsWith(`${prefix}/`)));
776
+ const covered = prefixes.some((prefix) => (prefix === null || changed === prefix || changed.startsWith(`${prefix}/`) // allow-handrolled-containment: advisory scope-coverage match against a caller-declared prefix (documented above as deliberately distinct from a security gate) — not a filesystem root-confinement decision
777
+ ));
776
778
  if (covered)
777
779
  continue;
778
780
  warnings.push({ code: WAVE_CLEANUP_WARNING.SCOPE_OUT_OF_DECLARED, branch, path: changed });
@@ -1581,11 +1583,15 @@ function cmdWorktreeCreate(cwd, args = [], deps = {}) {
1581
1583
  {
1582
1584
  const absRoot = node_path_1.default.resolve(cwd, rootFlag);
1583
1585
  const absWorktree = node_path_1.default.resolve(cwd, plan.entry.worktree_path);
1584
- const rel = node_path_1.default.relative(absRoot, absWorktree);
1585
- // rel === '' → the worktree IS the root (would clobber the checkout)
1586
- // rel === '..' / '../…' → escapes the root
1587
- // path.isAbsolute(rel) → a different Windows drive or UNC root
1588
- if (rel === '' || rel === '..' || rel.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(rel)) {
1586
+ // Lexical containment (ADR-4650): the worktree does not exist yet, so there is
1587
+ // nothing to realpath. Both operands are already resolved above, so the shared
1588
+ // `isContainedIn` comparison applies directly (mirrors the already-resolved-caller
1589
+ // exception documented on `isContainedIn`) — a different Windows drive/UNC root
1590
+ // fails the prefix comparison the same way an escaping relative path does.
1591
+ // The canonical predicate treats target === root as CONTAINED; this call site
1592
+ // explicitly REJECTS that case (absWorktree === absRoot below), because a worktree
1593
+ // AT the root would clobber the checkout — the inversion this migration preserves.
1594
+ if (absWorktree === absRoot || !(0, security_cjs_1.isContainedIn)(absWorktree, absRoot)) {
1589
1595
  const hint = `--path must resolve INSIDE --root (root="${absRoot}", path="${absWorktree}"). A worktree outside the declared root is unreachable by manifest-scoped cleanup and would let a spawned executor write outside the project.`;
1590
1596
  writeErr(`[gsd] worktree.create: path_outside_root — ${hint}\n`);
1591
1597
  write(`${JSON.stringify({ ok: false, reason: 'path_outside_root', hint }, null, 2)}\n`);
@@ -38,6 +38,7 @@
38
38
  "ui_phase": true,
39
39
  "ui_safety_gate": true,
40
40
  "text_mode": false,
41
+ "compact_content": false,
41
42
  "research_before_questions": false,
42
43
  "discuss_mode": "discuss",
43
44
  "skip_discuss": false,
@@ -22,6 +22,7 @@
22
22
  "workflow.smart_zone_tokens",
23
23
  "workflow.human_verify_mode",
24
24
  "workflow.text_mode",
25
+ "workflow.compact_content",
25
26
  "workflow.research_before_questions",
26
27
  "workflow.discuss_mode",
27
28
  "workflow.skip_discuss",
@@ -75,7 +76,11 @@
75
76
  "planner.stall_threshold_minutes",
76
77
  "workflow.inline_plan_threshold",
77
78
  "hooks.context_warnings",
79
+ "hooks.context_warning_threshold",
80
+ "hooks.context_critical_threshold",
78
81
  "hooks.workflow_guard",
82
+ "hooks.commit_types",
83
+ "hooks.community",
79
84
  "workflow.context_coverage_gate",
80
85
  "statusline.show_last_command",
81
86
  "statusline.context_position",