@pareto4/pp-core 1.11.1 → 1.12.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 (255) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +70 -6
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +93 -4
  59. package/pp-core/bin/lib/init.cjs +124 -134
  60. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  61. package/pp-core/bin/lib/install-run.cjs +536 -0
  62. package/pp-core/bin/lib/io.cjs +15 -0
  63. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  64. package/pp-core/bin/lib/milestone.cjs +345 -48
  65. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  66. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  67. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  68. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  69. package/pp-core/bin/lib/phase.cjs +247 -21
  70. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  71. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  72. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  73. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  74. package/pp-core/bin/lib/profile-output.cjs +11 -3
  75. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  76. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  77. package/pp-core/bin/lib/roadmap.cjs +141 -39
  78. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  79. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  80. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  81. package/pp-core/bin/lib/state-document.cjs +124 -7
  82. package/pp-core/bin/lib/state.cjs +784 -257
  83. package/pp-core/bin/lib/template.cjs +22 -6
  84. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  85. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  86. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  87. package/pp-core/bin/lib/vault.cjs +555 -23
  88. package/pp-core/bin/lib/verify.cjs +53 -31
  89. package/pp-core/bin/lib/workstream.cjs +1 -1
  90. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  91. package/pp-core/bin/portable-timeout.sh +89 -0
  92. package/pp-core/bin/pp-tools.cjs +149 -9
  93. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  94. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  95. package/pp-core/references/autonomous-best-effort.md +461 -0
  96. package/pp-core/references/autonomous-lifecycle.md +146 -0
  97. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  98. package/pp-core/references/checkpoints.md +1 -1
  99. package/pp-core/references/discovery-id-conventions.md +202 -0
  100. package/pp-core/references/discovery-rebaseline.md +254 -0
  101. package/pp-core/references/discovery-research-track.md +283 -0
  102. package/pp-core/references/discovery-traceability.md +400 -0
  103. package/pp-core/references/loop-hook-dispatch.md +7 -0
  104. package/pp-core/references/mandatory-initial-read.md +1 -1
  105. package/pp-core/references/planner-gap-closure.md +1 -0
  106. package/pp-core/references/planner-human-verify-mode.md +1 -1
  107. package/pp-core/references/planner-load-graph-context.md +1 -1
  108. package/pp-core/references/planner-plan-graph.md +66 -0
  109. package/pp-core/references/planner-tdd.md +1 -0
  110. package/pp-core/references/planning-config.md +8 -0
  111. package/pp-core/references/sketch-tooling.md +4 -0
  112. package/pp-core/references/tdd.md +1 -0
  113. package/pp-core/templates/claude-md.md +1 -1
  114. package/pp-core/templates/phase-prompt.md +6 -2
  115. package/pp-core/templates/prototype-review/review.js +303 -0
  116. package/pp-core/templates/state.md +6 -0
  117. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  118. package/pp-core/workflows/add-backlog.md +1 -1
  119. package/pp-core/workflows/add-phase.md +1 -1
  120. package/pp-core/workflows/add-tests.md +1 -1
  121. package/pp-core/workflows/add-todo.md +1 -1
  122. package/pp-core/workflows/ai-integration-phase.md +2 -2
  123. package/pp-core/workflows/audit-fix.md +12 -16
  124. package/pp-core/workflows/audit-milestone.md +1 -1
  125. package/pp-core/workflows/audit-uat.md +1 -1
  126. package/pp-core/workflows/autonomous.md +68 -306
  127. package/pp-core/workflows/check-todos.md +1 -1
  128. package/pp-core/workflows/cleanup.md +1 -1
  129. package/pp-core/workflows/code-review-fix.md +1 -1
  130. package/pp-core/workflows/code-review.md +19 -7
  131. package/pp-core/workflows/complete-milestone.md +41 -60
  132. package/pp-core/workflows/debug.md +1 -1
  133. package/pp-core/workflows/diagnose-issues.md +1 -1
  134. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  135. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  137. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  138. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  139. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  140. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  153. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  154. package/pp-core/workflows/discovery-sprint.md +79 -1714
  155. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  156. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  157. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  158. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  159. package/pp-core/workflows/discovery-to-build.md +21 -238
  160. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  161. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  163. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  164. package/pp-core/workflows/discuss-phase.md +9 -2
  165. package/pp-core/workflows/do.md +4 -4
  166. package/pp-core/workflows/docs-update.md +1 -1
  167. package/pp-core/workflows/edit-phase.md +1 -1
  168. package/pp-core/workflows/eval-review.md +1 -1
  169. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  170. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  171. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  173. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  174. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  175. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  176. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  177. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  178. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  179. package/pp-core/workflows/execute-phase.md +102 -216
  180. package/pp-core/workflows/execute-plan.md +6 -2
  181. package/pp-core/workflows/explore.md +1 -1
  182. package/pp-core/workflows/extract-learnings.md +1 -1
  183. package/pp-core/workflows/forensics.md +4 -4
  184. package/pp-core/workflows/graduation.md +1 -1
  185. package/pp-core/workflows/health.md +1 -1
  186. package/pp-core/workflows/help/modes/brief.md +1 -0
  187. package/pp-core/workflows/help/modes/default.md +2 -1
  188. package/pp-core/workflows/help/modes/full.md +12 -5
  189. package/pp-core/workflows/import.md +1 -1
  190. package/pp-core/workflows/ingest-docs.md +1 -1
  191. package/pp-core/workflows/init.md +18 -2
  192. package/pp-core/workflows/insert-phase.md +1 -1
  193. package/pp-core/workflows/list-workspaces.md +1 -1
  194. package/pp-core/workflows/manager.md +3 -3
  195. package/pp-core/workflows/map-codebase.md +2 -2
  196. package/pp-core/workflows/milestone-summary.md +4 -4
  197. package/pp-core/workflows/mvp-phase.md +1 -1
  198. package/pp-core/workflows/new-milestone.md +7 -8
  199. package/pp-core/workflows/new-project.md +6 -1
  200. package/pp-core/workflows/new-workspace.md +1 -1
  201. package/pp-core/workflows/next.md +2 -2
  202. package/pp-core/workflows/note.md +2 -2
  203. package/pp-core/workflows/pause-work.md +1 -1
  204. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  205. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  206. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  207. package/pp-core/workflows/plan-phase.md +43 -167
  208. package/pp-core/workflows/plan-review-convergence.md +2 -2
  209. package/pp-core/workflows/plant-seed.md +1 -1
  210. package/pp-core/workflows/pr-branch.md +1 -1
  211. package/pp-core/workflows/profile-user.md +1 -1
  212. package/pp-core/workflows/progress.md +3 -3
  213. package/pp-core/workflows/quick.md +3 -1
  214. package/pp-core/workflows/remove-phase.md +1 -1
  215. package/pp-core/workflows/remove-workspace.md +1 -1
  216. package/pp-core/workflows/resume-project.md +6 -12
  217. package/pp-core/workflows/review.md +1 -1
  218. package/pp-core/workflows/scan.md +1 -1
  219. package/pp-core/workflows/secure-phase.md +1 -1
  220. package/pp-core/workflows/settings-advanced.md +1 -1
  221. package/pp-core/workflows/settings-integrations.md +1 -1
  222. package/pp-core/workflows/settings.md +1 -1
  223. package/pp-core/workflows/ship.md +25 -8
  224. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  225. package/pp-core/workflows/sketch.md +2 -1
  226. package/pp-core/workflows/spec-phase.md +1 -1
  227. package/pp-core/workflows/spike-wrap-up.md +1 -1
  228. package/pp-core/workflows/spike.md +1 -1
  229. package/pp-core/workflows/stats.md +2 -2
  230. package/pp-core/workflows/thread.md +1 -1
  231. package/pp-core/workflows/transition.md +5 -9
  232. package/pp-core/workflows/ui-phase.md +23 -2
  233. package/pp-core/workflows/ui-review.md +1 -1
  234. package/pp-core/workflows/ultraplan-phase.md +2 -2
  235. package/pp-core/workflows/validate-phase.md +1 -1
  236. package/pp-core/workflows/vault-write-back.md +1 -1
  237. package/pp-core/workflows/verify-phase.md +29 -26
  238. package/pp-core/workflows/verify-work.md +14 -10
  239. package/scripts/base64-scan.sh +7 -41
  240. package/scripts/changeset/cli.cjs +25 -5
  241. package/scripts/dev-env.cjs +462 -0
  242. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  243. package/scripts/lib/size-budget.cjs +311 -0
  244. package/scripts/lint-neutrality-names.cjs +694 -0
  245. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  246. package/scripts/lint-org-coordinates.cjs +426 -0
  247. package/scripts/lint-skill-deps.cjs +48 -9
  248. package/scripts/lint-test-file-count.cjs +17 -2
  249. package/scripts/prompt-injection-scan.sh +41 -20
  250. package/scripts/sync-manifest-versions.cjs +41 -7
  251. package/scripts/sync-runtime-launcher.cjs +106 -1
  252. package/scripts/update-size-baseline.cjs +274 -30
  253. package/scripts/workflow-size.cjs +61 -9
  254. package/commands/pp-build-handoff.md +0 -46
  255. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
