@opengsd/gsd-core 1.9.0 → 1.10.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 (223) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +131 -34
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +503 -341
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +607 -63
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +106 -51
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -25
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/project-root.cjs +48 -0
  62. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  63. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  64. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  65. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  66. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  67. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  68. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  69. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  70. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  71. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  72. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  73. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  74. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  75. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  76. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  77. package/gsd-core/bin/lib/state.cjs +151 -62
  78. package/gsd-core/bin/lib/surface.cjs +12 -1
  79. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  80. package/gsd-core/bin/lib/uat.cjs +320 -21
  81. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  82. package/gsd-core/bin/lib/verification.cjs +29 -12
  83. package/gsd-core/bin/lib/verify.cjs +29 -5
  84. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  85. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  86. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  87. package/gsd-core/bin/lib/workstream.cjs +6 -0
  88. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  89. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  90. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  91. package/gsd-core/references/artifact-types.md +10 -3
  92. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  93. package/gsd-core/references/debugger-techniques.md +255 -0
  94. package/gsd-core/references/research-documentation-lookup.md +5 -3
  95. package/gsd-core/references/specless-probe-fallback.md +7 -6
  96. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  97. package/gsd-core/references/worktree-branch-check.md +2 -2
  98. package/gsd-core/templates/summary-complex.md +2 -0
  99. package/gsd-core/templates/summary-minimal.md +2 -0
  100. package/gsd-core/templates/summary-standard.md +2 -0
  101. package/gsd-core/templates/summary.md +2 -0
  102. package/gsd-core/workflows/audit-milestone.md +3 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  107. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  108. package/gsd-core/workflows/autonomous.md +32 -69
  109. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  110. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  111. package/gsd-core/workflows/code-review.md +42 -145
  112. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  113. package/gsd-core/workflows/complete-milestone.md +23 -81
  114. package/gsd-core/workflows/debug.md +9 -12
  115. package/gsd-core/workflows/diagnose-issues.md +22 -0
  116. package/gsd-core/workflows/discovery-phase.md +4 -4
  117. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  118. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  119. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  120. package/gsd-core/workflows/docs-update.md +8 -51
  121. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  122. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  123. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  124. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  126. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  127. package/gsd-core/workflows/execute-phase.md +65 -137
  128. package/gsd-core/workflows/execute-plan.md +1 -1
  129. package/gsd-core/workflows/help/modes/full.md +6 -1
  130. package/gsd-core/workflows/ingest-docs.md +2 -1
  131. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  132. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  133. package/gsd-core/workflows/new-milestone.md +21 -38
  134. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  135. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  136. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  137. package/gsd-core/workflows/new-project.md +13 -226
  138. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  139. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  140. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  142. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  143. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  144. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  145. package/gsd-core/workflows/plan-phase.md +49 -193
  146. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  147. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  148. package/gsd-core/workflows/progress.md +11 -153
  149. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  150. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  151. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  152. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  153. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  154. package/gsd-core/workflows/quick.md +20 -390
  155. package/gsd-core/workflows/resume-project.md +3 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  157. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  158. package/gsd-core/workflows/review.md +15 -8
  159. package/gsd-core/workflows/section-manifest.json +219 -0
  160. package/gsd-core/workflows/sketch.md +1 -1
  161. package/gsd-core/workflows/spec-phase.md +17 -14
  162. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  163. package/gsd-core/workflows/spike.md +50 -16
  164. package/gsd-core/workflows/sync-skills.md +49 -11
  165. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  166. package/gsd-core/workflows/transition.md +8 -21
  167. package/gsd-core/workflows/ui-phase.md +8 -7
  168. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  169. package/gsd-core/workflows/update.md +18 -7
  170. package/gsd-core/workflows/verify-phase.md +4 -7
  171. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  172. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  173. package/gsd-core/workflows/verify-work.md +8 -58
  174. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  175. package/hooks/dist/gsd-check-update-worker.js +14 -5
  176. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  177. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  178. package/hooks/dist/gsd-statusline.js +72 -6
  179. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  180. package/hooks/dist/gsd-write-guard.js +359 -0
  181. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  182. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  183. package/hooks/gsd-agent-isolation-guard.js +428 -0
  184. package/hooks/gsd-check-update-worker.js +14 -5
  185. package/hooks/gsd-cursor-subagent-start.js +532 -26
  186. package/hooks/gsd-read-injection-scanner.js +7 -0
  187. package/hooks/gsd-statusline.js +72 -6
  188. package/hooks/gsd-worktree-path-guard.js +2 -1
  189. package/hooks/gsd-write-guard.js +359 -0
  190. package/hooks/hooks.json +12 -0
  191. package/hooks/lib/isolation-sentinel.js +268 -0
  192. package/hooks/managed-hooks-registry.cjs +2 -0
  193. package/package.json +14 -5
  194. package/pi/gsd.cjs +57 -12
  195. package/scripts/build-hooks.js +9 -0
  196. package/scripts/changeset/lint.cjs +9 -2
  197. package/scripts/changeset/serialize.cjs +5 -1
  198. package/scripts/gen-capability-matrix.cjs +1 -1
  199. package/scripts/gen-context-index.cjs +448 -0
  200. package/scripts/gen-inventory-manifest.cjs +101 -1
  201. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  202. package/scripts/gen-registry.cjs +39 -15
  203. package/scripts/gen-section-manifest.cjs +638 -0
  204. package/scripts/generate-package-identity.cjs +4 -2
  205. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  206. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  207. package/scripts/lint-docs-command-form.cjs +195 -0
  208. package/scripts/lint-docs-required.cjs +9 -1
  209. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  210. package/scripts/lint-example-parser-parity.cjs +395 -0
  211. package/scripts/lint-test-file-count.allowlist.json +27 -1
  212. package/scripts/mutation-matrix.cjs +13 -0
  213. package/scripts/prompt-injection-scan.sh +27 -6
  214. package/scripts/registry-schema.cjs +323 -94
  215. package/scripts/run-tests.cjs +3 -2
  216. package/scripts/validate-registry.cjs +10 -6
  217. package/skills/gsd-autonomous/SKILL.md +1 -1
  218. package/skills/gsd-execute-phase/SKILL.md +1 -1
  219. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  220. package/skills/gsd-new-milestone/SKILL.md +1 -1
  221. package/skills/gsd-plan-phase/SKILL.md +2 -2
  222. package/vscode/package.json +1 -1
  223. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * Plan Dependency Graph — shared halt-propagation over a plan's depends_on DAG (#2830).
