@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
@@ -20,12 +20,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.TRACKER_RESOLVE_BUDGET_MS = void 0;
23
24
  exports.decisionMentioned = decisionMentioned;
24
25
  exports.phaseDirProblem = phaseDirProblem;
25
26
  exports.loadPlanContents = loadPlanContents;
26
27
  exports.loadSummaryContents = loadSummaryContents;
27
28
  exports.loadDecisionExtraction = loadDecisionExtraction;
28
29
  exports.extractPlanDesignatedSections = extractPlanDesignatedSections;
30
+ exports.resolveTrackerBodies = resolveTrackerBodies;
29
31
  exports.buildPlanMessage = buildPlanMessage;
30
32
  exports.buildVerifyMessage = buildVerifyMessage;
31
33
  exports.phaseCommitMessages = phaseCommitMessages;
@@ -46,6 +48,12 @@ const { scanPhasePlans } = planScanMod;
46
48
  // eslint-disable-next-line @typescript-eslint/no-require-imports
47
49
  const planningScopeMod = require("./planning-scope.cjs");
48
50
  const { SCOPE } = planningScopeMod;
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const planDocumentMod = require("./plan-document.cjs");
53
+ const { parsePlanDocument } = planDocumentMod;
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ const taskContentResolutionMod = require("./task-content-resolution.cjs");
56
+ const { resolveTaskContent, splitTrackerId, loadResolverCapabilities, isResolverFailure, contentTexts, } = taskContentResolutionMod;
49
57
  // ─── Decision matching ────────────────────────────────────────────────────────