@@ -27,13 +27,145 @@ const phaseIdMod = require("./phase-id.cjs");
27
27
  const { escapeRegex, normalizePhaseName, phaseTokenMatches } = phaseIdMod;
28
28
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
29
  const roadmapParserMod = require("./roadmap-parser.cjs");
30
- const { getMilestonePhaseFilter, extractCurrentMilestone } = roadmapParserMod;
30
+ const { getMilestonePhaseFilter, extractCurrentMilestone, getClosedMilestonePhaseDirOwners } = roadmapParserMod;
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const coreUtilsMod = require("./core-utils.cjs");
33
- const { extractOneLinerFromBody } = coreUtilsMod;
33
+ const { extractOneLinerFromBody, toPosixPath } = coreUtilsMod;
34
34
  const { planningPaths } = planningWorkspace;
35
35
  const { extractFrontmatter } = frontmatterMod;
36
- const { writeStateMd, stateReplaceFieldWithFallback } = stateMod;
36
+ const { writeStateMd, stateReplaceField, stateReplaceFieldWithFallback, matchSessionSection } = stateMod;
37
+ /**
38
+ * Write `value` into the Session-scoped `Stopped at:` field — the exact scope the
39
+ * frontmatter `stopped_at` is derived from (matchSessionSection, as
40
+ * sessionStoppedAtField reads it) — so writeStateMd sees the caller changed the
41
+ * field and the frontmatter follows (261007-rly D-5, D-7). A Session section
42
+ * without the line gains it; a body without a Session section gains a
43
+ * `## Session` block holding the canonical `**Stopped at:**` label that
44
+ * `state record-session` writes.
45
+ */
46
+ function setSessionStoppedAt(content, value) {
47
+ const m = matchSessionSection(content);
48
+ if (m && m.index !== undefined) {
49
+ const start = m.index + m[0].length - m[1].length;
50
+ const slice = m[1];
51
+ const replaced = stateReplaceField(slice, 'Stopped At', value) || stateReplaceField(slice, 'Stopped at', value);
52
+ const next = replaced ?? `${slice.replace(/\s+$/, '')}\n**Stopped at:** ${value}\n`;
53
+ return content.slice(0, start) + next + content.slice(start + slice.length);
54
+ }
55
+ return `${content.trimEnd()}\n\n## Session\n\n**Stopped at:** ${value}\n`;
56
+ }
57
+ // ─── The milestone record (261007-rly D-1, D-2, D-6) ─────────────────────────
58
+ /** The marker for an index cell the engine cannot state truthfully — never a guess. */
59
+ const NOT_RECORDED = '(not recorded)';
60
+ /**
61
+ * The milestone label: the version once, then the name only when it says
62
+ * something the version does not (261007-rly D-2). One helper for the two
63
+ * surfaces that used to read `v1.1 v1.1` when no --name was given: the
64
+ * MILESTONES.md heading and the archived REQUIREMENTS header.
65
+ */
66
+ function milestoneLabel(version, name) {
67
+ const n = (name || '').trim();
68
+ return !n || n === version ? version : `${version} ${n}`;
69
+ }
70
+ function splitTableRow(line) {
71
+ const t = line.trim();
72
+ if (!t.startsWith('|'))
73
+ return null;
74
+ return t.replace(/^\|/, '').replace(/(?<!\\)\|$/, '').split(/(?<!\\)\|/).map((c) => c.trim());
75
+ }
76
+ /** One cell for one header, read from the header text at write time (D-1's column map). */
77
+ function indexCell(header, cells) {
78
+ let value;
79
+ switch (header.trim().toLowerCase()) {
80
+ case 'version':
81
+ value = cells.version;
82
+ break;
83
+ case 'shipped':
84
+ case 'date':
85
+ value = cells.shipped;
86
+ break;
87
+ case 'phases':
88
+ value = cells.phases === undefined ? undefined : String(cells.phases);
89
+ break;
90
+ case 'plans':
91
+ value = cells.plans === undefined ? undefined : String(cells.plans);
92
+ break;
93
+ case 'tasks':
94
+ value = cells.tasks === undefined ? undefined : String(cells.tasks);
95
+ break;
96
+ case 'name':
97
+ value = cells.name;
98
+ break;
99
+ case 'archive':
100
+ value = cells.archive;
101
+ break;
102
+ default: value = undefined; // Requirements, PRs, anything unforeseen
103
+ }
104
+ const text = (value ?? '').replace(/\s*\r?\n\s*/g, ' ').trim();
105
+ return text ? text.replace(/(?<!\\)\|/g, '\\|') : NOT_RECORDED;
106
+ }
107
+ /**
108
+ * The ONE writer of a milestone record into MILESTONES.md, shared by
109
+ * `milestone complete` and `validate health`'s backfill (261007-rly D-1, D-6).
110
+ *
111
+ * (a) The index row. Before the first `## ` line, a pipe table whose header row
112
+ * has a `Version` cell is the file's index: a row for this version is added
113
+ * directly below the separator (newest first, the order the live table and the
114
+ * sections both use), unless a body row already carries the version. No such
115
+ * table → no row; a table is never invented.
116
+ * (b) The section. Inserted immediately before the first `## ` line — after the
117
+ * H1, any preamble and the index table — never between the H1 and the table,
118
+ * which is where the old header-regex prepend put it on the live shape. With no
119
+ * `## ` line it is appended; an empty or missing file gets `# Milestones` + entry.
120
+ */
121
+ function insertMilestoneRecord(existing, record) {
122
+ if (existing === null || existing === undefined || !existing.trim()) {
123
+ return { content: `# Milestones\n\n${record.entry}`, index_row: 'no_index_table' };
124
+ }
125
+ const crlf = existing.includes('\r\n');
126
+ const bare = (l) => l.replace(/\r$/, '');
127
+ const lines = existing.split('\n');
128
+ const firstSection = () => lines.findIndex((l) => /^## /.test(bare(l)));
129
+ let indexRow = 'no_index_table';
130
+ const limit = firstSection() === -1 ? lines.length : firstSection();
131
+ for (let i = 0; i + 1 < limit; i++) {
132
+ const header = splitTableRow(bare(lines[i]));
133
+ const versionCol = header ? header.findIndex((c) => c.toLowerCase() === 'version') : -1;
134
+ if (!header || versionCol === -1)
135
+ continue;
136
+ const separator = splitTableRow(bare(lines[i + 1]));
137
+ if (!separator || !separator.every((c) => /^:?-+:?$/.test(c)))
138
+ continue;
139
+ let present = false;
140
+ for (let j = i + 2; j < lines.length; j++) {
141
+ const row = splitTableRow(bare(lines[j]));
142
+ if (!row)
143
+ break;
144
+ if ((row[versionCol] || '') === record.version)
145
+ present = true;
146
+ }
147
+ if (present) {
148
+ indexRow = 'already_present';
149
+ }
150
+ else {
151
+ const row = `| ${header.map((h) => indexCell(h, record.cells)).join(' | ')} |`;
152
+ lines.splice(i + 2, 0, crlf ? `${row}\r` : row);
153
+ indexRow = 'added';
154
+ }
155
+ break;
156
+ }
157
+ const entry = crlf ? record.entry.replace(/\n/g, '\r\n') : record.entry;
158
+ const at = firstSection();
159
+ let content;
160
+ if (at === -1) {
161
+ content = `${lines.join('\n').replace(/\s+$/, '')}${crlf ? '\r\n\r\n' : '\n\n'}${entry}`;
162
+ }
163
+ else {
164
+ const head = at === 0 ? '' : `${lines.slice(0, at).join('\n')}\n`;
165
+ content = head + entry + lines.slice(at).join('\n');
166
+ }
167
+ return { content, index_row: indexRow };
168
+ }
37
169
  function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
38
170
  if (!reqIdsRaw || reqIdsRaw.length === 0) {
39
171
  error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
@@ -103,6 +235,21 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
103
235
  total: reqIds.length,
104
236
  }, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
105
237
  }
