@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
@@ -677,8 +677,25 @@ function stageValidated(opts) {
677
677
  throw new Error(`Hook fragment validation failed: ${fragErrs.join('; ')}`);
678
678
  }
679
679
  // Cross-capability validations (contract, consumes, cross-capability).
680
- const capMap = new Map([[id, cap]]);
681
- const centralKeys = new Set();
680
+ //
681
+ // #3929: seed the validation set the way the loader builds its accepted
682
+ // map — frozen first-party registry, then each committed overlay of the
683
+ // TARGET (global) install scope accepted incrementally (structural +
684
+ // engines + full-suite-clean), then the candidate LAST (mirroring
685
+ // `acceptedMap.set(id, cap)`). The singleton seed
686
+ // `new Map([[id, cap]])` this replaced made every non-empty `requires`
687
+ // unsatisfiable (membership is checked against the map) and left cycles,
688
+ // tier-monotone and central config-key exclusivity vacuous at install.
689
+ // The seed builder lives in capability-loader so the overlay semantics
690
+ // have one owner and are clean by construction — pre-existing junk in the
691
+ // install scope is skipped, never attributed to the candidate. No swallow:
692
+ // if the loader cannot build the seed the install fails loudly — silently
693
+ // degrading to the singleton map would re-hide #3929.
694
+ /* eslint-disable @typescript-eslint/no-require-imports */
695
+ const seedLoader = require('./capability-loader.cjs');
696
+ /* eslint-enable @typescript-eslint/no-require-imports */
697
+ const { capMap, centralKeys } = seedLoader.crossValidationSeed(process.cwd(), gsdHome, hostVersion, capValidator, semverMod);
698
+ capMap.set(id, cap);
682
699
  const crossErrs = [
683
700
  ...capValidator.validateAgainstContract(cap, id),
684
701
  ...capValidator.validateConsumesGlobal(capMap),
@@ -1956,6 +1956,11 @@ const KNOWN_HOST_BEHAVIORS = new Set([
1956
1956
  'sourceMarkerFile',
1957
1957
  'tomlConfigInstall',
1958
1958
  'trackCategoryDescription',
1959
+ // #2586: declares runtime-level feature axes GSD does not/cannot support on
1960
+ // this host (e.g. Codex's `["context-warnings","phase-lifecycle-display"]`)
1961
+ // — present-and-populated / absent-is-unsupported-empty convention, so
1962
+ // omitting the key on every other runtime carries no inverted meaning.
1963
+ 'unsupportedFeatures',
1959
1964
  'verificationStyle',
1960
1965
  'writeCategoryDescription',
1961
1966
  ]);
@@ -2796,7 +2801,7 @@ function materializeHookFragments(cap, capDir) {
2796
2801
 
2797
2802
  const abs = path.resolve(capDir, fragment.path);
2798
2803
  const capRoot = path.resolve(capDir);
2799
- if (abs !== capRoot && !abs.startsWith(capRoot + path.sep)) {
2804
+ if (abs !== capRoot && !abs.startsWith(capRoot + path.sep)) { // allow-handrolled-containment: committed pre-build .cjs; compiled security.cjs is untracked build output
2800
2805
  errors.push(
2801
2806
  cap.id + '/' + groupName + '[' + i + '].fragment.path escapes capability directory: ' +
2802
2807
  fragment.path,
@@ -2948,6 +2953,14 @@ function validateStep(step, prefix, declaredSkills, declaredAgents) {
2948
2953
  errors.push(prefix + '.pointFrom must be a string if present');
2949
2954
  }
2950
2955
 
2956
+ // #4209 DISP-02: strict optional boolean opt-in trait. Absent or false is
2957
+ // inert; only a literal `true` reaches the projected active hook. Reject
2958
+ // every other type (including truthy non-boolean values) so a typo can
2959
+ // never silently opt a step into reviewer-lane dispatch.
2960
+ if (step.supportsReviewerLanes !== undefined && typeof step.supportsReviewerLanes !== 'boolean') {
2961
+ errors.push(prefix + '.supportsReviewerLanes must be a boolean if present');
2962
+ }
2963
+
2951
2964
  if (step.fragment !== undefined) {
2952
2965
  errors.push(...validateFragment(step.fragment, prefix + '.fragment'));
2953
2966
  }
@@ -14,7 +14,13 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_child_process_1 = require("node:child_process");
15
15
  // eslint-disable-next-line @typescript-eslint/no-require-imports
16
16
  const io = require("./io.cjs");
17
- const { output, error, ERROR_REASON } = io;
17
+ const { output, ERROR_REASON } = io;
18
+ // Explicitly annotated so TypeScript applies never-return control-flow narrowing.
19
+ // A destructured `const { error } = io` is a const WITHOUT a type annotation, and TS
20
+ // only narrows after a never-returning call when the callee is a function declaration
21
+ // or an annotated const. Without the annotation every `error(...)` guard below would
22
+ // need a dead `throw` after it to convince the checker that the value is non-null.
23
+ const error = io.error;
18
24
  // eslint-disable-next-line @typescript-eslint/no-require-imports
19
25
  const planningWorkspaceMod = require("./planning-workspace.cjs");
20
26
  const { planningDir } = planningWorkspaceMod;
@@ -89,7 +95,12 @@ function readIfExists(filePath) {
89
95
  }
90
96
  }
91
97
  function resolvePath(inputPath, projectDir) {
92
- return node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
98
+ const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
99
+ const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
100
+ if (contained === null) {
101
+ error(`path escapes its allowed directory: ${inputPath}`, ERROR_REASON.USAGE);
102
+ }
103
+ return contained;
93
104
  }
94
105
  function readWorkflowConfig(projectDir) {
95
106
  const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
@@ -272,9 +283,34 @@ function loadDecisionExtraction(contextPath) {
272
283
  outcome: extraction.outcome,
273
284
  };
274
285
  }
286
+ /**
287
+ * `check decision-coverage-plan` — blocking plan-phase decision-coverage gate
288
+ * (#2492, #1365 fail-loud, #2770 empty-arg fail-closed).
289
+ *
290
+ * Invocation (the context path may be supplied EITHER way; #4130 follow-up):
291
+ * gsd_run check decision-coverage-plan <phase-dir> <context-path> (positional, the workflow caller's form)
292
+ * gsd_run check decision-coverage-plan --context <path> [<phase-dir>]
293
+ *
294
+ * `--context <path>` follows the sibling flag convention (`check predicate`,
295
+ * #2008): `--flag value` pairs parsed by the shared partitionPredicateArgs
296
+ * pass, the flag WINNING over a same-purpose positional when both appear,
297
+ * and a valueless `--context` counting as no context at all (it falls
298
+ * through to the #2770 caller-error branch, not to the "CONTEXT.md missing"
299
+ * green skip). The positional form keeps working unchanged — no sibling
300
+ * check verb deprecates positionals and the plan-phase workflow passes them.
301
+ */
275
302
  function cmdDecisionCoveragePlan(projectDir, args, raw) {
276
- const phaseDir = args[2] ? resolvePath(args[2], projectDir) : '';
277
- const contextArg = args[3];
303
+ // args[0]='check', args[1]=subcommand — partition the REST so flag tokens
304
+ // and their values never land in a positional slot.
305
+ const { flags, positionals } = partitionPredicateArgs(args.slice(2));
306
+ const phaseDir = positionals[0] ? resolvePath(positionals[0], projectDir) : '';
307
+ // A VALUELESS `--context` stays a bare token in the positionals (sibling
308
+ // parser semantics); it must not then be read as the context PATH — a
309
+ // `--`-prefixed "path" is a caller mistake, and #2770's law says a missing
310
+ // context argument fails CLOSED, never a silent "CONTEXT.md missing" green
311
+ // skip. So only a non-flag positional may serve as the context.
312
+ const positionalContext = positionals[1] && !positionals[1].startsWith('--') ? positionals[1] : '';
313
+ const contextArg = flags['context'] ?? positionalContext ?? '';
278
314
  const contextPath = contextArg ? resolvePath(contextArg, projectDir) : '';
279
315
  if (!gateEnabled(projectDir)) {
280
316
  output({ passed: true, skipped: true, reason: 'workflow.context_coverage_gate is false', total: 0, covered: 0, uncovered: [], message: 'Decision coverage gate disabled by config.' }, raw, undefined);
@@ -309,12 +345,15 @@ function cmdDecisionCoveragePlan(projectDir, args, raw) {
309
345
  uncovered: [],
310
346
  message: partialParse
311
347
  ? 'Decision coverage gate: decisions could not be fully parsed — one or more ' +
312
- '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator). ' +
313
- 'Fix the bullet format so all D-NN decisions can be read before re-running the gate.'
348
+ '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
349
+ 'prefix that is not a digit run, e.g. `D4x-01`). Fix the bullet format so all decisions ' +
350
+ 'can be read before re-running the gate.'
314
351
  : 'Decision coverage gate: could not parse decisions — possible format mismatch. ' +
315
352
  'The CONTEXT.md appears to be decision-shaped (has a <decisions> block, a decisions heading, ' +
316
- 'or D- tokens) but no D-NN bullets could be extracted. Check the formatting of the decisions ' +
317
- 'block and ensure bullets follow the `- **D-NN:** text` or `- **D-NN — title** body` form.',
353
+ 'or D- tokens) but no decision bullets could be extracted. Check the formatting of the decisions ' +
354
+ 'block and ensure bullets follow the `- **D-NN:** text`, `- **D4-NN:** text` (phase-prefixed), ' +
355
+ 'or `- **D-NN — title** body` form. An ID grammar the parser does not support (e.g. `DEC-01`) ' +
356
+ 'also lands here.',
318
357
  }, raw, undefined);
319
358
  return;
320
359
  }
@@ -354,11 +393,6 @@ function recentCommitMessages(projectDir) {
354
393
  return '';
355
394
  }
356
395
  }
357
- function isInsideRoot(candidatePath, rootDir) {
358
- const root = node_path_1.default.resolve(rootDir);
359
- const target = node_path_1.default.resolve(root, candidatePath);
360
- return target === root || target.startsWith(`${root}${node_path_1.default.sep}`);
361
- }
362
396
  function readModifiedFilesContent(projectDir, summaries) {
363
397
  const out = [];
364
398
  let total = 0;
@@ -371,9 +405,17 @@ function readModifiedFilesContent(projectDir, summaries) {
371
405
  for (const file of files) {
372
406
  if (total >= 50)
373
407
  break;
374
- if (!file || !isInsideRoot(file, projectDir))
408
+ if (!file)
409
+ continue;
410
+ // Migrated off the hand-rolled prefix check (ADR-4650): resolve+contain in one
411
+ // step via the canonical realpath predicate — the eventual read below follows
412
+ // symlinks, so containment must be decided on the resolved target, not a lexical
413
+ // prefix. Read the value the predicate RETURNED; do not re-derive the path.
414
+ const candidate = node_path_1.default.isAbsolute(file) ? file : node_path_1.default.join(projectDir, file);
415
+ const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
416
+ if (contained === null)
375
417
  continue;
376
- const raw = readIfExists(resolvePath(file, projectDir));
418
+ const raw = readIfExists(contained);
377
419
  out.push(raw.length > 256 * 1024 ? raw.slice(0, 256 * 1024) : raw);
378
420
  total++;
379
421
  }
@@ -411,9 +453,11 @@ function cmdDecisionCoverageVerify(projectDir, args, raw) {
411
453
  not_honored: [],
412
454
  message: partialParse
413
455
  ? 'Decision coverage verify (warning): decisions could not be fully parsed — one or more ' +
414
- '`- **D-NN ...**` bullets appear malformed. Fix the bullet format in the CONTEXT.md decisions block.'
456
+ '`- **D-NN ...**` bullets appear malformed (missing `:` or ` — ` separator, or a phase ' +
457
+ 'prefix that is not a digit run). Fix the bullet format in the CONTEXT.md decisions block.'
415
458
  : 'Decision coverage verify (warning): could not parse decisions — possible format mismatch. ' +
416
- 'Check the formatting of the CONTEXT.md decisions block.',
459
+ 'Check the formatting of the CONTEXT.md decisions block (accepted forms: `- **D-NN:** text`, ' +
460
+ '`- **D4-NN:** text` (phase-prefixed), `- **D-NN — title** body`).',
417
461
  }, raw, undefined);
418
462
  return;
419
463
  }
@@ -1019,8 +1063,9 @@ function cmdGapAnalysisPlanPost(projectDir, args, raw) {
1019
1063
  error('gap-analysis.plan-post requires a phase-dir argument: check gap-analysis.plan-post <phase-dir> [phase-req-ids]', ERROR_REASON.SDK_MISSING_ARG);
1020
1064
  return;
1021
1065
  }
1066
+ const resolvedPhaseDir = resolvePath(phaseDir, projectDir);
1022
1067
  const phaseReqIds = args[3] ?? undefined;
1023
- const result = runGapAnalysis(projectDir, phaseDir, { phaseReqIds });
1068
+ const result = runGapAnalysis(projectDir, resolvedPhaseDir, { phaseReqIds });
1024
1069
  // Uniform gate contract: block = false (gap-analysis is always advisory, never blocks).
1025
1070
  // `message` carries the human-readable gap analysis report so the dispatch's
1026
1071
  // advisory branch can surface it. --raw emits JSON (rawValue=undefined), not
@@ -1071,21 +1116,21 @@ function buildPredicateDeps() {
1071
1116
  node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
1072
1117
  return null;
1073
1118
  }
1074
- const directPath = (0, security_cjs_1.validatePath)(artifactSuffix, phaseDir);
1075
- if (directPath.safe && node_fs_1.default.existsSync(directPath.resolved) && node_fs_1.default.statSync(directPath.resolved).isFile()) {
1076
- return directPath.resolved;
1119
+ const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
1120
+ if (directContained !== null && node_fs_1.default.existsSync(directContained) && node_fs_1.default.statSync(directContained).isFile()) {
1121
+ return directContained;
1077
1122
  }
1078
- const planningPath = (0, security_cjs_1.validatePath)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
1079
- if (planningPath.safe && node_fs_1.default.existsSync(planningPath.resolved) && node_fs_1.default.statSync(planningPath.resolved).isFile()) {
1080
- return planningPath.resolved;
1123
+ const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
1124
+ if (planningContained !== null && node_fs_1.default.existsSync(planningContained) && node_fs_1.default.statSync(planningContained).isFile()) {
1125
+ return planningContained;
1081
1126
  }
1082
1127
  try {
1083
1128
  const files = node_fs_1.default.readdirSync(phaseDir);
1084
1129
  for (const f of files) {
1085
1130
  if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
1086
- const candidate = (0, security_cjs_1.validatePath)(f, phaseDir);
1087
- if (candidate.safe && node_fs_1.default.statSync(candidate.resolved).isFile())
1088
- return candidate.resolved;
1131
+ const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
1132
+ if (candidateContained !== null && node_fs_1.default.statSync(candidateContained).isFile())
1133
+ return candidateContained;
1089
1134
  }
1090
1135
  }
1091
1136
  }
@@ -1101,23 +1146,42 @@ function buildPredicateDeps() {
1101
1146
  }
1102
1147
  };
1103
1148
  }
1104
- /** Parse `--flag value` pairs from an args array into a map (last write wins). */
1105
- function parsePredicateFlags(args) {
1106
- const out = {};
1149
+ /**
1150
+ * Split an args array into `--flag value` pairs and the leftover positional
1151
+ * tokens, in ONE pass, with the semantics `check predicate` established
1152
+ * (#2008): a `--flag` followed by a non-`--` token consumes it as the value
1153
+ * (last write wins); a `--flag` with no value stays a bare token and moves to
1154
+ * the positionals; everything else is positional. `parsePredicateFlags` is
1155
+ * the flags half of this same pass — there is exactly one parser, so the
1156
+ * flag-taking check verbs cannot drift apart (#4130 follow-up: `check
1157
+ * decision-coverage-plan --context <path>` shares it).
1158
+ */
1159
+ function partitionPredicateArgs(args) {
1160
+ const flags = {};
1161
+ const positionals = [];
1107
1162
  for (let i = 0; i < args.length; i++) {
1108
1163
  const a = args[i];
1109
1164
  if (typeof a !== 'string')
1110
1165
  continue;
1111
- if (!a.startsWith('--'))
1166
+ if (!a.startsWith('--')) {
1167
+ positionals.push(a);
1112
1168
  continue;
1169
+ }
1113
1170
  const key = a.slice(2);
1114
1171
  const next = args[i + 1];
1115
1172
  if (key.length > 0 && typeof next === 'string' && !next.startsWith('--')) {
1116
- out[key] = next;
1173
+ flags[key] = next;
1117
1174
  i++;
1118
1175
  }
1176
+ else {
1177
+ positionals.push(a);
1178
+ }
1119
1179
  }
1120
- return out;
1180
+ return { flags, positionals };
1181
+ }
1182
+ /** Parse `--flag value` pairs from an args array into a map (last write wins). */
1183
+ function parsePredicateFlags(args) {
1184
+ return partitionPredicateArgs(args).flags;
1121
1185
  }
1122
1186
  /**
1123
1187
  * `check predicate` — generic evaluator for capability gate `check.predicate`
@@ -1152,10 +1216,15 @@ function cmdCheckPredicate(projectDir, args, raw) {
1152
1216
  error('predicate --predicate value must be valid JSON', ERROR_REASON.USAGE);
1153
1217
  return;
1154
1218
  }
1219
+ const rawPhaseDir = flags['phase-dir'];
1220
+ let resolvedPhaseDir = rawPhaseDir;
1221
+ if (typeof rawPhaseDir === 'string' && rawPhaseDir !== '') {
1222
+ resolvedPhaseDir = resolvePath(rawPhaseDir, projectDir);
1223
+ }
1155
1224
  const ctx = {
1156
1225
  cwd: projectDir,
1157
1226
  phaseNumber: flags['phase-number'],
1158
- phaseDir: flags['phase-dir'],
1227
+ phaseDir: resolvedPhaseDir,
1159
1228
  phaseReqIds: flags['phase-req-ids'],
1160
1229
  };
1161
1230
  let result;
@@ -1249,7 +1318,12 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
1249
1318
  // Defense-in-depth: the resolved dir must be inside the phases root (or a
1250
1319
  // milestone archive under .planning/milestones).
1251
1320
  const milestonesRoot = node_path_1.default.join(pDir, 'milestones');
1252
- if (!isInsideRoot(resolvedDir, phasesRoot) && !isInsideRoot(resolvedDir, milestonesRoot)) {
1321
+ // Lexical containment (ADR-4650): resolvedDir is a directory path, not read
1322
+ // through here — mirrors the prior path.resolve(root, candidate)-based check
1323
+ // without introducing a filesystem/realpath dependency this defense-in-depth
1324
+ // recheck never had.
1325
+ if ((0, security_cjs_1.tryWithinRootLexical)(resolvedDir, phasesRoot) === null &&
1326
+ (0, security_cjs_1.tryWithinRootLexical)(resolvedDir, milestonesRoot) === null) {
1253
1327
  output({
1254
1328
  block: true,
1255
1329
  passed: false,
@@ -1578,7 +1652,9 @@ function routeCheckCommand({ args, cwd, raw }) {
1578
1652
  // this for any gate whose `check` carries a `predicate` (instead of a `query`),
1579
1653
  // passing the predicate object as --predicate '<json>'. NOTE: unlike the
1580
1654
  // `check.query` subcommands above (which take positional phase args), this
1581
- // subcommand parses --flag value pairs.
1655
+ // subcommand is flag-driven. `decision-coverage-plan` above now ALSO accepts
1656
+ // `--context <path>` (its positionals still work) — both share
1657
+ // partitionPredicateArgs, the one flag parser.
1582
1658
  cmdCheckPredicate(cwd, args, raw);
1583
1659
  return;
1584
1660
  }
@@ -1606,6 +1682,7 @@ module.exports = {
1606
1682
  cmdCheckPredicate,
1607
1683
  buildPredicateDeps,
1608
1684
  parsePredicateFlags,
1685
+ partitionPredicateArgs,
1609
1686
  // Fail-closed phase-scope reader for the api-coverage gate — exported for
1610
1687
  // in-process failure-injection tests (#2365 review).
1611
1688
  readPhaseScope,
@@ -61,7 +61,7 @@ function normalizeRelPath(p, repoRoot) {
61
61
  let relativized = false;
62
62
  if (typeof repoRoot === 'string' && repoRoot !== '') {
63
63
  const rootNormalized = repoRoot.trim().replace(/\\/g, '/').replace(/\/+$/, '');
64
- if (rootNormalized !== '' && value.startsWith(`${rootNormalized}/`)) {
64
+ if (rootNormalized !== '' && value.startsWith(`${rootNormalized}/`)) { // allow-handrolled-containment: display-path normalization — strips a caller-declared repoRoot prefix so a path renders repo-relative, not a security root-confinement decision
65
65
  value = value.slice(rootNormalized.length + 1);
66
66
  relativized = true;
67
67
  }
@@ -86,7 +86,7 @@ function normalizeRelPath(p, repoRoot) {
86
86
  * with `rulePath + '/'`. Both arguments must already be normalized. Case-sensitive.
87
87
  */
88
88
  function ruleMatchesFile(rulePath, filePath) {
89
- return filePath === rulePath || filePath.startsWith(`${rulePath}/`);
89
+ return filePath === rulePath || filePath.startsWith(`${rulePath}/`); // allow-handrolled-containment: rule-to-file segment match for selecting which review-depth rule applies — not a filesystem root-confinement gate
90
90
  }
91
91
  /**
92
92
  * Validate + normalize a single rule path (not yet matched against files).