@opengsd/gsd-core 1.18.0 → 1.19.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 +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-code-fixer.compact.md +2 -2
  5. package/agents/gsd-code-fixer.md +4 -3
  6. package/agents/gsd-verifier.md +1 -1
  7. package/bin/install.js +422 -103
  8. package/gsd-core/bin/gsd-tools.cjs +106 -7
  9. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  10. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  11. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  12. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  13. package/gsd-core/bin/lib/capability-registry.cjs +156 -74
  14. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  15. package/gsd-core/bin/lib/capability-validator.cjs +17 -0
  16. package/gsd-core/bin/lib/config-loader.cjs +10 -0
  17. package/gsd-core/bin/lib/config.cjs +256 -58
  18. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  19. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  20. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  21. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  22. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  23. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  24. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  25. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  26. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  27. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  28. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  29. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  30. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  31. package/gsd-core/bin/lib/init.cjs +65 -1
  32. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  33. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  34. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  35. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  36. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  37. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  38. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  39. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  40. package/gsd-core/bin/lib/milestone.cjs +43 -1
  41. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  42. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  43. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  44. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  45. package/gsd-core/bin/lib/phase.cjs +115 -26
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  47. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  48. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  49. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  51. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  52. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  53. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  54. package/gsd-core/bin/lib/roadmap.cjs +33 -25
  55. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  56. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  57. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  58. package/gsd-core/bin/lib/shell-command-projection.cjs +121 -16
  59. package/gsd-core/bin/lib/smart-entry.cjs +13 -0
  60. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  61. package/gsd-core/bin/lib/state-document.cjs +12 -4
  62. package/gsd-core/bin/lib/state-transition.cjs +119 -24
  63. package/gsd-core/bin/lib/state.cjs +50 -28
  64. package/gsd-core/bin/lib/surface.cjs +9 -0
  65. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  66. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  67. package/gsd-core/bin/lib/verification.cjs +96 -9
  68. package/gsd-core/bin/lib/verify.cjs +2 -0
  69. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  70. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  71. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  72. package/gsd-core/references/git-integration.md +11 -0
  73. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  74. package/gsd-core/references/planning-config.md +2 -0
  75. package/gsd-core/references/tdd.md +2 -2
  76. package/gsd-core/templates/DEBUG.md +16 -0
  77. package/gsd-core/templates/config.json +2 -1
  78. package/gsd-core/templates/verification-report.md +1 -1
  79. package/gsd-core/workflows/audit-fix.md +2 -1
  80. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  81. package/gsd-core/workflows/code-review.md +61 -7
  82. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  83. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  84. package/gsd-core/workflows/complete-milestone.md +37 -150
  85. package/gsd-core/workflows/diagnose-issues.md +29 -5
  86. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  87. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  88. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  89. package/gsd-core/workflows/discuss-phase.md +6 -10
  90. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  91. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  92. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  93. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  94. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  95. package/gsd-core/workflows/execute-phase.md +9 -5
  96. package/gsd-core/workflows/health.md +2 -1
  97. package/gsd-core/workflows/import.md +1 -1
  98. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  99. package/gsd-core/workflows/plan-phase.md +13 -13
  100. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  101. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  102. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  103. package/gsd-core/workflows/quick.md +1 -1
  104. package/gsd-core/workflows/review.md +16 -13
  105. package/gsd-core/workflows/section-manifest.json +5 -0
  106. package/gsd-core/workflows/settings.md +1 -0
  107. package/gsd-core/workflows/ship.md +3 -4
  108. package/gsd-core/workflows/spec-phase.md +3 -6
  109. package/gsd-core/workflows/verify-work.md +4 -3
  110. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  111. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  112. package/hooks/dist/gsd-path-export.js +95 -0
  113. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  114. package/hooks/dist/gsd-statusline.js +35 -2
  115. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  116. package/hooks/dist/gsd-workflow-guard.js +1 -1
  117. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  118. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  119. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  120. package/hooks/gsd-agent-isolation-guard.js +11 -1
  121. package/hooks/gsd-cursor-subagent-start.js +6 -2
  122. package/hooks/gsd-path-export.js +95 -0
  123. package/hooks/gsd-secret-read-guard.js +172 -39
  124. package/hooks/gsd-statusline.js +35 -2
  125. package/hooks/gsd-windsurf-pre-write.js +28 -1
  126. package/hooks/gsd-workflow-guard.js +1 -1
  127. package/hooks/gsd-worktree-path-guard.js +28 -1
  128. package/hooks/hooks.json +1 -0
  129. package/hooks/lib/isolation-sentinel.js +44 -0
  130. package/hooks/managed-hooks-registry.cjs +1 -0
  131. package/package.json +13 -6
  132. package/pi/gsd.cjs +1047 -10
  133. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  134. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  135. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  136. package/pi/package-agents/gsd-code-fixer.md +769 -0
  137. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  138. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  139. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  140. package/pi/package-agents/gsd-debugger.md +1274 -0
  141. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  142. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  143. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  144. package/pi/package-agents/gsd-doc-writer.md +626 -0
  145. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  146. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  147. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  148. package/pi/package-agents/gsd-eval-planner.md +149 -0
  149. package/pi/package-agents/gsd-executor.md +894 -0
  150. package/pi/package-agents/gsd-framework-selector.md +159 -0
  151. package/pi/package-agents/gsd-integration-checker.md +476 -0
  152. package/pi/package-agents/gsd-intel-updater.md +338 -0
  153. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  154. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  155. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  156. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  157. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  158. package/pi/package-agents/gsd-planner.md +1003 -0
  159. package/pi/package-agents/gsd-project-researcher.md +611 -0
  160. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  161. package/pi/package-agents/gsd-roadmapper.md +779 -0
  162. package/pi/package-agents/gsd-security-auditor.md +171 -0
  163. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  164. package/pi/package-agents/gsd-ui-checker.md +417 -0
  165. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  166. package/pi/package-agents/gsd-user-profiler.md +174 -0
  167. package/pi/package-agents/gsd-verifier.md +963 -0
  168. package/pi/package-agents/manifest.json +39 -0
  169. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  170. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  171. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  172. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  173. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  174. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  175. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  176. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  177. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  178. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  179. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  180. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  181. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  182. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  183. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  184. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  185. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  186. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  187. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  188. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  189. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  190. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  191. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  192. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  193. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  194. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  195. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  196. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  197. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  198. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  199. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  200. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  201. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  202. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  203. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  204. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  205. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  206. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  207. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  208. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  209. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  210. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  211. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  212. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  213. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  214. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  215. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  216. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  217. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  218. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  219. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  220. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  221. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  222. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  223. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  224. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  225. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  226. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  227. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  228. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  229. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  230. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  231. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  232. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  233. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  234. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  235. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  236. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  237. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  238. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  239. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  240. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  241. package/scripts/build-hooks.js +4 -0
  242. package/scripts/command-contract-helpers.cjs +1 -1
  243. package/scripts/docs-guard-registry.cjs +20 -7
  244. package/scripts/gen-health-docs.cjs +13 -1
  245. package/scripts/gen-pi-package-skills.cjs +217 -0
  246. package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
  247. package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
  248. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  249. package/scripts/lint-retired-runtime-name.cjs +5 -0
  250. package/scripts/npm-audit-baseline.cjs +41 -7
  251. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  252. package/scripts/select-docs-guards.cjs +1 -1
  253. package/scripts/self-test-report-policy.cjs +350 -0
  254. package/vscode/package.json +1 -1
  255. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  256. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  257. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -43,6 +43,8 @@ const node_child_process_1 = __importDefault(require("node:child_process"));