238
+ /**
239
+ * The ONE refusal for "the planning base in scope has no ROADMAP.md" — the root of a
240
+ * project partitioned into workstreams when no --ws, env or pointer selects one, or a
241
+ * workstream right after `workstream create`. The predicate is the milestone filter's
242
+ * own `roadmapMissing` signal (CARRY-04); `milestone complete` and `phases
243
+ * archive-closed` both call this, so the two verbs answer the same input the same way
244
+ * (261007-mmz follow-up: archive-closed used to return an empty success there, which
245
+ * let new-milestone §7.5's stop check stay silent on an unsubstituted ${GSD_WS}).
246
+ */
247
+ function refuseWithoutRoadmap(roadmapMissing, roadmapPath, what) {
248
+ if (roadmapMissing) {
249
+ error(`ROADMAP.md not found at ${roadmapPath} — nothing to ${what}. ` +
250
+ `Write it, or pass --ws <name> if this project is partitioned into workstreams.`);
251
+ }
252
+ }
106
253
  function cmdMilestoneComplete(cwd, version, options, raw) {
107
254
  if (!version) {
108
255
  error('version required for milestone complete (e.g., v1.0)');
@@ -110,20 +257,55 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
110
257
  const roadmapPath = planningPaths(cwd).roadmap;
111
258
  const reqPath = planningPaths(cwd).requirements;
112
259
  const statePath = planningPaths(cwd).state;
113
- const milestonesPath = node_path_1.default.join(cwd, '.planning', 'MILESTONES.md');
114
- const archiveDir = node_path_1.default.join(cwd, '.planning', 'milestones');
260
+ // A milestone record belongs beside the ROADMAP it closes, so a project
261
+ // partitioned into workstreams archives into its OWN .planning/workstreams/
262
+ // <name>/milestones/ rather than the repo root — which is where all three of
263
+ // these used to land while roadmap/requirements/state/phases above already
264
+ // resolved per workstream. `--ws` needs no parsing here: pp-tools.cjs strips
265
+ // it centrally and applies it as GSD_WORKSTREAM, which planningDir() reads,
266
+ // and routing through planningPaths inherits that function's rejection of
267
+ // path separators and `..` in the name (phase 12, CARRY-04).
268
+ //
269
+ // CARRY-05: the location is no longer joined here — planningPaths owns it, so
270
+ // this writer and every reader resolve the same two paths from one place.
271
+ const milestonesPath = planningPaths(cwd).milestones;
272
+ const archiveDir = planningPaths(cwd).milestonesArchive;
115
273
  const phasesDir = planningPaths(cwd).phases;
116
274
  const today = new Date().toISOString().split('T')[0];
117
275
  const milestoneName = options.name || version;
118
- // Ensure archive directory exists
119
- (0, shell_command_projection_cjs_1.platformEnsureDir)(archiveDir);
120
276
  // Scope stats and accomplishments to only the phases belonging to the
121
277
  // current milestone's ROADMAP. Uses the shared filter from roadmap-parser.cjs
122
278
  // (same logic used by cmdPhasesList and other callers).
279
+ //
280
+ // All three refusals run BEFORE the archive directory is created below: a
281
+ // close that is going to be refused must leave nothing behind, not even an
282
+ // empty milestones/ directory. This is also the ONLY consumer of the filter's
283
+ // signal properties — the other nine call sites read none (phase 12).
123
284
  const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
285
+ refuseWithoutRoadmap(isDirInMilestone.roadmapMissing, roadmapPath, 'close');
124
286
  if (isDirInMilestone.missingExplicitVersion) {
125
287
  error(`no phases found for milestone ${version} in ROADMAP.md`);
126
288
  }
289
+ // The third "could not scope this" arm (261007-mmz D-1, D-3). A ROADMAP that
290
+ // reads fine and declares the version but names no phase for it — or whose
291
+ // parse threw, which the filter swallows — yields the same pass-all callable
292
+ // with phaseCount 0. At this consumer that is indistinguishable from "this
293
+ // milestone legitimately contains every directory", so it used to count every
294
+ // directory under phases/ into MILESTONES.md, overwrite the archived
295
+ // <version>-ROADMAP.md snapshot, and under --archive-phases rename them all
296
+ // under this version's label, exit 0. The filter stays pass-all for its nine
297
+ // non-destructive callers (changing it changes ten call sites at once, the
298
+ // CARRY-04 argument); the refusal lives here, unconditionally, because the
299
+ // close without --archive-phases writes those false records too.
300
+ // See todos/done/milestone-complete-archives-every-directory-when-the-roadmap-lists-no-phases.md.
301
+ if (isDirInMilestone.phaseCount === 0) {
302
+ error(`milestone ${version}: ROADMAP.md at ${roadmapPath} names no phase for this milestone — ` +
303
+ `refusing to close; nothing was archived, moved or written. ` +
304
+ `List the milestone's phases as "### Phase N: Name" headings under its milestone, ` +
305
+ `or pass the version the ROADMAP declares.`);
306
+ }
307
+ // Ensure archive directory exists
308
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(archiveDir);
127
309
  // Guard: prevent marking complete when ROADMAP still lists phases that have
128
310
  // no directory on disk (disk_status: no_directory). This catches the case
129
311
  // where the active milestone was erroneously marked complete before phases
@@ -245,43 +427,54 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
245
427
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent);
246
428
  }