4
+ *
5
+ * Two independent "which plans are incomplete" readers exist in this codebase:
6
+ * phase.cts's wave-grouping (`cmdPhasePlanIndex`) and phase-locator.cts's
7
+ * phase-location primitive (`searchPhaseInDir`, consumed by ~50 symbols across
8
+ * five command routers). Before #2830, only the former parsed `depends_on` —
9
+ * and even it used the DAG only for topological wave assignment, never to
10
+ * propagate a halted plan's block onto its dependents. The latter never parsed
11
+ * `depends_on` at all; it derived completion from summary-file presence only.
12
+ * A plan that reaches a designed stop still writes a SUMMARY (recording the
13
+ * halt in prose only — no prior structured status existed for it), so both
14
+ * readers saw it as ordinary "complete" and reported its dependents as
15
+ * ordinary incomplete work, available to spawn against.
16
+ *
17
+ * This module is the SINGLE topological-order + halt-propagation engine both
18
+ * readers call, so the two can never re-diverge on this rule again. Each
19
+ * caller resolves its own raw `depends_on` tokens to canonical plan ids
20
+ * (case-fold + `extractCanonicalPlanId` fallback — the same resolution
21
+ * already performed by phase.cts's `computeDependencyLevels`) before
22
+ * building `PlanHaltNode[]`; this module owns the graph traversal (exactly
23
+ * one pass — Kahn's algorithm, or none at all when the caller already has a
24
+ * valid topological order, see `computeHaltPropagation`'s `precomputedOrder`
25
+ * parameter) plus the two small pure helpers below (`isHaltedStatus`,
26
+ * `buildSummaryFileIndex`) that both callers would otherwise duplicate
27
+ * identically — the exact "two implementations, one drifts" failure mode
28
+ * this fix exists to close. Each caller still owns its own file I/O (the
29
+ * actual `fs.readFileSync` + `extractFrontmatter` calls); only the
30
+ * interpretive logic is centralized here — except the SUMMARY-file
31
+ * read+extract wrapper itself (`isSummaryFileHalted`), which both callers
32
+ * previously duplicated near-identically and which is now centralized here
33
+ * too, for the same reason.
34
+ */
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ const node_fs_1 = __importDefault(require("node:fs"));
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
40
+ const frontmatterMod = require("./frontmatter.cjs");
41
+ const { extractFrontmatter } = frontmatterMod;
42
+ /**
43
+ * The one place "does this SUMMARY status value mean halted" is decided.
44
+ * Case-insensitive, trims whitespace. Both `phase.cts`'s `cmdPhasePlanIndex`
45
+ * and `phase-locator.cts`'s `searchPhaseInDir` call this after reading a
46
+ * completed plan's SUMMARY frontmatter `status` field, so the definition of
47
+ * "halted" cannot drift between the two readers.
48
+ */
49
+ function isHaltedStatus(status) {
50
+ if (typeof status !== 'string')
51
+ return false;
52
+ // #2830 review (defect 2): strip an unquoted trailing YAML comment (a run
53
+ // of whitespace followed by `#` and the rest of the line) before
54
+ // trimming/lowercasing. YAML scalars don't need quoting to carry an inline
55
+ // comment (`status: halted # designed stop`), but `extractFrontmatter`
56
+ // does not strip one — and all four summary templates literally show that
57
+ // spelling as guidance on the value line. Without this, an executor that
58
+ // mimics the template's own presentation would write a halt that silently
59
+ // reads back as not-halted. A `#` with no preceding whitespace is NOT a
60
+ // YAML comment start, so `halted#nospace` intentionally still fails to match.
61
+ const withoutTrailingComment = status.replace(/\s+#.*$/, '');
62
+ return withoutTrailingComment.trim().toLowerCase() === 'halted';
63
+ }
64
+ /**
65
+ * Read a plan's SUMMARY file and report whether it declares `status: halted`
66
+ * (a designed stop, not an ordinary completion). Returns false — never
67
+ * throws — on a missing/unreadable/malformed SUMMARY, so an unreadable file
68
+ * degrades to the pre-#2830 behavior ("has a SUMMARY = complete") rather
69
+ * than breaking either caller.
70
+ *
71
+ * Two callers share this wrapper: `phase.cts`'s `cmdPhasePlanIndex` and
72
+ * `phase-locator.cts`'s `searchPhaseInDir` (the phase-location primitive
73
+ * consumed by ~50 symbols across five command routers). Both previously
74
+ * carried a near-identical local copy (read file -> `extractFrontmatter` ->
75
+ * `isHaltedStatus` -> swallow errors) that this module's own header comment
76
+ * calls out as the exact "two implementations, one drifts" failure mode it
77
+ * exists to prevent — centralizing the read+extract wrapper here, not just
78
+ * the `isHaltedStatus` predicate, closes that gap.
79
+ *
80
+ * Takes a single resolved `summaryPath` (not a `dir` + `filename` pair) —
81
+ * `phase-locator.cts`'s prior local copy took the two parts separately and
82
+ * `path.join`'d them internally; that caller now does the join itself
83
+ * before calling in, so both callers share one signature.
84
+ *
85
+ * @param summaryPath - absolute or relative path to a `*-SUMMARY.md` file.
86
+ */
87
+ function isSummaryFileHalted(summaryPath) {
88
+ try {
89
+ const content = node_fs_1.default.readFileSync(summaryPath, 'utf-8');
90
+ const fm = extractFrontmatter(content, summaryPath);
91
+ return isHaltedStatus(fm['status']);
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * Build a planId -> summary-filename lookup from a phase's summary file
99
+ * list, keyed by both the exact SUMMARY-file-derived id and its canonical
100
+ * form (mirrors the `completedPlanIds` construction each caller already
101
+ * performs for its own SUMMARY-presence check — same `summaryFiles` list,
102
+ * same exact/canonical key pair — so the two can never disagree about which
103
+ * summary file belongs to which plan id). `extractCanonicalPlanId` is
104
+ * supplied by the caller (each module owns its own resolution helper).
105
+ */
106
+ function buildSummaryFileIndex(summaryFiles, extractCanonicalPlanId) {
107
+ const index = new Map();
108
+ for (const s of summaryFiles) {
109
+ const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
110
+ const canonical = extractCanonicalPlanId(s);
111
+ index.set(exact, s);
112
+ if (canonical !== exact)
113
+ index.set(canonical, s);
114
+ }
115
+ return index;
116
+ }
117
+ /**
118
+ * Computes halt-propagation over a plan dependency DAG.
119
+ *
120
+ * `precomputedOrder`: when the caller ALREADY has a valid topological order
121
+ * for these exact node ids (e.g. phase.cts's `cmdPhasePlanIndex`, which runs
122
+ * `computeDependencyLevels`'s Kahn's-algorithm pass for wave assignment
123
+ * before ever calling this function), pass it here and this function skips
124
+ * running Kahn's algorithm a second time — the halt-propagation forward pass
125
+ * below only needs A valid topological order, not to derive one itself.
126
+ * Omit it (as phase-locator.cts's `searchPhaseInDir` does — it has no prior
127
+ * traversal of this DAG) and this function derives the order itself; either
128
+ * way, exactly one Kahn's-algorithm pass runs per caller, never two.
129
+ *
130
+ * Diamond-safe (a plan blocked via two different halted ancestors gets both
131
+ * in its `blockedBy` list, deduplicated) and transitive-safe (a dependent of
132
+ * a dependent of a halted plan is blocked, at any depth).
133
+ */
134
+ function computeHaltPropagation(nodes, precomputedOrder) {
135
+ const byId = new Map(nodes.map((n) => [n.id, n]));
136
+ let order;
137
+ let visited;
138
+ if (precomputedOrder) {
139
+ // Caller already ran Kahn's algorithm over this exact node set (same ids)
140
+ // — trust its order and skip re-deriving one. `visited` mirrors the same
141
+ // "count of nodes reachable in valid topological order" contract a fresh
142
+ // derivation would produce.
143
+ order = precomputedOrder;
144
+ visited = precomputedOrder.length;
145
+ }
146
+ else {
147
+ const inDeg = new Map();
148
+ const adj = new Map(); // dependency id -> dependent ids
149
+ for (const n of nodes) {
150
+ if (!inDeg.has(n.id))
151
+ inDeg.set(n.id, 0);
152
+ if (!adj.has(n.id))
153
+ adj.set(n.id, []);
154
+ for (const depId of n.resolvedDependsOn) {
155
+ if (!byId.has(depId))
156
+ continue; // fail-safe: caller should have filtered these already
157
+ if (!adj.has(depId))
158
+ adj.set(depId, []);
159
+ adj.get(depId).push(n.id);
160
+ inDeg.set(n.id, (inDeg.get(n.id) ?? 0) + 1);
161
+ }
162
+ }
163
+ const queue = [];
164
+ for (const n of nodes) {
165
+ if ((inDeg.get(n.id) ?? 0) === 0)
166
+ queue.push(n.id);
167
+ }
168
+ // Dequeue by head index, not Array.shift() — O(1) amortized, same
169
+ // rationale as computeDependencyLevels (#307).
170
+ let head = 0;
171
+ let v = 0;
172
+ while (head < queue.length) {
173
+ const cur = queue[head++];
174
+ v++;
175
+ for (const dep of adj.get(cur) ?? []) {
176
+ inDeg.set(dep, inDeg.get(dep) - 1);
177
+ if (inDeg.get(dep) === 0)
178
+ queue.push(dep);
179
+ }
180
+ }
181
+ order = queue;
182
+ visited = v;
183
+ }
184
+ // `order` is a valid topological order for every visited node: for edge
185
+ // dep -> dependent, dep appears before dependent. A single forward pass
186
+ // over it — using each node's own already-resolved dependsOn ids —
187
+ // computes blockedBy without any further graph traversal.
188
+ const blockedBy = new Map();
189
+ for (const id of order) {
190
+ const node = byId.get(id);
191
+ if (!node)
192
+ continue;
193
+ const causes = new Set();
194
+ for (const depId of node.resolvedDependsOn) {
195
+ const depNode = byId.get(depId);
196
+ if (!depNode)
197
+ continue;
198
+ if (depNode.halted)
199
+ causes.add(depId);
200
+ const depCauses = blockedBy.get(depId);
201
+ if (depCauses) {
202
+ for (const c of depCauses)
203
+ causes.add(c);
204
+ }
205
+ }
206
+ if (causes.size > 0)
207
+ blockedBy.set(id, Array.from(causes));
208
+ }
209
+ // #2830 review (defect 1): a node involved in a depends_on cycle (or
210
+ // downstream of one) never reaches indegree 0, so it never appears in
211
+ // `order` and the forward pass above never visits it — it would otherwise
212
+ // end up absent from BOTH `blockedBy` and any cycle diagnostic, i.e.
213
+ // reported as ordinary runnable. A node whose position in the topological
214
+ // order is undecidable cannot be shown to be safe to run: silently
215
+ // dropping it is the exact silent-disappearance failure #2830 exists to
216
+ // prevent. Fail closed — give every such non-halted node an explicit,
217
+ // non-empty `blockedBy` entry so no consumer (present or future) can
218
+ // re-admit it as runnable merely by checking "absent from blockedBy".
219
+ // (A node that is itself halted is not "blocked" — it IS the blocker —
220
+ // so it is left out here exactly as the normal forward pass leaves it out.)
221
+ if (order.length < nodes.length) {
222
+ const orderSet = new Set(order);
223
+ for (const n of nodes) {
224
+ if (orderSet.has(n.id) || n.halted)
225
+ continue;
226
+ const causes = Array.from(new Set(n.resolvedDependsOn.filter((depId) => byId.has(depId)))).sort();
227
+ blockedBy.set(n.id, causes.length > 0 ? causes : [n.id]);
228
+ }
229
+ }
230
+ return { order, visited, blockedBy };
231
+ }
232
+ module.exports = { computeHaltPropagation, isHaltedStatus, buildSummaryFileIndex, isSummaryFileHalted };
@@ -146,6 +146,10 @@ function planningPaths(cwd, ws) {
146
146
  config: node_path_1.default.join(base, 'config.json'),
147
147
  phases: node_path_1.default.join(base, 'phases'),
148
148
  requirements: node_path_1.default.join(base, 'REQUIREMENTS.md'),
149
+ // #3149: the debug-session directory. Single source for both `state.load`'s
150
+ // `debug_dir` field and `init.debug`'s — previously each composed its own
151
+ // `path.join(planning, 'debug')` (DEFECT.GENERATIVE-FIX).
152
+ debug: node_path_1.default.join(base, 'debug'),
149
153
  };
150
154
  }
151
155
  /**
@@ -63,6 +63,33 @@ function findProjectRoot(startDir) {
63
63
  }
64
64
  return false;
65
65
  }
66
+ // #2843: nearest ancestor (including `from` itself) that contains a `.git`,
67
+ // bounded by `upTo` (exclusive). Returns the git-repo root, or null if none
68
+ // exists before `upTo` / the filesystem root. Used to detect a NESTED child
69
+ // repo whose root is strictly below a candidate ancestor `.planning/` — in
70
+ // that case the caller's repo boundary sits between start and the ancestor,
71
+ // so trusting the ancestor's `.planning/` would silently cross into a
72
+ // different project. (No `git` subprocess — fs walk only, matching
73
+ // isInsideGitRepo's deliberate no-spawn contract.)
74
+ function nearestGitRoot(from, upTo) {
75
+ let d = from;
76
+ while (d !== fsRoot) {
77
+ if (d === upTo)
78
+ break;
79
+ try {
80
+ if (node_fs_1.default.existsSync(d + node_path_1.default.sep + '.git'))
81
+ return d;
82
+ }
83
+ catch {
84
+ // ignore
85
+ }
86
+ const next = node_path_1.default.dirname(d);
87
+ if (next === d)
88
+ break;
89
+ d = next;
90
+ }
91
+ return null;
92
+ }
66
93
  let dir = resolvedStart;
67
94
  let depth = 0;
68
95
  while (dir !== fsRoot && depth < FIND_PROJECT_ROOT_MAX_DEPTH) {
@@ -114,6 +141,18 @@ function findProjectRoot(startDir) {
114
141
  // claims our startDir — explicit sub_repos config takes precedence over the
115
142
  // implicit .git signal. (#1422)
116
143
  if (isInsideGitRepo(parent)) {
144
+ // #2843: do NOT cross a git-repo boundary. If the caller is inside its
145
+ // OWN nested repo whose root is strictly below `parent`, trusting
146
+ // `parent`'s .planning/ would silently resolve to a DIFFERENT project.
147
+ // isInsideGitRepo only proved SOME .git exists between start and parent;
148
+ // verify that .git is parent's own (or absent between), not a nested
149
+ // child repo. If nearestGitRoot finds a .git strictly below parent,
150
+ // the boundary is crossed — fall through (do not return parent).
151
+ if (nearestGitRoot(resolvedStart, parent) !== null) {
152
+ dir = parent;
153
+ depth += 1;
154
+ continue;
155
+ }
117
156
  // Lookahead: walk ancestors above `parent` to find a sub_repos claim.
118
157
  let ancestor = node_path_1.default.dirname(parent);
119
158
  let ancestorDepth = 0;
@@ -169,6 +208,15 @@ function findProjectRoot(startDir) {
169
208
  try {
170
209
  const candidatePlanning = parent2 + node_path_1.default.sep + '.planning';
171
210
  if (node_fs_1.default.existsSync(candidatePlanning) && node_fs_1.default.statSync(candidatePlanning).isDirectory()) {
211
+ // #2843: do not cross a git-repo boundary. If the caller is inside its
212
+ // own nested repo (a .git strictly below parent2), parent2's .planning/
213
+ // belongs to a DIFFERENT project — keep walking is wrong; stop and fall
214
+ // through to the startDir fallback instead of silently resolving to the
215
+ // ancestor project. (Reached only when no .git exists anywhere in the
216
+ // chain per the triage, but guard defensively.)
217
+ if (nearestGitRoot(resolvedStart, parent2) !== null) {
218
+ break;
219
+ }
172
220
  return parent2;
173
221
  }
174
222
  }