50
58
  function normalizePhrase(text) {
51
59
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -189,6 +197,72 @@ function extractPlanDesignatedSections(planContent) {
189
197
  parts.push((0, markdown_sectionizer_cjs_1.extractXmlTagBodies)(cleaned));
190
198
  return parts.join('\n\n');
191
199
  }
200
+ /** Aggregate wall-clock budget for all resolver spawns in one gate run; registry load and each spawn's timeout are inside it (spawn timeouts are clamped to the remainder), so this is the true bound for resolver work. */
201
+ exports.TRACKER_RESOLVE_BUDGET_MS = 120_000;
202
+ const unreadable = (reason, span) => ({ kind: 'unreadable', reason, span });
203
+ /**
204
+ * Extra scannable text per plan (same index) from tracker-resolved task content; '' for plans without `tracker-id=`.
205
+ * `missingResolver` names the first tracker id with no installed resolver: coverage is monotone (a mention anywhere counts), so
206
+ * that can only cause a false "uncovered" — callers go `unreadable` only when a decision would otherwise be uncovered.
207
+ */
208
+ function resolveTrackerBodies(projectDir, planContents, deps = {}) {
209
+ let missingResolver = null;
210
+ let capabilities = null;
211
+ const seen = new Map();
212
+ const resolveFn = deps.resolveTaskContentFn ?? resolveTaskContent;
213
+ const bodies = [];
214
+ const now = deps.now ?? (() => performance.now());
215
+ const startedAt = now();
216
+ for (const raw of planContents) {
217
+ const content = stripCommentsAndFences(raw);
218
+ if (!/\btracker-id\s*=/i.test(content)) {
219
+ bodies.push('');
220
+ continue;
221
+ }
222
+ const parts = [];
223
+ for (const task of parsePlanDocument(content).tasks) {
224
+ const trackerId = task.trackerId;
225
+ if (trackerId === null)
226
+ continue;
227
+ const cached = seen.get(trackerId);
228
+ if (cached) {
229
+ parts.push(...cached);
230
+ continue;
231
+ }
232
+ if (capabilities === null) {
233
+ try {
234
+ capabilities = (deps.loadCapabilities ?? loadResolverCapabilities)(projectDir);
235
+ }
236
+ catch (err) {
237
+ return unreadable(`capability registry unreadable: ${err instanceof Error ? err.message : String(err)}`, trackerId);
238
+ }
239
+ }
240
+ // Floor: spawnSync rejects a fractional timeout, and a 0 timeout means unbounded, so a sub-ms remainder is a rejection.
241
+ const remainingMs = Math.floor(exports.TRACKER_RESOLVE_BUDGET_MS - (now() - startedAt));
242
+ if (remainingMs <= 0) {
243
+ return unreadable(`tracker resolution exceeded the ${exports.TRACKER_RESOLVE_BUDGET_MS} ms aggregate budget`, trackerId);
244
+ }
245
+ try {
246
+ const result = resolveFn({ trackerId, capabilities, timeoutOverrideMs: remainingMs });
247
+ if (result.kind === 'no-resolver') {
248
+ missingResolver ??= { span: trackerId, reason: `no task-content resolver installed for tracker prefix "${splitTrackerId(trackerId)?.prefix}"` };
249
+ }
250
+ // Per field: an unclosed fence in one field must not swallow the rest.
251
+ const texts = result.kind === 'resolved' ? contentTexts(result.content).map(stripCommentsAndFences) : [];
252
+ seen.set(trackerId, texts);
253
+ parts.push(...texts);
254
+ }
255
+ catch (err) {
256
+ if (isResolverFailure(err)) {
257
+ return unreadable(`tracker task content unresolved: ${err.message}`, trackerId);
258
+ }
259
+ throw err;
260
+ }
261
+ }
262
+ bodies.push(parts.join('\n'));
263
+ }
264
+ return (0, gate_evidence_cjs_1.evidenceFound)({ bodies, missingResolver });
265
+ }
192
266
  function buildPlanMessage(uncovered) {
193
267
  if (uncovered.length === 0)
194
268
  return 'All trackable CONTEXT.md decisions are covered by plans.';
@@ -34,7 +34,10 @@ const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
34
34
  const planningWorkspace = require("./planning-workspace.cjs");
35
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- verification.cjs is an export= CommonJS module
36
36
  const verificationMod = require("./verification.cjs");
37
- const { planningDir } = planningWorkspace;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
38
+ const phaseIdMod = require("./phase-id.cjs");
39
+ const { scopeToPhase } = phaseIdMod;
40
+ const { planningDir, isPhaseSpecFile, findPhaseSpecIn } = planningWorkspace;
38
41
  const { defaultPhaseCleanCommitTimesMs } = verificationMod;
39
42
  /**
40
43
  * Pure comparator: which of `entries` have an effective last-changed time
@@ -54,7 +57,7 @@ function buildContextDriftMessage(staleArtifacts, phaseArg, projectDir) {
54
57
  if (staleArtifacts.some((f) => f.endsWith('-PATTERNS.md'))) {
55
58
  parts.push(`Regenerate patterns: delete the PATTERNS.md file, then re-run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, 'plan-phase')}.`);
56
59
  }
57
- if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || (f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md')))) {
60
+ if (staleArtifacts.some((f) => f.endsWith('-VALIDATION.md') || isPhaseSpecFile(f))) {
58
61
  parts.push('Regenerate or manually reconcile VALIDATION.md / SPEC.md against the current decisions.');
59
62
  }
60
63
  parts.push('Do not hand-inject the newer decisions into a prompt as a substitute for regenerating — that carries the staleness forward.');
@@ -98,7 +101,9 @@ function runContextDriftGate(cwd, phaseArg) {
98
101
  const researchFile = phaseFiles.find((f) => f.endsWith('-RESEARCH.md'));
99
102
  const patternsFile = phaseFiles.find((f) => f.endsWith('-PATTERNS.md'));
100
103
  const validationFile = phaseFiles.find((f) => f.endsWith('-VALIDATION.md'));
101
- const specFile = phaseFiles.find((f) => f.endsWith('-SPEC.md') && !f.endsWith('-AI-SPEC.md') && !f.endsWith('-UI-SPEC.md'));
104
+ // #5629: the shared phase-SPEC predicate (the init payloads' `has_spec` / `spec_path` use the same one).
105
+ // Scoped to this phase's own artifacts, as init scopes it, so a misfiled `02-SPEC.md` agrees in both.
106
+ const specFile = findPhaseSpecIn(scopeToPhase(phaseFiles, node_path_1.default.basename(phaseDir)));
102
107
  const upstreamFiles = [researchFile, patternsFile, validationFile, specFile].filter((f) => !!f);
103
108
  if (upstreamFiles.length === 0) {
104
109
  return skip('no-upstream-artifacts');
@@ -163,7 +163,10 @@ function evaluateDecisionCoveragePlan(input) {
163
163
  if (phaseDir && planContents.value.length === 0) {
164
164
  return (0, gate_verdict_cjs_1.gateVerdict)('block', true, { passed: false, skipped: false, reason: 'phase directory holds no plans', total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" holds no PLAN files, so no plans were scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
165
165
  }
166
- const sections = planContents.value.map(decision_coverage_support_cjs_1.extractPlanDesignatedSections);
166
+ const tracker = (0, decision_coverage_support_cjs_1.resolveTrackerBodies)(projectDir, planContents.value, input.deps);
167
+ if (tracker.kind === 'unreadable')
168
+ return unreadableDecisionGate(`${tracker.span}: ${tracker.reason}`);
169
+ const sections = planContents.value.map((c, i) => (0, decision_coverage_support_cjs_1.extractPlanDesignatedSections)(c) + (tracker.value.bodies[i] ? '\n' + tracker.value.bodies[i] : ''));
167
170
  const uncovered = [];
168
171
  let covered = 0;
169
172
  for (const decision of decisions) {
@@ -172,6 +175,8 @@ function evaluateDecisionCoveragePlan(input) {
172
175
  else
173
176
  uncovered.push({ id: decision.id, text: decision.text, category: decision.category });
174
177
  }
178
+ if (uncovered.length > 0 && tracker.value.missingResolver)
179
+ return unreadableDecisionGate(`${tracker.value.missingResolver.span}: ${tracker.value.missingResolver.reason}`);
175
180
  const passed = uncovered.length === 0;
176
181
  return (0, gate_verdict_cjs_1.gateVerdict)(passed ? 'pass' : 'block', !passed, {
177
182
  passed,
@@ -103,6 +103,9 @@ function evaluateDecisionCoverageVerify(input) {
103
103
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
104
104
  if (planContents.kind === 'unreadable')
105
105
  return unreadableVerify(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
106
+ const tracker = (0, decision_coverage_support_cjs_1.resolveTrackerBodies)(projectDir, planContents.value, input.deps);
107
+ if (tracker.kind === 'unreadable')
108
+ return unreadableVerify(`${tracker.span}: ${tracker.reason}`);
106
109
  const summaryParts = (0, decision_coverage_support_cjs_1.loadSummaryContents)(phaseDir);
107
110
  if (summaryParts.kind === 'unreadable')
108
111
  return unreadableVerify(`${summaryParts.span ?? phaseDir}: ${summaryParts.reason}`);
@@ -115,7 +118,7 @@ function evaluateDecisionCoverageVerify(input) {
115
118
  if (modifiedFiles.kind === 'unreadable')
116
119
  return unreadableVerify(`${modifiedFiles.span ?? 'files_modified'}: ${modifiedFiles.reason}`);
117
120
  const haystack = [
118
- planContents.value.join('\n\n'),
121
+ planContents.value.map((c, i) => c + (tracker.value.bodies[i] ? '\n' + tracker.value.bodies[i] : '')).join('\n\n'),
119
122
  summaryParts.value.join('\n\n'),
120
123
  modifiedFiles.value,
121
124
  (0, decision_coverage_support_cjs_1.phaseCommitMessages)(projectDir, phaseDir),
@@ -128,6 +131,8 @@ function evaluateDecisionCoverageVerify(input) {
128
131
  else
129
132
  notHonored.push({ id: decision.id, text: decision.text, category: decision.category });
130
133
  }
134
+ if (notHonored.length > 0 && tracker.value.missingResolver)
135
+ return unreadableVerify(`${tracker.value.missingResolver.span}: ${tracker.value.missingResolver.reason}`);
131
136
  return (0, gate_verdict_cjs_1.gateVerdict)(notHonored.length === 0 ? 'pass' : 'advisory', false, {
132
137
  skipped: false,
133
138
  blocking: false,
@@ -35,9 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MAX_COMMITS_CEILING = exports.SCOPE_EXCLUSION_PATHSPECS = void 0;
37
37
  exports.extractTaskCommitRefs = extractTaskCommitRefs;
38
+ exports.qualifiedPlanSubjectPattern = qualifiedPlanSubjectPattern;
38
39
  exports.planSubjectPattern = planSubjectPattern;
39
40
  exports.isSafeRefArgument = isSafeRefArgument;
40
41
  exports.isSafeDateArgument = isSafeDateArgument;
42
+ exports.commitIsAncestor = commitIsAncestor;
41
43
  exports.resolveEvaluationScope = resolveEvaluationScope;
42
44
  exports.evaluateEvaluationScope = evaluateEvaluationScope;
43
45
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -46,6 +48,9 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
46
48
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
47
49
  const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
48
50
  const pattern_cjs_1 = require("./pattern.cjs");
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const planningWorkspaceMod = require("./planning-workspace.cjs");
53
+ const { readScopedConfigValue } = planningWorkspaceMod;
49
54
  const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
50
55
  // ─── Constants ────────────────────────────────────────────────────────────────
51
56
  /** Paths that are never part of a code-review or UI scope (planning artifacts, lockfiles). */
@@ -107,25 +112,124 @@ function paddedPattern(value) {
107
112
  const intPart = (m[1] ?? '').replace(/^0+(?=\d)/, '');
108
113
  return `0*${(0, pattern_cjs_1.escapeEre)(intPart)}${(0, pattern_cjs_1.escapeEre)(m[2] ?? '')}`;
109
114
  }
115
+ /**
116
+ * The `<phase>-<plan>` fragment of a plan subject pattern, or null when `id` is not that shape.
117
+ *
118
+ * Zero-padding tolerant in both halves: `03-01` yields `0*3-0*1`, so `feat(03-01):`, `test(3-1):`
119
+ * and `fix(03-01)!:` match while `feat(03-010):` does not (#4003, #4619, #4748).
120
+ */
121
+ function phasePlanPattern(id) {
122
+ const dash = id.indexOf('-');
123
+ const phasePart = dash > 0 ? id.slice(0, dash) : '';
124
+ const planPart = dash > 0 ? id.slice(dash + 1) : '';
125
+ if (/^[0-9A-Za-z.]+$/.test(phasePart) && /^[0-9A-Za-z.]+$/.test(planPart)) {
126
+ return `${paddedPattern(phasePart)}-${paddedPattern(planPart)}`;
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * The anchored subject pattern for a `<phase>-<plan>` ERE fragment, optionally behind one scope
132
+ * qualifier as the group `(<qualifier>-)?`.
133
+ *
134
+ * Every spelling of a plan id goes through this one builder: the bare id, the id carrying the
135
+ * configured project code (#5271), and the workstream-qualified id `<ws>-<phase>-<plan>` that #5177
136
+ * proposes. The qualifier is ERE-escaped, so a workstream name (a path segment, not a project-code
137
+ * token) is matched literally. The group is a real capture group, never `(?:…)`: the pattern goes
138
+ * to `git log --extended-regexp`, and POSIX ERE has no non-capturing group.
139
+ */
140
+ function qualifiedPlanSubjectPattern(fragment, qualifier = null) {
141
+ const group = qualifier === null ? '' : `(${(0, pattern_cjs_1.escapeEre)(qualifier)}-)?`;
142
+ return `^[a-z]+\\(${group}${fragment}\\)!?:`;
143
+ }
144
+ /**
145
+ * The `<CODE>-` prefix of `planId` when it is EXACTLY the project's configured `project_code`
146
+ * followed by a dash — the separator included — else null.
147
+ *
148
+ * The match is CASE-SENSITIVE and anchored to the configured value. The phase-id module's
149
+ * `stripProjectCodePrefix` grammar (case-insensitive, any code before a digit) recognises ANY
150
+ * letter prefix, which is deliberately wider than the project: `AUTH-01-02` and `setup-1-2` are
151
+ * ids this project never assigned a code to, and stripping their prefixes made them resolve
152
+ * another plan's commits (#5271). Only the code the project actually configures may be
153
+ * stripped, so an id that merely LOOKS coded keeps its literal-match guarantee.
154
+ */
155
+ function configuredCodePrefix(planId, projectCode) {
156
+ if (projectCode === null || !isUsableProjectCode(projectCode))
157
+ return null;
158
+ const prefix = `${projectCode}-`;
159
+ return planId.startsWith(prefix) ? prefix : null;
160
+ }
110
161
  /**
111
162
  * The anchored subject pattern for a plan id, or null when `planId` is empty, over-long or carries a
112
163
  * control / whitespace character.
113
164
  *
114
165
  * A `<phase>-<plan>` id (`03-01`) is zero-padding tolerant: `feat(03-01):`, `test(3-1):` and
115
- * `fix(03-01)!:` match, `feat(03-010):` does not. Any other id (a plan FILE NAME that does not follow
116
- * the numbering) is matched LITERALLY — every ERE metacharacter escaped — so `x.*` or `a[b]` can only
117
- * match a commit that names that exact id, and never widens the pattern.
166
+ * `fix(03-01)!:` match, `feat(03-010):` does not. An id carrying the CONFIGURED project code
167
+ * (`PRJ-01-01` with `project_code: "PRJ"`, #5271) matches either spelling of the SAME plan —
168
+ * `feat(PRJ-01-01):` and `feat(01-01):` — because the two init verbs hand out different phase
169
+ * tokens for one plan. The code is stripped ONLY when it is the configured one: an id like
170
+ * `AUTH-01-02` or `setup-1-2`, which merely looks coded, is matched LITERALLY — every ERE
171
+ * metacharacter escaped — so it can only match a commit naming that exact id and never widens the
172
+ * pattern to another plan's commits. Any other id (a plan FILE NAME that does not follow the
173
+ * numbering) is matched literally for the same reason.
174
+ *
175
+ * `projectCode` is the project's configured `project_code`; with none (no config, no `planningDir`),
176
+ * every id is matched literally, which is the pre-#5271 behaviour.
118
177
  */
119
- function planSubjectPattern(planId) {
178
+ function planSubjectPattern(planId, projectCode = null) {
120
179
  if (planId.length === 0 || planId.length > 200 || /[\s\x00-\x1f\x7f]/.test(planId))
121
180
  return null;
122
- const dash = planId.indexOf('-');
123
- const phasePart = dash > 0 ? planId.slice(0, dash) : '';
124
- const planPart = dash > 0 ? planId.slice(dash + 1) : '';
125
- if (/^[0-9A-Za-z.]+$/.test(phasePart) && /^[0-9A-Za-z.]+$/.test(planPart)) {
126
- return `^[a-z]+\\(${paddedPattern(phasePart)}-${paddedPattern(planPart)}\\)!?:`;
181
+ const direct = phasePlanPattern(planId);
182
+ if (direct)
183
+ return qualifiedPlanSubjectPattern(direct);
184
+ // #5271: `init.execute-phase` returns `phase_number: "PRJ-01"` where `init.plan-phase` returns
185
+ // `padded_phase: "01"`, and every gate builds its `--plan` from the former. Splitting `PRJ-01-01`
186
+ // at its FIRST dash leaves `01-01` as the plan half, which is not a plan number, so the id fell
187
+ // through to the literal pattern that no executor commit can ever carry — an empty scope at exit
188
+ // 0, which is the correct answer for a plan that has NOT started and the wrong one for one that
189
+ // has. Retry on the CONFIGURED project-code-stripped id, with the code OPTIONAL so the commit the
190
+ // executor actually wrote still matches.
191
+ //
192
+ // The `(PRJ-)?` group comes from `qualifiedPlanSubjectPattern`, and it rides ONLY on a remainder
193
+ // that could be a plan number, so an empty or non-numeric remainder keeps the whole id literal —
194
+ // never WIDER than the pre-#5271 pattern.
195
+ const prefix = configuredCodePrefix(planId, projectCode);
196
+ if (prefix !== null) {
197
+ const stripped = planId.slice(prefix.length);
198
+ const coded = phasePlanPattern(stripped);
199
+ // The only remainder `phasePlanPattern` cannot split is the N-segment milestone shape
200
+ // (`01-02-03`), so require that whole shape: a bare `1`, `01-` or a trailing dash would
201
+ // otherwise widen the optional group onto `feat(1):`.
202
+ const planShaped = coded !== null || /^\d[0-9A-Za-z.]*(-[0-9A-Za-z.]+){2,}$/.test(stripped);
203
+ if (!planShaped)
204
+ return qualifiedPlanSubjectPattern((0, pattern_cjs_1.escapeEre)(planId));
205
+ return qualifiedPlanSubjectPattern(coded ?? (0, pattern_cjs_1.escapeEre)(stripped), projectCode);
127
206
  }
128
- return `^[a-z]+\\(${(0, pattern_cjs_1.escapeEre)(planId)}\\)!?:`;
207
+ return qualifiedPlanSubjectPattern((0, pattern_cjs_1.escapeEre)(planId));
208
+ }
209
+ /**
210
+ * Whether a configured `project_code` can safely be stripped from a plan id and put in a pattern.
211
+ *
212
+ * `config-set` stores `project_code` unvalidated and docs/CONFIGURATION.md calls it "any short
213
+ * string", so this is a SAFETY check, not the phase-id module's uppercase grammar: a letter first,
214
+ * then letters, digits, `_`, `.` or `-`, at most 32 characters. That admits `gsd` and `AUTH-X`
215
+ * (which the uppercase grammar left in the #5271 symptom) while excluding whitespace, control
216
+ * characters and anything long enough to be a sentence. The value is ERE-escaped by the pattern
217
+ * builder, so no metacharacter it admits can change the pattern.
218
+ */
219
+ function isUsableProjectCode(code) {
220
+ return /^[A-Za-z][A-Za-z0-9_.-]{0,31}$/.test(code);
221
+ }
222
+ /**
223
+ * The project's configured `project_code`, or null when it is absent, non-string or unusable
224
+ * (`isUsableProjectCode`). An unusable value reads as ABSENT: the coded id keeps its LITERAL
225
+ * spelling, which is never WIDER than the pre-#5271 pattern, since a literal id can only match a
226
+ * subject naming that exact id.
227
+ */
228
+ function configuredProjectCode(projectDir) {
229
+ const { present, value } = readScopedConfigValue(projectDir, ['project_code']);
230
+ if (!present || typeof value !== 'string')
231
+ return null;
232
+ return isUsableProjectCode(value) ? value : null;
129
233
  }
130
234
  // ─── Git plumbing ─────────────────────────────────────────────────────────────
131
235
  class ScopeUnreadable extends Error {
@@ -189,6 +293,23 @@ function isSafeDateArgument(candidate) {
189
293
  function isAncestor(git, sha, of) {
190
294
  return gitOrNo(git, ['merge-base', '--is-ancestor', sha, of]) !== null;
191
295
  }
296
+ /**
297
+ * Whether commit `sha` is an ancestor of commit `of` (#5275): ancestry, never log position or commit
298
+ * date, so a history merged from worktree branches is judged by what it descends from. `null` means
299
+ * git could not answer (missing, hung, a bad object): the caller must not read that as "no".
300
+ */
301
+ function commitIsAncestor(projectDir, sha, of, execGit) {
302
+ if (!isSafeRefArgument(sha) || !isSafeRefArgument(of))
303
+ return null;
304
+ try {
305
+ return isAncestor(makeGit(projectDir, execGit ?? shell_command_projection_cjs_1.execGit), sha, of);
306
+ }
307
+ catch (error) {
308
+ if (error instanceof ScopeUnreadable)
309
+ return null;
310
+ throw error;
311
+ }
312
+ }
192
313
  // ─── The resolver ─────────────────────────────────────────────────────────────
193
314
  function emptyScope(unit) {
194
315
  return {
@@ -210,7 +331,7 @@ function resolveEvaluationScope(projectDir, unit, options = {}) {
210
331
  if (unit.kind === 'phase')
211
332
  resolvePhase(projectDir, unit, scope, git, ref, options, repoRoot);
212
333
  else
213
- resolveBySubject(unit, scope, git, ref, options, repoRoot);
334
+ resolveBySubject(projectDir, unit, scope, git, ref, options, repoRoot);
214
335
  return scope;
215
336
  }
216
337
  catch (error) {
@@ -388,7 +509,7 @@ function resolvePhase(projectDir, unit, scope, git, ref, options, repoRoot) {
388
509
  scope.commits = parseLog(git(['log', `${scope.rangeBase}..${ref}`, logFormat(options.includeBody === true), '-n', String(max)]), options.includeBody === true);
389
510
  finalizeFiles(scope, repoRoot, rangeFiles(git, scope.rangeBase, ref));
390
511
  }
391
- function resolveBySubject(unit, scope, git, ref, options, repoRoot) {
512
+ function resolveBySubject(projectDir, unit, scope, git, ref, options, repoRoot) {
392
513
  const withBody = options.includeBody === true;
393
514
  // Refuse rather than truncate: a silently dropped positive pathspec narrows the match.
394
515
  const pathspecs = options.pathspecs ?? [];
@@ -412,7 +533,7 @@ function resolveBySubject(unit, scope, git, ref, options, repoRoot) {
412
533
  const grepArgs = [];
413
534
  let anchored = null;
414
535
  if (unit.kind === 'plan') {
415
- const pattern = planSubjectPattern(unit.planId);
536
+ const pattern = planSubjectPattern(unit.planId, configuredProjectCode(projectDir));
416
537
  if (pattern === null)
417
538
  throw new ScopeUnreadable('invalid-plan-id');
418
539
  grepArgs.push('--extended-regexp', `--grep=${pattern}`);
@@ -29,6 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.TDD_ROW_REASON = void 0;
32
33
  exports.evaluateTddReviewCheckpoint = evaluateTddReviewCheckpoint;
33
34
  const node_path_1 = __importDefault(require("node:path"));
34
35
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
@@ -38,6 +39,9 @@ const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
38
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
40
  const frontmatterMod = require("./frontmatter.cjs");
40
41
  const { frontmatterKeyHasValue, extractFrontmatter } = frontmatterMod;
42
+ exports.TDD_ROW_REASON = {
43
+ GREEN_BEFORE_RED: 'green_before_red',
44
+ };
41
45
  /**
42
46
  * True when the plan's frontmatter declares `type: tdd` (CRLF included, #2449). The block is the
43
47
  * one fence owner's, read as RAW text — not through the YAML parser — so a block the parser refuses
@@ -78,19 +82,39 @@ function transitiveDependents(planId, dependents) {
78
82
  return [...seen].sort();
79
83
  }
80
84
  /**
81
- * The commit types (`test`, `feat`, `refactor`, …) among the plan's own commits — those that
82
- * touched at least one path (`pathspecs: ['.']`, as the old `git log -- .` lookup did). An
83
- * unresolvable scope (git unavailable, an id that is not `<phase>-<plan>`) is "no commits".
85
+ * The plan's own commits (`test`, `feat`, `refactor`, …) — those that touched at least one path
86
+ * (`pathspecs: ['.']`, as the old `git log -- .` lookup did) — with the sha the ordering check needs.
87
+ * An unresolvable scope (git unavailable, an id that is not `<phase>-<plan>`) is "no commits".
84
88
  */
85
- function planCommitKinds(projectDir, planId) {
89
+ function planCommits(projectDir, planId) {
86
90
  const scope = (0, gate_evaluation_scope_cjs_1.resolveEvaluationScope)(projectDir, { kind: 'plan', planId }, { pathspecs: ['.'], commitsOnly: true });
87
- const kinds = new Set();
91
+ const commits = [];
88
92
  for (const commit of scope.commits) {
89
93
  const kind = /^([a-z]+)\(/.exec(commit.subject)?.[1];
90
94
  if (kind)
91
- kinds.add(kind);
95
+ commits.push({ sha: commit.sha, kind });
92
96
  }
93
- return kinds;
97
+ return commits;
98
+ }
99
+ const shasOfKind = (commits, kind) => commits.filter((c) => c.kind === kind).map((c) => c.sha);
100
+ /**
101
+ * Whether some GREEN descends from some RED (#5275). Decided by ancestry only — never log position or
102
+ * commit date — so a history merged from worktree branches is judged by what each commit descends from.
103
+ * `true` as soon as one pair is in order; `null` when git could not answer for a pair and none was
104
+ * in order (an environment fault is not a violation, so the caller keeps the credit); else `false`.
105
+ */
106
+ function greenFollowsRed(projectDir, reds, greens) {
107
+ let unanswered = false;
108
+ for (const red of reds) {
109
+ for (const green of greens) {
110
+ const redIsAncestor = (0, gate_evaluation_scope_cjs_1.commitIsAncestor)(projectDir, red, green);
111
+ if (redIsAncestor === true)
112
+ return true;
113
+ if (redIsAncestor === null)
114
+ unanswered = true;
115
+ }
116
+ }
117
+ return unanswered ? null : false;
94
118
  }
95
119
  /**
96
120
  * The verdict when the review could not read a plan or the phase directory. Advisory policy is
@@ -177,26 +201,41 @@ function evaluateTddReviewCheckpoint(input) {
177
201
  const rows = [];
178
202
  for (const planPath of tddPlanFiles) {
179
203
  const planId = node_path_1.default.basename(planPath, '-PLAN.md');
180
- const kinds = planCommitKinds(projectDir, planId);
181
- const red = kinds.has('test');
182
- const refactor = kinds.has('refactor');
204
+ const commits = planCommits(projectDir, planId);
205
+ const reds = shasOfKind(commits, 'test');
206
+ const red = reds.length > 0;
207
+ const refactor = commits.some((c) => c.kind === 'refactor');
208
+ // #5275: with a RED present, a GREEN counts only when it descends from a RED. Without one there is
209
+ // nothing to order against and the plan is already missing RED, so a GREEN commit stays credited.
210
+ const inOrder = (greens) => greens.length > 0 && (!red || greenFollowsRed(projectDir, reds, greens) !== false);
211
+ const ownGreens = shasOfKind(commits, 'feat');
212
+ const ownOk = inOrder(ownGreens);
183
213
  // #4867: a RED-only probe plan is satisfied by the GREEN of a plan that (transitively) declares
184
- // `depends_on` it. Only a real `feat(...)` commit under such a plan counts; the plan's own RED is
185
- // still required, and a plan nothing depends on is scored on its own commits alone.
186
- const ownGreen = kinds.has('feat');
187
- const greenFrom = ownGreen
214
+ // `depends_on` it. Only a real `feat(...)` commit under such a plan counts, and it must follow the
215
+ // RED too (#5275); the plan's own RED is still required, and a plan nothing depends on is scored
216
+ // on its own commits alone.
217
+ const dependentGreens = ownOk
188
218
  ? []
189
- : transitiveDependents(planId, dependents).filter((dependentId) => planCommitKinds(projectDir, dependentId).has('feat'));
190
- const green = ownGreen || greenFrom.length > 0;
219
+ : transitiveDependents(planId, dependents).map((id) => ({ id, greens: shasOfKind(planCommits(projectDir, id), 'feat') }));
220
+ const greenFrom = dependentGreens.filter((d) => inOrder(d.greens)).map((d) => d.id);
221
+ const green = ownOk || greenFrom.length > 0;
222
+ const anyGreenCommit = ownGreens.length > 0 || dependentGreens.some((d) => d.greens.length > 0);
191
223
  const missing = [];
192
224
  if (!red)
193
225
  missing.push('RED');
194
- if (!green)
226
+ if (!anyGreenCommit)
195
227
  missing.push('GREEN');
196
- const status = missing.length === 0 ? 'Pass' : 'FAIL';
197
- rows.push(greenFrom.length > 0
198
- ? { planId, red, green, refactor, status, missing, greenFrom }
199
- : { planId, red, green, refactor, status, missing });
228
+ const status = missing.length === 0 && green ? 'Pass' : 'FAIL';
229
+ // Both commit kinds exist but no GREEN follows a RED: its own reason. A row that fails for a
230
+ // missing commit keeps the exact shape it always had (`missing` already says why).
231
+ const reason = status === 'FAIL' && missing.length === 0
232
+ ? exports.TDD_ROW_REASON.GREEN_BEFORE_RED
233
+ : undefined;
234
+ rows.push({
235
+ planId, red, green, refactor, status, missing,
236
+ ...(reason ? { reason } : {}),
237
+ ...(greenFrom.length > 0 ? { greenFrom } : {}),
238
+ });
200
239
  }
201
240
  const violations = rows.filter(r => r.status === 'FAIL').length;
202
241
  // Build review table
@@ -218,7 +257,9 @@ function evaluateTddReviewCheckpoint(input) {
218
257
  if (violations > 0) {
219
258
  table += '\n\n⚠ Gate violations are advisory — review before advancing.';
220
259
  for (const r of rows.filter(row => row.status === 'FAIL')) {
221
- table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
260
+ table += r.reason === exports.TDD_ROW_REASON.GREEN_BEFORE_RED
261
+ ? `\n Plan ${r.planId} GREEN precedes RED: no feat(...) commit descends from a test(...) commit.`
262
+ : `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
222
263
  table += `\n Expected commit pattern: test(${r.planId}): ... → feat(${r.planId}): ...`;
223
264
  }
224
265
  }
@@ -21,6 +21,8 @@ const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
21
21
  const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
22
22
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
23
23
  const ui_safety_gate_cjs_1 = require("./ui-safety-gate.cjs");
24
+ const ui_frontend_evidence_cjs_1 = require("./ui-frontend-evidence.cjs");
25
+ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
24
26
  const UI_FILE_EXTENSIONS_RE = /\.(tsx|jsx|css|scss|sass|less|vue|svelte|html)$/i;
25
27
  const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|frontend)\//i;
26
28
  /**
@@ -32,7 +34,10 @@ const UI_PATH_PATTERNS_RE = /\/(components|pages|views|screens|layouts|ui|fronte
32
34
  * git call is bounded by the resolver's seam; an unreadable scope is reported, not "clean").
33
35
  * (d) Phase directory → `*-UI-SPEC.md`.
34
36
  *
35
- * `block = frontend && hasUiFiles && !hasUiSpec`.
37
+ * (c') Static frontend evidence (`readStaticFrontendEvidence`, the definition `computeUiPlanGate` uses),
38
+ * probed only for a frontend phase that changed UI files (#5646).
39
+ *
40
+ * `block = frontend && hasUiFiles && hasFrontendEvidence && !hasUiSpec`.
36
41
  */
37
42
  function computeUiSafetyGate(projectDir, phase) {
38
43
  // (a) phase section text (same two-pass lookup as computeUiPlanGate)
@@ -43,14 +48,27 @@ function computeUiSafetyGate(projectDir, phase) {
43
48
  // (c) any UI files in the phase's own commits? (deleted paths count: a removed component is a UI change)
44
49
  const scope = (0, gate_evaluation_scope_cjs_1.resolveEvaluationScope)(projectDir, { kind: 'phase', phase });
45
50
  const hasUiFiles = scope.changedFiles.some((f) => f.trim() && (UI_FILE_EXTENSIONS_RE.test(f) || UI_PATH_PATTERNS_RE.test(f)));
51
+ // (c') #5646 — static structural corroboration, the same evidence `computeUiPlanGate` requires (#3312):
52
+ // `hasUiFiles` is a weak signal (an `.html` fixture, or any file under a `ui/` directory in any
53
+ // language), so on its own it halted projects with no frontend that the plan gate had cleared. Only
54
+ // probed when it could change the answer. `unreadable` (#5170) is a manifest or tree the probe could
55
+ // not look at: absence of evidence is then not established, and the verdict says so (below).
56
+ const frontendEvidence = frontend && hasUiFiles ? (0, ui_frontend_evidence_cjs_1.readStaticFrontendEvidence)(projectDir) : (0, gate_evidence_cjs_1.evidenceNone)();
57
+ const hasFrontendEvidence = frontendEvidence.kind === 'found';
46
58
  // (d) phase directory and *-UI-SPEC.md
47
59
  // `none` is "no spec"; `unreadable` is "could not look" (#5170) and is carried to the verdict.
48
60
  const uiSpec = (0, gate_phase_context_cjs_1.locateUiSpec)(projectDir, phase);
49
61
  const hasUiSpec = uiSpec.kind === 'found';
50
- const readError = roadmapReadError ?? (uiSpec.kind === 'unreadable' ? uiSpec.reason : undefined);
51
- // block only when: this is a frontend phase AND UI files were changed AND no UI-SPEC exists
52
- const block = frontend && hasUiFiles && !hasUiSpec;
53
- const result = { frontend, hasUiFiles, hasUiSpec, block };
62
+ // Unreadable frontend evidence only matters when it could flip the verdict: with a UI-SPEC present the
63
+ // gate does not block whatever the tree holds.
64
+ const frontendReadError = frontendEvidence.kind === 'unreadable' && !hasUiSpec
65
+ ? `static frontend evidence could not be read (${frontendEvidence.reason})`
66
+ : undefined;
67
+ const readError = roadmapReadError ?? (uiSpec.kind === 'unreadable' ? uiSpec.reason : undefined) ?? frontendReadError;
68
+ // block only when: this is a frontend phase AND UI files were changed AND the project has static
69
+ // frontend evidence AND no UI-SPEC exists
70
+ const block = frontend && hasUiFiles && hasFrontendEvidence && !hasUiSpec;
71
+ const result = { frontend, hasUiFiles, hasFrontendEvidence, hasUiSpec, block };
54
72
  if (block) {
55
73
  result.message = `UI files changed in this wave but no UI-SPEC.md exists for Phase ${phase}. ` +
56
74
  `Run ${(0, runtime_slash_cjs_1.runtimeCommand)(projectDir, `ui-phase ${phase}`)} to generate the design contract before continuing.`;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Keep a machine-local file out of `git status` without touching any tracked
4
+ * file: append an anchored entry to the enclosing repo's `info/exclude` (#5551).
5
+ *
6
+ * Used for files GSD writes under `.planning/` that are per-machine state, not
7
+ * project content (the milestone claim). A copy that is already tracked is left
8
+ * alone — an exclude entry never untracks, and migrating it is the user's call.
9
+ *
10
+ * Best-effort by contract: never throws. Ignoring is optional, so no failure
11
+ * here (no git, read-only `.git`, odd path) may perturb the caller.
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.excludeFromGit = excludeFromGit;
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
21
+ const text_lines_cjs_1 = require("./text-lines.cjs");
22
+ /** Escape gitignore metacharacters so a directory or file name matches literally. */
23
+ function escapeGitignore(s) {
24
+ return s.replace(/[\\*?[]/g, '\\$&');
25
+ }
26
+ /**
27
+ * Exclude `<dir>/<filename>` from git via the repo-local `info/exclude`.
28
+ * `dir` must already exist (it is the spawn cwd).
29
+ */
30
+ function excludeFromGit(dir, filename) {
31
+ try {
32
+ // gitignore cannot express a newline in a pattern, and git's line output could not be split.
33
+ if (/[\r\n]/.test(dir))
34
+ return;
35
+ // cwd must pick the repo: drop inherited repository-location variables (e.g. inside a git hook).
36
+ const env = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined, GIT_COMMON_DIR: undefined };
37
+ // The prefix goes last: stdout is trimmed, and a non-empty prefix ends in `/`, so only an
38
+ // empty prefix (dir is the git root) loses its line; a leading space survives.
39
+ const revParse = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--git-path', 'info/exclude', '--show-prefix'], {
40
+ cwd: dir,
41
+ env: env,
42
+ });
43
+ const [excludeRel, prefix = ''] = (0, text_lines_cjs_1.splitLines)(String(revParse.stdout));
44
+ if (revParse.exitCode !== 0 || !excludeRel)
45
+ return;
46
+ const exclude = node_path_1.default.resolve(dir, excludeRel);
47
+ const entry = `/${escapeGitignore(prefix)}${escapeGitignore(filename)}`;
48
+ const existing = node_fs_1.default.existsSync(exclude) ? node_fs_1.default.readFileSync(exclude, 'utf8') : '';
49
+ if ((0, text_lines_cjs_1.splitLines)(existing).includes(entry))
50
+ return;
51
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(exclude), { recursive: true });
52
+ node_fs_1.default.appendFileSync(exclude, (existing && !existing.endsWith('\n') ? '\n' : '') + entry + '\n');
53
+ }
54
+ catch {
55
+ // advisory: see the module header.
56
+ }
57
+ }