247
429
  // Archive REQUIREMENTS.md
430
+ // 261007-rly D-1: the Archive cell links only what THIS run wrote.
431
+ const archivedHere = [];
432
+ if (node_fs_1.default.existsSync(roadmapPath))
433
+ archivedHere.push(`${version}-ROADMAP.md`);
248
434
  if (node_fs_1.default.existsSync(reqPath)) {
435
+ archivedHere.push(`${version}-REQUIREMENTS.md`);
249
436
  const reqContent = node_fs_1.default.readFileSync(reqPath, 'utf-8');
250
- const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
437
+ // 261007-rly D-2: the version once when no --name was given.
438
+ const archiveHeader = `# Requirements Archive: ${milestoneLabel(version, options.name)}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
251
439
  (0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent);
252
440
  }
253
441
  // Archive audit file if exists
254
- const auditFile = node_path_1.default.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`);
442
+ const auditFile = node_path_1.default.join(planningPaths(cwd).planning, `${version}-MILESTONE-AUDIT.md`);
255
443
  if (node_fs_1.default.existsSync(auditFile)) {
256
444
  node_fs_1.default.renameSync(auditFile, node_path_1.default.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
445
+ archivedHere.push(`${version}-MILESTONE-AUDIT.md`);
257
446
  }
258
- // Create/append MILESTONES.md entry
447
+ // Under --archive-phases the directory is created unconditionally below and a
448
+ // failed move ends the verb naming the partial state, so the target exists
449
+ // whenever the verb returns (261007-rly D-1).
450
+ if (options.archivePhases)
451
+ archivedHere.push(`${version}-phases/`);
452
+ // Create/append MILESTONES.md entry — one row in the index table (when the
453
+ // file has one) and the section after it, through the shared record writer
454
+ // (261007-rly D-1, D-2, D-6).
259
455
  const accomplishmentsList = accomplishments.map((a) => `- ${a}`).join('\n');
260
- const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
261
- if (node_fs_1.default.existsSync(milestonesPath)) {
262
- const existing = node_fs_1.default.readFileSync(milestonesPath, 'utf-8');
263
- if (!existing.trim()) {
264
- // Empty file — treat like new
265
- (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
266
- }
267
- else {
268
- // Insert after the header line(s) for reverse chronological order (newest first)
269
- const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
270
- if (headerMatch) {
271
- const header = headerMatch[1];
272
- const rest = existing.slice(header.length);
273
- (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, header + milestoneEntry + rest);
274
- }
275
- else {
276
- // No recognizable header — prepend the entry
277
- (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, milestoneEntry + existing);
278
- }
279
- }
280
- }
281
- else {
282
- (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, `# Milestones\n\n${milestoneEntry}`);
283
- }
456
+ const milestoneEntry = `## ${milestoneLabel(version, options.name)} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
457
+ const linkPrefix = toPosixPath(node_path_1.default.relative(node_path_1.default.dirname(milestonesPath), archiveDir));
458
+ const archiveCell = archivedHere
459
+ .map((f) => `[${f}](${linkPrefix ? `${linkPrefix}/` : ''}${f})`)
460
+ .join(' · ');
461
+ const milestonesRecord = insertMilestoneRecord(node_fs_1.default.existsSync(milestonesPath) ? node_fs_1.default.readFileSync(milestonesPath, 'utf-8') : null, {
462
+ version,
463
+ entry: milestoneEntry,
464
+ cells: {
465
+ version,
466
+ shipped: today,
467
+ phases: phaseCount,
468
+ plans: totalPlans,
469
+ tasks: totalTasks,
470
+ name: options.name || null,
471
+ archive: archiveCell,
472
+ },
473
+ });
474
+ (0, shell_command_projection_cjs_1.platformWriteSync)(milestonesPath, milestonesRecord.content);
284
475
  // Update STATE.md — keep frontmatter/body semantically aligned after closure
476
+ // 261007-ixt D-3: every frontmatter key the shared writer changed is named.
477
+ let stateFrontmatterUpdated = [];
285
478
  if (node_fs_1.default.existsSync(statePath)) {
286
479
  let stateContent = node_fs_1.default.readFileSync(statePath, 'utf-8');
287
480
  stateContent = stateReplaceFieldWithFallback(stateContent, 'Status', null, `${version} milestone complete`);
@@ -308,28 +501,57 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
308
501
  else {
309
502
  stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with ${(0, runtime_slash_cjs_1.formatGsdSlash)('new-milestone', (0, runtime_slash_cjs_1.resolveRuntime)(cwd))}\n`;
310
503
  }
311
- writeStateMd(statePath, stateContent, cwd);
504
+ // 261007-rly D-5: the Session `Stopped at:` line — and through writeStateMd's
505
+ // delta the frontmatter stopped_at — moves only when the caller passes a new
506
+ // value. Without --stopped-at neither moves and the key is not reported.
507
+ // T-rly-03: operator text, written as ONE line — a newline in the value could
508
+ // otherwise open a new section of STATE.md below the Session block.
509
+ const stoppedAt = typeof options.stoppedAt === 'string' ? options.stoppedAt.replace(/\s+/g, ' ').trim() : '';
510
+ if (stoppedAt) {
511
+ stateContent = setSessionStoppedAt(stateContent, stoppedAt);
512
+ }
513
+ stateFrontmatterUpdated = writeStateMd(statePath, stateContent, cwd, undefined, { stoppedAtSetByCaller: !!stoppedAt }).frontmatter_updated;
312
514
  }