43
43
  // eslint-disable-next-line @typescript-eslint/no-require-imports
44
44
  const ioMod = require("./io.cjs");
45
45
  const { formatDiagnosticToken } = ioMod;
46
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
47
+ const capabilityLoaderMod = require("./capability-loader.cjs");
46
48
  // ─── Result taxonomy ──────────────────────────────────────────────────────────
47
49
  /**
48
50
  * The four non-throwing outcomes of `resolveTaskContent`. Frozen because the
@@ -102,6 +104,17 @@ class ResolverFailedError extends Error {
102
104
  this.stderrTail = stderrTail;
103
105
  }
104
106
  }
107
+ /**
108
+ * The tracker-id's id segment cannot safely be one argv element: a leading `-`
109
+ * reads as an option to the resolver binary, and control characters are never
110
+ * legitimate. Thrown before any spawn.
111
+ */
112
+ class ResolverUnsafeIdError extends Error {
113
+ constructor() {
114
+ super('tracker-id rejected: id must not start with "-" or contain control characters');
115
+ this.name = 'ResolverUnsafeIdError';
116
+ }
117
+ }
105
118
  /** The resolver subprocess exceeded its declared `invoke.timeoutMs` bound. */
106
119
  class ResolverTimeoutError extends Error {
107
120
  timeoutMs;
@@ -312,6 +325,9 @@ function resolveTaskContent(input) {
312
325
  const split = splitTrackerId(input.trackerId);
313
326
  if (split === null)
314
327
  return { kind: TASK_CONTENT_RESULT.NOT_APPLICABLE };
328
+ if (split.id.trimStart().startsWith('-') || /[\u0000-\u001f\u007f]/.test(input.trackerId ?? '')) {
329
+ throw new ResolverUnsafeIdError();
330
+ }
315
331
  const resolver = findResolver(split.prefix, input.capabilities ?? []);
316
332
  if (resolver === null)
317
333
  return { kind: TASK_CONTENT_RESULT.NO_RESOLVER };
@@ -327,7 +343,7 @@ function resolveTaskContent(input) {
327
343
  throw new ResolverAmbiguousError(split.prefix, ids);
328
344
  }
329
345
  const invocation = buildInvocation(resolver, split.id);
330
- const timeoutMs = input.timeoutOverrideMs ?? invocation.timeoutMs;
346
+ const timeoutMs = Math.floor(Math.min(input.timeoutOverrideMs ?? invocation.timeoutMs, invocation.timeoutMs));
331
347
  const execFn = input.execFn ?? realExec;
332
348
  const result = execFn(invocation.binary, invocation.args, { timeout: timeoutMs });
333
349
  if (isExecTimeout(result)) {
@@ -354,12 +370,41 @@ function resolveTaskContent(input) {
354
370
  }
355
371
  return { kind: TASK_CONTENT_RESULT.RESOLVED, content };
356
372
  }
373
+ /**
374
+ * Production capability loader for tracker-id resolution: the merged
375
+ * first-party + validated-installed-overlay registry (ADR-1244 D2), the
376
+ * established runtime read path for "installed capabilities including
377
+ * third-party" — as opposed to `capability-loader.cts`'s heavier build-time
378
+ * validation entry points or the static `capability-registry.cjs` alone
379
+ * (first-party only, would miss a third-party capability's
380
+ * `taskContentResolver` declaration entirely).
381
+ */
382
+ function loadResolverCapabilities(cwd) {
383
+ const registry = capabilityLoaderMod.loadRegistry({ includeInstalled: true, cwd });
384
+ return Object.values(registry.capabilities ?? {});
385
+ }
386
+ /** True for every error `resolveTaskContent` throws as a hard halt (the one classifier all consumers share). */
387
+ function isResolverFailure(err) {
388
+ return (err instanceof ResolverAmbiguousError ||
389
+ err instanceof ResolverFailedError ||
390
+ err instanceof ResolverTimeoutError ||
391
+ err instanceof ResolverMalformedOutputError ||
392
+ err instanceof ResolverUnsafeIdError);
393
+ }
394
+ /** Every text field of resolved content, so a consumer scanning for text cannot miss one when the shape grows. */
395
+ function contentTexts(c) {
396
+ return [c.action, c.verify, ...c.acceptanceCriteria, ...c.readFirst, c.done].filter((v) => v !== null);
397
+ }
357
398
  const taskContentResolution = {
358
399
  TASK_CONTENT_RESULT,
359
400
  splitTrackerId,
360
401
  findResolver,
361
402
  buildInvocation,
362
403
  resolveTaskContent,
404
+ loadResolverCapabilities,
405
+ isResolverFailure,
406
+ contentTexts,
407
+ ResolverUnsafeIdError,
363
408
  ResolverAmbiguousError,
364
409
  ResolverFailedError,
365
410
  ResolverTimeoutError,
@@ -47,6 +47,8 @@ const coreUtilsMod = require("./core-utils.cjs");
47
47
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-scope.cjs is an export= CommonJS module
48
48
  const planningScopeMod = require("./planning-scope.cjs");
49
49
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
50
+ const text_lines_cjs_1 = require("./text-lines.cjs");
51
+ const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
50
52
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
51
53
  const security_cjs_1 = require("./security.cjs");
52
54
  const pattern_cjs_1 = require("./pattern.cjs");
@@ -607,17 +609,19 @@ function phaseArtifactPaths(phaseDir, projectRoot) {
607
609
  * `.planning/<project>/workstreams/<ws>`), covering a
608
610
  * per-project/per-workstream store symlink (amendment 1,
609
611
  * revised R7).
612
+ * v4 (#5351) — v3 with each CRLF byte pair hashed as LF. Every other
613
+ * byte, including a lone CR and non-UTF-8 input, is preserved.
610
614
  *
611
615
  * A stored digest names its own version (`v<N>:sha256:…`), and
612
616
  * `readVerificationStatus` recomputes under the STORED version rather than
613
617
  * this constant — so bumping it does not flip every already-verified phase
614
- * to `stale` on upgrade. A legacy v1/v2 report keeps its own semantics
615
- * (shared documents included for v1; no report filter or artifact union for
616
- * either) until it is re-fingerprinted; only a version outside
618
+ * to `stale` on upgrade. A legacy v1/v2/v3 report keeps its own raw-byte
619
+ * semantics (shared documents included for v1; no report filter or artifact
620
+ * union for v1/v2) until it is re-fingerprinted; only a version outside
617
621
  * `KNOWN_FINGERPRINT_VERSIONS` is unrecomputable and fails closed.
618
622
  */
619
- const FINGERPRINT_VERSION = 3;
620
- const KNOWN_FINGERPRINT_VERSIONS = new Set([1, 2, 3]);
623
+ const FINGERPRINT_VERSION = 4;
624
+ const KNOWN_FINGERPRINT_VERSIONS = new Set([1, 2, 3, 4]);
621
625
  /**
622
626
  * #4623: the planning roots whose DIRECT children are repo-wide planning
623
627
  * documents, as project-root-relative posix paths. Always `.planning`; plus
@@ -815,7 +819,22 @@ function deriveCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_VE
815
819
  catch {
816
820
  return { ok: true, digest: null, files: declared, mappedPhaseDir: artifactMappedPhaseDir };
817
821
  }
818
- const fileHash = node_crypto_1.default.createHash('sha256').update(bytes).digest('hex');
822
+ const hash = node_crypto_1.default.createHash('sha256');
823
+ if (version >= 4) {
824
+ // #5351: drop only the CR in each CRLF pair without decoding the
825
+ // covered input — binary files and lone CR bytes keep their meaning.
826
+ // Hash Buffer slices directly, retaining the LF at each new start.
827
+ let start = 0;
828
+ for (let crlf = bytes.indexOf('\r\n'); crlf !== -1; crlf = bytes.indexOf('\r\n', start)) {
829
+ hash.update(bytes.subarray(start, crlf));
830
+ start = crlf + 1;
831
+ }
832
+ hash.update(bytes.subarray(start));
833
+ }
834
+ else {
835
+ hash.update(bytes);
836
+ }
837
+ const fileHash = hash.digest('hex');
819
838
  parts.push(`${rel}\n${fileHash}\n`);
820
839
  hashed++;
821
840
  }
@@ -1175,6 +1194,72 @@ function findVerificationStatusError(phaseDirs, deps = {}) {
1175
1194
  }
1176
1195
  return null;
1177
1196
  }
1197
+ /**
1198
+ * #5291: legacy report statuses `validate health --repair` may rewrite, and the
1199
+ * closed-set status each becomes. Deliberately minimal — only the value the
1200
+ * maintainer ruled on (`pass_with_caveats` -> `passed`). Every other status
1201
+ * outside the closed set (`pending`, `partial`, …) has NO mapping: it stays
1202
+ * refused and its W030 stays advise-only (fail closed; only `passed` satisfies
1203
+ * the gate). The original value is preserved in `legacy_status`.
1204
+ */
1205
+ const LEGACY_STATUS_REPAIRS = Object.freeze({
1206
+ pass_with_caveats: VERIFICATION_STATUS.PASSED,
1207
+ });
1208
+ /** The closed-set status a known legacy `raw` status repairs to, or `null` when it has no mapping. */
1209
+ function legacyStatusRepairTarget(raw) {
1210
+ return typeof raw === 'string' && Object.prototype.hasOwnProperty.call(LEGACY_STATUS_REPAIRS, raw)
1211
+ ? LEGACY_STATUS_REPAIRS[raw]
1212
+ : null;
1213
+ }
1214
+ /**
1215
+ * #5291: rewrite one report's frontmatter `status:` from a known legacy value
1216
+ * to its closed-set equivalent and record the original as `legacy_status:`
1217
+ * right after it. Touches only those lines — every other byte (body, other
1218
+ * keys, line endings) is preserved — and never overwrites an existing
1219
+ * `legacy_status`. Refuses (writes nothing) for a path that is not a report
1220
+ * contained in its own phase directory, a report with no frontmatter `status`,
1221
+ * and any status without a mapping.
1222
+ */
1223
+ function repairLegacyVerificationStatus(filePath) {
1224
+ if (!isReportContained(node_path_1.default.dirname(filePath), filePath)) {
1225
+ return { ok: false, reason: 'the report is not contained in its phase directory' };
1226
+ }
1227
+ let content;
1228
+ try {
1229
+ content = node_fs_1.default.readFileSync(filePath, 'utf-8');
1230
+ }
1231
+ catch (err) {
1232
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
1233
+ }
1234
+ // The fence owner (`locateFrontmatterFence`) is the one place that knows where
1235
+ // the frontmatter body starts and ends (BOM, CRLF, lenient closing fence).
1236
+ const fence = (0, frontmatter_fence_cjs_1.locateFrontmatterFence)(content);
1237
+ if (!fence || !fence.closed)
1238
+ return { ok: false, reason: 'the report has no closed frontmatter' };
1239
+ const lines = (0, text_lines_cjs_1.splitLines)(content.slice(fence.openEnd, fence.bodyEnd));
1240
+ // The YAML reader keeps the LAST of duplicate keys, so the line the repair
1241
+ // edits is the last `status:` line — the very value the refusal named. A
1242
+ // trailing comment (`status: pass_with_caveats # x`) is not part of the value.
1243
+ let statusAt = -1;
1244
+ lines.forEach((l, i) => { if (/^status:/.test(l))
1245
+ statusAt = i; });
1246
+ if (statusAt === -1)
1247
+ return { ok: false, reason: 'the report frontmatter has no status' };
1248
+ const raw = lines[statusAt]
1249
+ .replace(/^status:\s*/, '')
1250
+ .replace(/\s+#.*$/, '')
1251
+ .replace(/\s+$/, '')
1252
+ .replace(/^(["'])(.*)\1$/, '$2');
1253
+ const to = legacyStatusRepairTarget(raw);
1254
+ if (to === null)
1255
+ return { ok: false, reason: `${describeRawStatus(raw)} has no known repair` };
1256
+ const legacyStatusKept = lines.some((l) => /^legacy_status:/.test(l));
1257
+ lines[statusAt] = `status: ${to}`;
1258
+ if (!legacyStatusKept)
1259
+ lines.splice(statusAt + 1, 0, `legacy_status: ${raw}`);
1260
+ (0, shell_command_projection_cjs_1.platformWriteSync)(filePath, content.slice(0, fence.openEnd) + (0, text_lines_cjs_1.joinLines)(lines, fence.eol) + content.slice(fence.bodyEnd), { verbatim: true });
1261
+ return { ok: true, from: raw, to, legacyStatusKept };
1262
+ }
1178
1263
  /**
1179
1264
  * #3518: the shared phase-pinned artifact-selection core BOTH single-pick
1180
1265
  * resolvers (`resolveVerificationFile` for `*-VERIFICATION.md`,
@@ -1499,11 +1584,11 @@ function readVerificationStatus(phaseDir, opts = {}) {
1499
1584
  isStale = true;
1500
1585
  }
1501
1586
  else if (storedVersion >= 3) {
1502
- // #5095 (R1): v3's digest is computed over the declared set UNIONED
1587
+ // #5095 (R1): the v3+ digest is computed over the declared set UNIONED
1503
1588
  // with the phase's own live artifacts — a plan/summary added after
1504
1589
  // fingerprinting already moves the digest itself, so the separate
1505
1590
  // live-directory re-scan (`uncoveredCurrentArtifacts`) is redundant
1506
- // for v3 and is not run.
1591
+ // for v3+ and is not run.
1507
1592
  isStale =
1508
1593
  computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal;
1509
1594
  if (isStale) {
@@ -1784,7 +1869,7 @@ function parseFingerprintFileArgs(tokens) {
1784
1869
  * directly assignable. #5095 (ADR-5057 Phase 2): `raw`
1785
1870
  * returning only the digest is safe even though the
1786
1871
  * emitted `covered_files` is a SUPERSET of the caller's
1787
- * declared list — the v3 digest is computed over
1872
+ * declared list — the v3+ digest is computed over
1788
1873
  * `canonicalize(declared ∪ phaseArtifactPaths)` on BOTH
1789
1874
  * the emit side (here) and the check side
1790
1875
  * (`readVerificationStatus`), so a `--raw` caller that
@@ -2134,6 +2219,8 @@ const verificationModule = {
2134
2219
  isReportContained,
2135
2220
  routeResult,
2136
2221
  findVerificationStatusError,
2222
+ legacyStatusRepairTarget,
2223
+ repairLegacyVerificationStatus,
2137
2224
  defaultPhaseCleanCommitTimesMs,
2138
2225
  resolvePhaseArtifactFile,
2139
2226
  resolveVerificationFile,
@@ -1809,6 +1809,8 @@ function repairFixText(slash, action) {
1809
1809
  return `Run ${slash('health')} --repair to add key`;
1810
1810
  case REMEDY_ACTION.BACKFILL_MILESTONES:
1811
1811
  return `Run ${slash('health')} --backfill to synthesize missing entries from archive snapshots`;
1812
+ case REMEDY_ACTION.REPAIR_VERIFICATION_STATUS:
1813
+ return `Run ${slash('health')} --repair to rewrite the legacy status (original kept as legacy_status)`;
1812
1814
  default:
1813
1815
  return '';
1814
1816
  }
@@ -176,7 +176,28 @@ function resolveWorktreeContext(cwd, deps = {}) {
176
176
  reason: 'has_local_planning',
177
177
  };
178
178
  }
179
- return resolveWorktreeLinkage(cwd, deps);
179
+ const linkage = resolveWorktreeLinkage(cwd, deps);
180
+ if (linkage.reason !== 'linked_worktree')
181
+ return linkage;
182
+ // #4885: from a SUBDIRECTORY of a LINKED worktree the cwd has no .planning of its own, but the
183
+ // worktree's own top-level may. That checkout is the project the caller means; remapping to the
184
+ // main checkout would make `commit` and every planning write land in the wrong repository. A linked
185
+ // worktree with no .planning anywhere still remaps to the main checkout, and the main checkout's own
186
+ // subdirectories are untouched.
187
+ const execGit = deps.execGit || execGitDefault;
188
+ const top = execGit(['rev-parse', '--show-toplevel'], { cwd });
189
+ const topText = String(top.stdout || '').trim();
190
+ if (!top.timedOut && top.exitCode === 0 && topText !== '') {
191
+ const topLevel = node_path_1.default.resolve(cwd, topText);
192
+ if (existsSync(node_path_1.default.join(topLevel, '.planning'))) {
193
+ return {
194
+ effectiveRoot: topLevel,
195
+ mode: 'current_directory',
196
+ reason: 'has_local_planning',
197
+ };
198
+ }
199
+ }
200
+ return linkage;
180
201
  }
181
202
  function planWorktreePrune(repoRoot, options = {}, deps = {}) {
182
203
  const parsePorcelain = deps.parseWorktreePorcelain || parseWorktreePorcelain;
@@ -22,6 +22,7 @@
22
22
  "git": {
23
23
  "branching_strategy": "none",
24
24
  "create_tag": true,
25
+ "repo_tagging": "single",
25
26
  "base_branch": null,
26
27
  "phase_branch_template": "gsd/phase-{phase}-{slug}",
27
28
  "milestone_branch_template": "gsd/{milestone}-{slug}",
@@ -51,6 +51,7 @@
51
51
  "git.protected_branches",
52
52
  "git.allow_default_branch_commits",
53
53
  "git.create_tag",
54
+ "git.repo_tagging",
54
55
  "git.phase_branch_template",
55
56
  "git.milestone_branch_template",
56
57
  "git.quick_branch_template",
@@ -295,4 +295,15 @@ This stages `src/api/users.ts` and `src/types/user.ts` in the `backend/` repo, a
295
295
 
296
296
  Files that don't match any configured sub-repo are reported as unmatched.
297
297
 
298
+ ### Milestone Tagging Across Sub-Repos
299
+
300
+ A GSD milestone version names planned work in `.planning/`; it is not any repository's release version. With `git.repo_tagging: "per_repo"` and a non-empty `sub_repos`, `/gsd:complete-milestone` records one release map entry per repository (status, release, reference, environment, tag choice) instead of creating one `v[X.Y]` tag in the current directory:
301
+
302
+ 1. `gsd_run query milestone release-tags --map <tags.json> --dry-run` checks every requested tag (repository listed in `sub_repos`, ref resolves, tag name valid, no tag at a different commit) before anything is written.
303
+ 2. After one consolidated confirmation, `gsd_run query milestone release-tags --map <tags.json>` creates the annotated tags, each with the target repository as its working directory. Re-running the same map is idempotent (`already_tagged`).
304
+ 3. `milestone complete <version> --release-map <file>` archives the map by recorded value.
305
+ 4. Nothing is pushed by default. Each repository's tag has its own confirmation naming the repository and its remote.
306
+
307
+ With `git.create_tag: false` the map is still recorded and no tag is created. See [How to tag a multi-repository milestone](../../docs/how-to/tag-a-multi-repo-milestone.md).
308
+
298
309
  </sub_repos_support>
@@ -91,12 +91,19 @@ not one shape of one.
91
91
  capability manifest, which may be third-party — and `gates[].check` is **not** one of the
92
92
  executable surfaces the install consent prompt discloses (`hooks`, command modules,
93
93
  `mcpServers`, reviewer lanes), so a capability can be consented to as declarative-only and
94
- still reach a shell through a gate. Check the query value you read from `activeHooks` against
95
- `^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)*$` yourself, IN-CONTEXT — **never** by pasting it into a
96
- shell command to be tested there, because a value carrying a quote, `;`, a `` ` ``, `$(`, or a
97
- newline would terminate the assignment and run as its own statement before any shell-side
98
- check could execute. A value that fails is a malformed manifest: record a warning, route it
99
- per the gate's `onError`, and do not run it. Pass `check.predicate` as a **single argv
94
+ still reach a shell through a gate. Normalize first, then validate: the registry declares query
95
+ ids in dotted form (`ui.plan-gate`, `verify.codebase-drift`) and `gsd_run check` accepts them by
96
+ turning each `.` into `-`, so replace every `.` with `-` in the query value you read from
97
+ `activeHooks`, then check that normalized value against
98
+ `^[a-z][a-z0-9]*(-[a-z0-9]+)*( [a-z][a-z0-9]*(-[a-z0-9]+)*)*$` yourself, IN-CONTEXT — **never** by
99
+ pasting it into a shell command to be tested there, because a value carrying a quote, `;`, a
100
+ `` ` ``, `$(`, or a newline would terminate the assignment and run as its own statement before
101
+ any shell-side check could execute. The normalized value is the one value you validate and the
102
+ one value you pass: wherever a workflow shows `${hook.check.query}`, write the normalized value.
103
+ A leading dot, a trailing dot or consecutive dots become a leading, trailing or doubled hyphen,
104
+ which the pattern rejects. A value that fails — or that passes but names no known check, which
105
+ `gsd_run check` rejects as an unknown check subcommand — is a malformed manifest: record a
106
+ warning, route it per the gate's `onError`, and do not run it. Pass `check.predicate` as a **single argv
100
107
  element** for the same reason — never re-quote it into a shell string, where an apostrophe
101
108
  would close the literal. This is the identical requirement `step` → `ref.command` carries
102
109
  above; it was stated there and omitted here, which is the gap #3559 closed.
@@ -35,6 +35,7 @@ Configuration options for `.planning/` directory behavior.
35
35
  | `git.base_branch` | `null` (auto-detect) | Target branch for PRs and merges (e.g. `"master"`, `"develop"`). When `null`, auto-detects from `git symbolic-ref refs/remotes/origin/HEAD`, falling back to `"main"`. |
36
36
  | `git.protected_branches` | (none) | Optional array of non-empty strings naming additional shared branches that should trigger protected-branch warnings |
37
37
  | `git.create_tag` | `true` | Create git tags on milestone completion |
38
+ | `git.repo_tagging` | `"single"` | Milestone tagging mode: `"single"` (existing behavior) or `"per_repo"` (record one release per configured repository; each tag is pushed only after a per-repository confirmation) |
38
39
  | `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy |
39
40
  | `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy |
40
41
  | `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
@@ -340,6 +341,7 @@ Set via `git.*` namespace (e.g., `"git": { "branching_strategy": "phase" }`).
340
341
  | `git.base_branch` | string\|null | `null` (auto-detect) | Any branch name | Target branch for PRs and merges; auto-detects from `origin/HEAD` when `null` |
341
342
  | `git.protected_branches` | array of non-empty strings | (none) | Non-empty branch names | Optional protected names added to the resolved base branch for execute-phase and ship warnings |
342
343
  | `git.create_tag` | boolean | `true` | `true`, `false` | Create git tags on milestone completion |
344
+ | `git.repo_tagging` | string | `"single"` | `"single"`, `"per_repo"` | Milestone tagging mode; `"per_repo"` opts a multi-repo project into per-repository release records |
343
345
  | `git.phase_branch_template` | string | `"gsd/phase-{phase}-{slug}"` | Template with `{phase}`, `{slug}` | Branch naming template for `phase` strategy |
344
346
  | `git.milestone_branch_template` | string | `"gsd/{milestone}-{slug}"` | Template with `{milestone}`, `{slug}` | Branch naming template for `milestone` strategy |
345
347
  | `git.quick_branch_template` | string\|null | `null` | Template with `{slug}` | Optional branch template for quick-task runs |
@@ -316,7 +316,7 @@ git log --oneline -E --grep="^feat\((0*${PHASE_N})-(0*${PLAN_N})\):" | head -1
316
316
  git log --oneline -E --grep="^refactor\((0*${PHASE_N})-(0*${PLAN_N})\):" | head -1
317
317
  ```
318
318
 
319
- If RED or GREEN gate commits are missing, add a `## TDD Gate Compliance` section to SUMMARY.md with the violation details.
319
+ If RED or GREEN gate commits are missing, or no GREEN commit follows a RED commit (#5275), add a `## TDD Gate Compliance` section to SUMMARY.md with the violation details.
320
320
  </gate_enforcement>
321
321
 
322
322
  <end_of_phase_review>
@@ -342,7 +342,7 @@ TDD Plans: {count} | Gate violations: {count}
342
342
 
343
343
  ### What the Review Checks
344
344
 
345
- 1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order. A plan is scored on its own `test(<plan>):` / `feat(<plan>):` commits, with one exception (#4867): a RED-only probe plan is satisfied by a `feat(...)` commit under a plan that declares it in `depends_on`, directly or through a chain. That is the gap-closure shape (probe plan, then the fix plan that depends on it). The plan's own RED is still required, a plan nothing depends on is scored alone, and the report names the plan that supplied the GREEN (`Plan {id} GREEN is supplied by dependent plan {id}.`).
345
+ 1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order (#5275). Order is commit ancestry, not log position or commit date: a GREEN counts only when it descends from a RED commit of the plan, so a history merged from worktree branches is judged by what each commit descends from. A plan with both commit kinds but no GREEN after a RED fails with `reason: green_before_red` and the line `Plan {id} GREEN precedes RED: no feat(...) commit descends from a test(...) commit.`, distinct from the `missing` row for a plan without a RED or GREEN commit. If git cannot answer the ancestry question, the GREEN keeps its credit rather than becoming a violation. A plan is scored on its own `test(<plan>):` / `feat(<plan>):` commits, with one exception (#4867): a RED-only probe plan is satisfied by a `feat(...)` commit under a plan that declares it in `depends_on`, directly or through a chain. That is the gap-closure shape (probe plan, then the fix plan that depends on it). The plan's own RED is still required, a plan nothing depends on is scored alone, and the report names the plan that supplied the GREEN (`Plan {id} GREEN is supplied by dependent plan {id}.`).
346
346
  2. **Test quality:** RED phase tests fail for the right reason (not import errors or syntax)
347
347
  3. **Minimal GREEN:** Implementation is minimal — no premature optimization in GREEN phase
348
348
  4. **Refactor discipline:** If REFACTOR commit exists, tests still pass
@@ -25,6 +25,17 @@ bug_class: null <!-- assigned at Phase 1.75 — bohrbug|heisenbug-mandelbug|con
25
25
  reasoning_checkpoint: null <!-- populated before every fix attempt — see structured_returns -->
26
26
  tdd_checkpoint: null <!-- populated when tdd_mode is active after root cause confirmed -->
27
27
 
28
+ ## Session Loop
29
+ <!-- Read and written only by the checkpoint loop that drives gsd-debugger (on Pi, the top-level session); gsd-debugger never edits it. OVERWRITE on each dispatch and each wait -->
30
+
31
+ goal: find_and_fix | find_root_cause_only
32
+ tdd_mode: false
33
+ tdd_phase: none | green
34
+ resume_count: 0
35
+ awaiting: none | [header of the return whose question is open]
36
+ question: none | [checkpoint type and the full question text shown to the user]
37
+ last_next_action: [next_action seen before the last re-dispatch]
38
+
28
39
  ## Symptoms
29
40
  <!-- Written during gathering, then immutable -->
30
41
 
@@ -78,6 +89,11 @@ files_changed: []
78
89
  - `reasoning_checkpoint`: OVERWRITE before every fix_and_verify — seven-field structured reasoning record (hypothesis, confirming_evidence, falsification_test, fix_rationale, blind_spots, candidate_causes, and_gate) — see `gsd-debugger.md` Structured Reasoning Checkpoint
79
90
  - `tdd_checkpoint`: OVERWRITE during TDD red/green phases — test file, name, status, failure output
80
91
 
92
+ **Session Loop:**
93
+ - Owned by the checkpoint loop that drives `gsd-debugger`; on Pi that loop is the top-level session, which holds no state in memory between turns (#5638)
94
+ - OVERWRITE on each dispatch and each wait; the debugger never edits it and `## Current Focus` rewrites never touch it
95
+ - Fields: goal, tdd_mode, tdd_phase, resume_count, awaiting, question, last_next_action
96
+
81
97
  **Symptoms:**
82
98
  - Written during initial gathering phase
83
99
  - IMMUTABLE after gathering complete
@@ -30,7 +30,8 @@
30
30
  "sub_repos": []
31
31
  },
32
32
  "git": {
33
- "create_tag": true
33
+ "create_tag": true,
34
+ "repo_tagging": "single"
34
35
  },
35
36
  "parallelization": {
36
37
  "enabled": true,
@@ -16,7 +16,7 @@ covered_files: # #4155 — see agents/gsd-verifier.md's "Create VERIFICATION.md"
16
16
  - .planning/phases/XX-name/{phase_num}-{plan}-PLAN.md
17
17
  - .planning/phases/XX-name/{phase_num}-{plan}-SUMMARY.md
18
18
  - src/{changed-file}.cts
19
- covered_digest: "v3:sha256:{digest from verification.fingerprint}"
19
+ covered_digest: "v4:sha256:{digest from verification.fingerprint}"
20
20
  behavior_unverified: 0 # Count of ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truths (present + wired, behavior not exercised)
21
21
  behavior_unverified_items: # Only if behavior_unverified > 0 — the truths above as structured items; emitted regardless of overall status
22
22
  - truth: "Observable truth whose state transition or cancellation/cleanup/ordering invariant no test exercises"
@@ -128,7 +128,8 @@ if [ -z "$AUDIT_TEST_CMD" ]; then
128
128
  elif [ -f "Justfile" ] || [ -f "justfile" ]; then
129
129
  AUDIT_TEST_CMD="just test"
130
130
  elif [ -f "package.json" ]; then
131
- AUDIT_TEST_CMD="npm test"
131
+ AUDIT_TEST_CMD=$(gsd_run query resolve-package-manager test --cwd . 2>/dev/null || echo "npm test")
132
+ [ -n "$AUDIT_TEST_CMD" ] || AUDIT_TEST_CMD="npm test"
132
133
  elif [ -f "Cargo.toml" ]; then
133
134
  AUDIT_TEST_CMD="cargo test"
134
135
  elif [ -f "go.mod" ]; then
@@ -2,8 +2,17 @@ When `FALLOW_ENABLED=true`:
2
2
 
3
3
  1) Resolve binary via `node_modules/.bin/fallow` first, then PATH.
4
4
  ```bash
5
+ # gsd-resolve-lib begin fallow-runner.cjs (#5640): load helpers from the installed runtime, not the cwd
6
+ _gsd_lib_probe="fallow-runner.cjs"
7
+ _gsd_rt="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
8
+ GSD_LIB_DIR=""
9
+ _gsd_pick() { for _gsd_d; do if [ -n "$_gsd_d" ] && [ -f "$_gsd_d/$_gsd_lib_probe" ]; then GSD_LIB_DIR="$_gsd_d"; return 0; fi; done; return 1; }
10
+ _gsd_pick "${GSD_TOOLS:+${GSD_TOOLS%/*}/lib}" "$_gsd_rt/gsd-core/bin/lib" "$_gsd_rt/.claude/gsd-core/bin/lib" "$_gsd_rt/.codex/gsd-core/bin/lib" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib" || true
11
+ if [ -z "$GSD_LIB_DIR" ]; then echo "ERROR: fallow-runner.cjs not found in the GSD runtime. Looked in: ${GSD_TOOLS:+${GSD_TOOLS%/*}/lib, }${_gsd_rt}/gsd-core/bin/lib (and its .claude and .codex forms), ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib, ${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib. Reinstall: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
12
+ export GSD_LIB_DIR
13
+ # gsd-resolve-lib end
5
14
  FALLOW_BIN=$(FALLOW_CWD="$(pwd)" node -e "
6
- const { resolveFallowBinary } = require('./gsd-core/bin/lib/fallow-runner.cjs');
15
+ const { resolveFallowBinary } = require(require('path').join(process.env.GSD_LIB_DIR, 'fallow-runner.cjs'));
7
16
  const resolved = resolveFallowBinary({ cwd: process.env.FALLOW_CWD });
8
17
  if (resolved) process.stdout.write(resolved);
9
18
  ")
@@ -32,8 +32,20 @@ PHASE_ARG="${1}"
32
32
  # logic (e.g., --all and --auto imply --fix). Resolved BEFORE the init call below so
33
33
  # the section-manifest gate forwards the RESOLVED (post-implication) fix decision, not
34
34
  # just a literal --fix token check.
35
+ # gsd-resolve-lib begin code-review-flags.cjs (#5640): load helpers from the installed runtime, not the cwd
36
+ _gsd_lib_probe="code-review-flags.cjs"
37
+ _gsd_rt="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
38
+ _gsd_gr="$(command -v gsd_run 2>/dev/null || true)"
39
+ case "$_gsd_gr" in /*) ;; *) _gsd_gr="" ;; esac # a gsd_run shell function prints its bare name: not a path
40
+ [ -n "$_gsd_gr" ] && _gsd_gr="$(dirname "$(node -p "require('fs').realpathSync(process.argv[1])" "$_gsd_gr" 2>/dev/null || echo "$_gsd_gr")")/lib"
41
+ GSD_LIB_DIR=""
42
+ _gsd_pick() { for _gsd_d; do if [ -n "$_gsd_d" ] && [ -f "$_gsd_d/$_gsd_lib_probe" ]; then GSD_LIB_DIR="$_gsd_d"; return 0; fi; done; return 1; }
43
+ _gsd_pick "${GSD_TOOLS:+${GSD_TOOLS%/*}/lib}" "$_gsd_rt/gsd-core/bin/lib" "$_gsd_rt/.claude/gsd-core/bin/lib" "$_gsd_rt/.codex/gsd-core/bin/lib" "$_gsd_gr" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib" || true
44
+ if [ -z "$GSD_LIB_DIR" ]; then echo "ERROR: code-review-flags.cjs not found in the GSD runtime. Looked in: ${GSD_TOOLS:+${GSD_TOOLS%/*}/lib, }${_gsd_rt}/gsd-core/bin/lib (and its .claude and .codex forms), ${_gsd_gr:-next to gsd_run (not on PATH)}, ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib, ${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib. Reinstall: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
45
+ export GSD_LIB_DIR
46
+ # gsd-resolve-lib end
35
47
  FLAGS_JSON=$(node -e "
36
- const { parseCodeReviewFlags } = require('./gsd-core/bin/lib/code-review-flags.cjs');
48
+ const { parseCodeReviewFlags } = require(require('path').join(process.env.GSD_LIB_DIR, 'code-review-flags.cjs'));
37
49
  const flags = parseCodeReviewFlags(process.argv.slice(1));
38
50
  process.stdout.write(JSON.stringify(flags));
39
51
  " -- "$@" 2>/dev/null)
@@ -125,12 +137,25 @@ If FILES_OVERRIDE is set (from --files flag):
125
137
  ```bash
126
138
  if [ -n "$FILES_OVERRIDE" ]; then
127
139
  REVIEW_FILES=()
128
- REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
140
+ # #5519: compare the repository root and each resolved path in ONE form. On Windows Git Bash,
141
+ # `git rev-parse --show-toplevel` (a native program) reports a mixed path (C:/Users/..) while
142
+ # GNU `realpath` reports an MSYS POSIX path (/c/Users/..); the two never share a prefix, so
143
+ # every entry looked "outside". `cygpath -m` exists exactly where the forms differ and maps both
144
+ # to the mixed form; elsewhere the value passes through unchanged.
145
+ _gsd_norm_path() {
146
+ if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else printf '%s' "$1"; fi
147
+ }
148
+ REPO_ROOT=$(_gsd_norm_path "$(git rev-parse --show-toplevel 2>/dev/null)")
129
149
 
130
150
  for file_path in "${FILES_ARRAY[@]}"; do
131
- # Security: validate path is within repository (prevent path traversal)
132
- ABS_PATH=$(realpath -m "${file_path}" 2>/dev/null || echo "${file_path}")
133
- if [[ "$ABS_PATH" != "$REPO_ROOT"* ]]; then
151
+ # Security: validate path is within repository (prevent path traversal). Compare on a path
152
+ # boundary: the root itself or anything under "<root>/" — a sibling such as "<root>-other"
153
+ # shares the prefix but is not inside.
154
+ ABS_PATH=$(_gsd_norm_path "$(realpath -m "${file_path}" 2>/dev/null || echo "${file_path}")")
155
+ # Fail closed on two shapes that cannot be trusted: an empty root (git failed, so nothing can be
156
+ # "inside" it) and a path that still holds ".." segments (a resolved path never does, so it was
157
+ # not resolved — no GNU realpath).
158
+ if [ -z "$REPO_ROOT" ] || [[ "$ABS_PATH" == */../* || "$ABS_PATH" == */.. ]] || [[ "$ABS_PATH" != "$REPO_ROOT" && "$ABS_PATH" != "$REPO_ROOT"/* ]]; then
134
159
  echo "Error: File path outside repository, skipping: ${file_path}"
135
160
  continue
136
161
  fi
@@ -437,8 +462,20 @@ DEPTH_PAYLOAD=$(printf '%s\n' "${REVIEW_FILES[@]}" | FLAG_DEPTH="$DEPTH_OVERRIDE
437
462
  }));
438
463
  ")
439
464
 
465
+ # gsd-resolve-lib begin code-review-depth.cjs (#5640): load helpers from the installed runtime, not the cwd
466
+ _gsd_lib_probe="code-review-depth.cjs"
467
+ _gsd_rt="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
468
+ _gsd_gr="$(command -v gsd_run 2>/dev/null || true)"
469
+ case "$_gsd_gr" in /*) ;; *) _gsd_gr="" ;; esac # a gsd_run shell function prints its bare name: not a path
470
+ [ -n "$_gsd_gr" ] && _gsd_gr="$(dirname "$(node -p "require('fs').realpathSync(process.argv[1])" "$_gsd_gr" 2>/dev/null || echo "$_gsd_gr")")/lib"
471
+ GSD_LIB_DIR=""
472
+ _gsd_pick() { for _gsd_d; do if [ -n "$_gsd_d" ] && [ -f "$_gsd_d/$_gsd_lib_probe" ]; then GSD_LIB_DIR="$_gsd_d"; return 0; fi; done; return 1; }
473
+ _gsd_pick "${GSD_TOOLS:+${GSD_TOOLS%/*}/lib}" "$_gsd_rt/gsd-core/bin/lib" "$_gsd_rt/.claude/gsd-core/bin/lib" "$_gsd_rt/.codex/gsd-core/bin/lib" "$_gsd_gr" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib" || true
474
+ if [ -z "$GSD_LIB_DIR" ]; then echo "ERROR: code-review-depth.cjs not found in the GSD runtime. Looked in: ${GSD_TOOLS:+${GSD_TOOLS%/*}/lib, }${_gsd_rt}/gsd-core/bin/lib (and its .claude and .codex forms), ${_gsd_gr:-next to gsd_run (not on PATH)}, ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib, ${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib. Reinstall: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi
475
+ export GSD_LIB_DIR
476
+ # gsd-resolve-lib end
440
477
  DEPTH_JSON=$(echo "$DEPTH_PAYLOAD" | node -e "
441
- const { resolveCodeReviewDepth } = require('./gsd-core/bin/lib/code-review-depth.cjs');
478
+ const { resolveCodeReviewDepth } = require(require('path').join(process.env.GSD_LIB_DIR, 'code-review-depth.cjs'));
442
479
  const input = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf-8'));
443
480
  process.stdout.write(JSON.stringify(resolveCodeReviewDepth(input)));
444
481
  ")
@@ -737,9 +774,21 @@ if [ -n "$FALLOW_JSON_PATH" ] && [ -f "$FALLOW_JSON_PATH" ]; then
737
774
  # Normalize fallow's raw report into the compact {summary, findings[]} contract
738
775
  # the reviewer consumes (real fallow schema -> normalized findings).
739
776
  FALLOW_NORMALIZED_PATH="${PHASE_DIR}/FALLOW-normalized.json"
777
+ # gsd-resolve-lib begin fallow-runner.cjs (#5640): load helpers from the installed runtime, not the cwd
778
+ _gsd_lib_probe="fallow-runner.cjs"
779
+ _gsd_rt="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
780
+ _gsd_gr="$(command -v gsd_run 2>/dev/null || true)"
781
+ case "$_gsd_gr" in /*) ;; *) _gsd_gr="" ;; esac # a gsd_run shell function prints its bare name: not a path
782
+ [ -n "$_gsd_gr" ] && _gsd_gr="$(dirname "$(node -p "require('fs').realpathSync(process.argv[1])" "$_gsd_gr" 2>/dev/null || echo "$_gsd_gr")")/lib"
783
+ GSD_LIB_DIR=""
784
+ _gsd_pick() { for _gsd_d; do if [ -n "$_gsd_d" ] && [ -f "$_gsd_d/$_gsd_lib_probe" ]; then GSD_LIB_DIR="$_gsd_d"; return 0; fi; done; return 1; }
785
+ _gsd_pick "${GSD_TOOLS:+${GSD_TOOLS%/*}/lib}" "$_gsd_rt/gsd-core/bin/lib" "$_gsd_rt/.claude/gsd-core/bin/lib" "$_gsd_rt/.codex/gsd-core/bin/lib" "$_gsd_gr" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib" || true
786
+ if [ -z "$GSD_LIB_DIR" ]; then echo "ERROR: fallow-runner.cjs not found in the GSD runtime (looked in ${_gsd_rt}/gsd-core/bin/lib (and its .claude and .codex forms), ${_gsd_gr:-next to gsd_run (not on PATH)}, ${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/lib, ${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/lib); continuing without it." >&2; fi
787
+ export GSD_LIB_DIR
788
+ # gsd-resolve-lib end
740
789
  FALLOW_SRC="$FALLOW_JSON_PATH" FALLOW_OUT="$FALLOW_NORMALIZED_PATH" node -e "
741
790
  const fs = require('fs');
742
- const { normalizeFallowReportFile } = require('./gsd-core/bin/lib/fallow-runner.cjs');
791
+ const { normalizeFallowReportFile } = require(require('path').join(process.env.GSD_LIB_DIR, 'fallow-runner.cjs'));
743
792
  const n = normalizeFallowReportFile(process.env.FALLOW_SRC);
744
793
  fs.writeFileSync(process.env.FALLOW_OUT, JSON.stringify(n, null, 2));
745
794
  " 2>/dev/null && FALLOW_EMBED_PATH="$FALLOW_NORMALIZED_PATH" || FALLOW_EMBED_PATH="$FALLOW_JSON_PATH"
@@ -943,6 +992,11 @@ grep -A 3 "^### CR-\|^### BL-\|^### WR-" "${REVIEW_PATH}" | head -n 12
943
992
  **Windows:** This workflow uses bash features (arrays, process substitution). On Windows, it requires
944
993
  Git Bash or WSL. Native PowerShell is not supported. The CI matrix (Ubuntu/macOS/Windows)
945
994
  runs under Git Bash on Windows runners, which provides bash compatibility.
995
+ The `--files` containment check compares the repository root with each resolved path, and Git Bash
996
+ reports them in different forms (`git rev-parse` returns `C:/Users/...`, GNU `realpath` returns
997
+ `/c/Users/...`), so both are normalized with `cygpath -m` (shipped with Git Bash) before the
998
+ comparison. Relative paths and absolute paths in either form are accepted when they are inside the
999
+ repository (#5519).
946
1000
 
947
1001
  **macOS:** macOS ships with bash 3.2 (GPL licensing). This workflow does NOT use `mapfile` (bash 4+
948
1002
  only) — all array construction uses portable `while IFS= read -r` loops compatible with bash 3.2.