313
515
  // Archive phase directories if requested
314
516
  let phasesArchived = false;
315
517
  if (options.archivePhases) {
518
+ const phaseArchiveDir = node_path_1.default.join(archiveDir, `${version}-phases`);
519
+ // A missing phases/ directory is "nothing to archive" — archived.phases
520
+ // stays false, which is true. Anything else that goes wrong is reported:
521
+ // a blanket catch here used to swallow a failed rename, so the verb exited
522
+ // 0 reporting archived.phases: false while every directory sorted before
523
+ // the failure HAD moved and the rest stayed behind (measured, 261007-mmz).
524
+ let phaseDirNames = [];
316
525
  try {
317
- const phaseArchiveDir = node_path_1.default.join(archiveDir, `${version}-phases`);
318
- (0, shell_command_projection_cjs_1.platformEnsureDir)(phaseArchiveDir);
319
- const phaseEntries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
320
- const phaseDirNames = phaseEntries.filter((e) => e.isDirectory()).map((e) => e.name);
321
- let archivedCount = 0;
322
- for (const dir of phaseDirNames) {
323
- if (!isDirInMilestone(dir))
324
- continue;
325
- node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
326
- archivedCount++;
526
+ phaseDirNames = node_fs_1.default
527
+ .readdirSync(phasesDir, { withFileTypes: true })
528
+ .filter((e) => e.isDirectory())
529
+ .map((e) => e.name)
530
+ .filter((name) => isDirInMilestone(name))
531
+ .sort();
532
+ }
533
+ catch (e) {
534
+ if (e?.code !== 'ENOENT') {
535
+ const message = e instanceof Error ? e.message : String(e);
536
+ error(`--archive-phases: could not read ${phasesDir}; the milestone record was already written. ${message}`);
327
537
  }
328
- phasesArchived = archivedCount > 0;
538
+ phaseDirNames = [];
329
539
  }
330
- catch {
331
- /* intentionally empty */
540
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(phaseArchiveDir);
541
+ const moved = [];
542
+ for (const dir of phaseDirNames) {
543
+ try {
544
+ node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, dir), node_path_1.default.join(phaseArchiveDir, dir));
545
+ }
546
+ catch (e) {
547
+ const message = e instanceof Error ? e.message : String(e);
548
+ error(`--archive-phases: could not move ${dir} into ${phaseArchiveDir} after moving ` +
549
+ `${moved.length} of ${phaseDirNames.length} (${moved.length > 0 ? moved.join(', ') : 'none'}); ` +
550
+ `the milestone record was already written. ${message}`);
551
+ }
552
+ moved.push(dir);
332
553
  }
554
+ phasesArchived = moved.length > 0;
333
555
  }
334
556
  const result = {
335
557
  version,
@@ -346,7 +568,9 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
346
568
  phases: phasesArchived,
347
569
  },
348
570
  milestones_updated: true,
571
+ milestones_index_row: milestonesRecord.index_row,
349
572
  state_updated: node_fs_1.default.existsSync(statePath),
573
+ state_frontmatter_updated: stateFrontmatterUpdated,
350
574
  };
351
575
  output(result, raw);
352
576
  }
@@ -374,8 +598,81 @@ function cmdPhasesClear(cwd, raw, args) {
374
598
  }
375
599
  output({ cleared }, raw, `${cleared} phase director${cleared === 1 ? 'y' : 'ies'} cleared`);
376
600
  }
601
+ /**
602
+ * `phases archive-closed [--dry-run]` (261007-mmz): move each directory under
603
+ * `phases/` that exactly one CLOSED milestone names into that milestone's
604
+ * `milestones/<version>-phases/`, and leave every other directory in place,
605
+ * reporting why. Membership comes from getClosedMilestonePhaseDirOwners — the
606
+ * archived `<version>-ROADMAP.md` snapshot lists the directory's number AND name,
607
+ * and the current milestone does not — the predicate the progress writer already
608
+ * reads (261007-ixt D-9). It replaces new-milestone.md's `--reset-phase-numbers`
609
+ * shell move, which renamed EVERY directory into the latest milestone's archive
610
+ * and so recorded a phase no milestone named as that milestone's (sweep R17a).
611
+ * Paths resolve through planningPaths, so `--ws` scopes it to one workstream.
612
+ */
613
+ function cmdPhasesArchiveClosed(cwd, raw, args) {
614
+ const dryRun = Array.isArray(args) && args.includes('--dry-run');
615
+ const phasesDir = planningPaths(cwd).phases;
616
+ const archiveDir = planningPaths(cwd).milestonesArchive;
617
+ refuseWithoutRoadmap(getMilestonePhaseFilter(cwd).roadmapMissing, planningPaths(cwd).roadmap, 'archive');
618
+ const ownersOf = getClosedMilestonePhaseDirOwners(cwd);
619
+ let dirNames = [];
620
+ try {
621
+ dirNames = node_fs_1.default
622
+ .readdirSync(phasesDir, { withFileTypes: true })
623
+ .filter((e) => e.isDirectory())
624
+ .map((e) => e.name)
625
+ .sort();
626
+ }
627
+ catch (e) {
628
+ if (e?.code !== 'ENOENT') {
629
+ const message = e instanceof Error ? e.message : String(e);
630
+ error(`phases archive-closed: could not read ${phasesDir}. ${message}`);
631
+ }
632
+ }
633
+ const rel = (p) => node_path_1.default.relative(cwd, p).split(node_path_1.default.sep).join('/');
634
+ const planned = [];
635
+ const left = [];
636
+ for (const dir of dirNames) {
637
+ const owners = ownersOf(dir);
638
+ if (owners.length === 1) {
639
+ planned.push({ dir, milestone: owners[0], to: node_path_1.default.join(archiveDir, `${owners[0]}-phases`, dir) });
640
+ }
641
+ else if (owners.length > 1) {
642
+ left.push({ dir, reason: 'named_by_several_closed_milestones', milestones: owners });
643
+ }
644
+ else if (/^999(?:\.|-|$)/.test(dir)) {
645
+ left.push({ dir, reason: 'backlog' });
646
+ }
647
+ else {
648
+ left.push({ dir, reason: 'not_named_by_a_closed_milestone' });
649
+ }
650
+ }
651
+ const moved = [];
652
+ if (!dryRun) {
653
+ for (const item of planned) {
654
+ try {
655
+ if (node_fs_1.default.existsSync(item.to))
656
+ throw new Error(`${rel(item.to)} already exists`);
657
+ (0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.dirname(item.to));
658
+ node_fs_1.default.renameSync(node_path_1.default.join(phasesDir, item.dir), item.to);
659
+ }
660
+ catch (e) {
661
+ const message = e instanceof Error ? e.message : String(e);
662
+ error(`phases archive-closed: could not move ${item.dir} into ${rel(node_path_1.default.dirname(item.to))} after moving ` +
663
+ `${moved.length} of ${planned.length} (${moved.length > 0 ? moved.map((m) => m.dir).join(', ') : 'none'}). ${message}`);
664
+ }
665
+ moved.push(item);
666
+ }
667
+ }
668
+ const shown = (dryRun ? planned : moved).map((m) => ({ dir: m.dir, milestone: m.milestone, to: rel(m.to) }));
669
+ output({ dry_run: dryRun, moved: shown, left }, raw, `${shown.length} phase director${shown.length === 1 ? 'y' : 'ies'} ${dryRun ? 'would be ' : ''}archived; ${left.length} left in place`);
670
+ }
377
671
  module.exports = {
378
672
  cmdRequirementsMarkComplete,
379
673
  cmdMilestoneComplete,
674
+ insertMilestoneRecord,
675
+ milestoneLabel,
380
676
  cmdPhasesClear,
677
+ cmdPhasesArchiveClosed,
381
678
  };
@@ -6,7 +6,7 @@
6
6
  * Unsupported in this router:
7
7
  * - scaffold: routed through top-level scaffold command.
8
8
  *
9
- * CJS-only subcommands: mvp-mode (dispatched directly, before hub).
9
+ * CJS-only subcommands: mvp-mode, dispatch-plan, install-run, tier, tier-propose (dispatched directly, before hub).
10
10
  *
11
11
  * #3788: dispatch is mediated by CommandRoutingHub. The public entry point
12
12
  * and observable CLI behaviour are unchanged.
@@ -51,6 +51,28 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
51
51
  phase.cmdPhaseMvpMode(cwd, args.slice(2), raw);
52
52
  return;
53
53
  }
54
+ // `dispatch-plan` (pack 9): read-only; the executor spawns, suite runs and
55
+ // tracking commits execute-phase obeys (src/dispatch-plan.cts).
56
+ if (subcommand === 'dispatch-plan') {
57
+ phase.cmdPhaseDispatchPlan(cwd, args.slice(2), raw);
58
+ return;
59
+ }
60
+ // `install-run` (pack 9): heavy-tier verification of the installed product —
61
+ // pack, install, init, run the phase's install checks (src/install-run.cts).
62
+ if (subcommand === 'install-run') {
63
+ phase.cmdPhaseInstallRun(cwd, args.slice(2), raw);
64
+ return;
65
+ }
66
+ // `tier` / `tier-propose` (pack 9): the phase's ceremony tier and its
67
+ // proposal (src/phase-tier.cts); `tier-propose --record` writes ROADMAP.
68
+ if (subcommand === 'tier') {
69
+ phase.cmdPhaseTier(cwd, args.slice(2), raw);
70
+ return;
71
+ }
72
+ if (subcommand === 'tier-propose') {
73
+ phase.cmdPhaseTierPropose(cwd, args.slice(2), raw);
74
+ return;
75
+ }
54
76
  // ── Build the CJS registry ──────────────────────────────────────────────────
55
77
  // Each handler receives a ctx object from the hub and must return a HubResult.
56
78
  const cjsRegistry = {
@@ -14,7 +14,7 @@
14
14
  * - node:fs / node:path (stdlib)
15
15
  * - ./phase-id.cjs (normalizePhaseName, phaseTokenMatches, extractPhaseToken)
16
16
  * - ./core-utils.cjs (readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath)
17
- * - ./planning-workspace.cjs (planningDir)
17
+ * - ./planning-workspace.cjs (planningDir, planningPaths)
18
18
  */
19
19
  var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -29,7 +29,7 @@ const coreUtilsModule = require("./core-utils.cjs");
29
29
  const { readSubdirectories, getPhaseFileStats, extractCanonicalPlanId, toPosixPath } = coreUtilsModule;
30
30
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
31
  const planningWorkspace = require("./planning-workspace.cjs");
32
- const { planningDir } = planningWorkspace;
32
+ const { planningDir, planningPaths } = planningWorkspace;
33
33
  // ─── Phase search helpers ─────────────────────────────────────────────────────
34
34
  function searchPhaseInDir(baseDir, relBase, normalized) {
35
35
  try {
@@ -83,9 +83,15 @@ function findPhaseInternal(cwd, phase) {
83
83
  const current = searchPhaseInDir(phasesDir, relPhasesDir, normalized);
84
84
  if (current)
85
85
  return current;
86
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
86
+ // CARRY-05: the archive directory is planningPaths' to name, not ours. The
87
+ // hardcoded path.join(cwd, '.planning', 'milestones') that stood here
88
+ // bypassed the workspace helper entirely, so a phase archived inside a
89
+ // workstream was unreachable from every verb that resolves through
90
+ // findPhaseInternal.
91
+ const milestonesDir = planningPaths(cwd).milestonesArchive;
87
92
  if (!node_fs_1.default.existsSync(milestonesDir))
88
93
  return null;
94
+ const relMilestonesDir = node_path_1.default.relative(cwd, milestonesDir);
89
95
  try {
90
96
  const milestoneEntries = node_fs_1.default.readdirSync(milestonesDir, { withFileTypes: true });
91
97
  const archiveDirs = milestoneEntries
@@ -97,7 +103,7 @@ function findPhaseInternal(cwd, phase) {
97
103
  const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
98
104
  const version = versionMatch[1];
99
105
  const archivePath = node_path_1.default.join(milestonesDir, archiveName);
100
- const relBase = '.planning/milestones/' + archiveName;
106
+ const relBase = toPosixPath(node_path_1.default.join(relMilestonesDir, archiveName));
101
107
  const result = searchPhaseInDir(archivePath, relBase, normalized);
102
108
  if (result) {
103
109
  result.archived = version;
@@ -109,7 +115,10 @@ function findPhaseInternal(cwd, phase) {
109
115
  return null;
110
116
  }
111
117
  function getArchivedPhaseDirs(cwd) {
112
- const milestonesDir = node_path_1.default.join(cwd, '.planning', 'milestones');
118
+ // CARRY-05: same owner as findPhaseInternal above and as the `milestone
119
+ // complete` writer — not a second hardcoded copy.
120
+ const milestonesDir = planningPaths(cwd).milestonesArchive;
121
+ const relMilestonesDir = node_path_1.default.relative(cwd, milestonesDir);
113
122
  const results = [];
114
123
  if (!node_fs_1.default.existsSync(milestonesDir))
115
124
  return results;
@@ -129,7 +138,7 @@ function getArchivedPhaseDirs(cwd) {
129
138
  results.push({
130
139
  name: dir,
131
140
  milestone: version,
132
- basePath: node_path_1.default.join('.planning', 'milestones', archiveName),
141
+ basePath: node_path_1.default.join(relMilestonesDir, archiveName),
133
142
  fullPath: node_path_1.default.join(archivePath, dir),
134
143
  });
135
144
  }