pan-wizard 3.33.0 → 3.35.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 (214) hide show
  1. package/README.md +29 -26
  2. package/agents/pan-conductor.md +18 -17
  3. package/agents/pan-counterfactual.md +2 -2
  4. package/agents/pan-debugger.md +30 -9
  5. package/agents/pan-design-checker.md +2 -1
  6. package/agents/pan-designer.md +2 -2
  7. package/agents/pan-distiller.md +3 -3
  8. package/agents/pan-document_code.md +14 -14
  9. package/agents/pan-executor.md +30 -9
  10. package/agents/pan-experiment-runner.md +9 -8
  11. package/agents/pan-hardener.md +1 -1
  12. package/agents/pan-integration-checker.md +2 -2
  13. package/agents/pan-knowledge.md +1 -1
  14. package/agents/pan-meta-reviewer.md +4 -4
  15. package/agents/pan-optimizer.md +39 -43
  16. package/agents/pan-phase-researcher.md +10 -10
  17. package/agents/pan-plan-checker.md +37 -34
  18. package/agents/pan-planner.md +57 -44
  19. package/agents/pan-previewer.md +1 -1
  20. package/agents/pan-project-researcher.md +12 -12
  21. package/agents/pan-release.md +1 -1
  22. package/agents/pan-research-synthesizer.md +3 -3
  23. package/agents/pan-reviewer.md +26 -3
  24. package/agents/pan-roadmapper.md +15 -14
  25. package/agents/pan-verifier.md +33 -25
  26. package/bin/install-lib.cjs +176 -38
  27. package/bin/install.js +81 -63
  28. package/commands/pan/army.md +15 -11
  29. package/commands/pan/assumptions.md +2 -2
  30. package/commands/pan/audit-deployment.md +11 -12
  31. package/commands/pan/cost.md +8 -8
  32. package/commands/pan/dashboard.md +0 -3
  33. package/commands/pan/debug.md +10 -7
  34. package/commands/pan/design-phase.md +5 -5
  35. package/commands/pan/discuss-phase.md +2 -0
  36. package/commands/pan/exec-phase.md +21 -37
  37. package/commands/pan/experiment.md +9 -8
  38. package/commands/pan/focus-auto.md +22 -20
  39. package/commands/pan/focus-design.md +7 -6
  40. package/commands/pan/focus-doc-audit.md +4 -3
  41. package/commands/pan/focus-drift-walking.md +4 -3
  42. package/commands/pan/focus-exec.md +6 -6
  43. package/commands/pan/focus-plan.md +12 -11
  44. package/commands/pan/focus-scan.md +5 -3
  45. package/commands/pan/focus-sync.md +3 -2
  46. package/commands/pan/git.md +8 -8
  47. package/commands/pan/health.md +2 -2
  48. package/commands/pan/hud.md +4 -7
  49. package/commands/pan/hygiene.md +14 -7
  50. package/commands/pan/knowledge.md +9 -10
  51. package/commands/pan/learn.md +10 -7
  52. package/commands/pan/links.md +4 -1
  53. package/commands/pan/map-codebase.md +1 -1
  54. package/commands/pan/mcp-bridge.md +19 -21
  55. package/commands/pan/milestone-done.md +6 -3
  56. package/commands/pan/milestone-new.md +1 -1
  57. package/commands/pan/new-project.md +10 -10
  58. package/commands/pan/optimize.md +18 -14
  59. package/commands/pan/patches.md +2 -6
  60. package/commands/pan/pause.md +1 -1
  61. package/commands/pan/phase-tests.md +3 -2
  62. package/commands/pan/plan-phase.md +9 -19
  63. package/commands/pan/preview.md +2 -3
  64. package/commands/pan/profile.md +2 -2
  65. package/commands/pan/quick.md +1 -1
  66. package/commands/pan/remove-phase.md +1 -1
  67. package/commands/pan/report.md +3 -7
  68. package/commands/pan/research-phase.md +16 -15
  69. package/commands/pan/resume.md +9 -11
  70. package/commands/pan/retro.md +5 -3
  71. package/commands/pan/review-deep.md +7 -9
  72. package/commands/pan/settings.md +1 -1
  73. package/commands/pan/verify-phase.md +0 -11
  74. package/commands/pan/what-if.md +3 -4
  75. package/hooks/dist/pan-context-monitor.js +352 -44
  76. package/hooks/dist/pan-cost-logger.js +30 -0
  77. package/hooks/dist/pan-guard.js +244 -0
  78. package/hooks/dist/pan-statusline.js +73 -24
  79. package/hooks/dist/pan-stop-guard.js +7 -2
  80. package/package.json +1 -1
  81. package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
  82. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  83. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  84. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  85. package/pan-wizard-core/bin/lib/commands.cjs +57 -6
  86. package/pan-wizard-core/bin/lib/config.cjs +7 -1
  87. package/pan-wizard-core/bin/lib/constants.cjs +18 -6
  88. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  89. package/pan-wizard-core/bin/lib/core.cjs +53 -19
  90. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  91. package/pan-wizard-core/bin/lib/cost.cjs +142 -13
  92. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  93. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  94. package/pan-wizard-core/bin/lib/findings.cjs +135 -21
  95. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  96. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  97. package/pan-wizard-core/bin/lib/hygiene.cjs +184 -17
  98. package/pan-wizard-core/bin/lib/init.cjs +13 -4
  99. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  100. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  101. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  102. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  103. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  104. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  105. package/pan-wizard-core/bin/lib/optimize.cjs +97 -17
  106. package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
  107. package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
  108. package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
  109. package/pan-wizard-core/bin/lib/phase.cjs +39 -13
  110. package/pan-wizard-core/bin/lib/preview.cjs +34 -31
  111. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  112. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  113. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  114. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  115. package/pan-wizard-core/bin/lib/state.cjs +43 -28
  116. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  117. package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
  118. package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
  119. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
  120. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
  121. package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
  122. package/pan-wizard-core/bin/lib/verify-scope.cjs +11 -7
  123. package/pan-wizard-core/bin/lib/verify.cjs +30 -28
  124. package/pan-wizard-core/bin/lib/whatif.cjs +53 -9
  125. package/pan-wizard-core/bin/pan-tools.cjs +114 -19
  126. package/pan-wizard-core/learnings/index.json +5 -5
  127. package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
  128. package/pan-wizard-core/mcp/native-tools.cjs +3 -3
  129. package/pan-wizard-core/opencode/pan-wizard.js +139 -0
  130. package/pan-wizard-core/references/checkpoints.md +3 -3
  131. package/pan-wizard-core/references/continuation-format.md +3 -3
  132. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  133. package/pan-wizard-core/references/design-methodology.md +1 -1
  134. package/pan-wizard-core/references/git-integration.md +4 -6
  135. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  136. package/pan-wizard-core/references/guardrails.md +2 -2
  137. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  138. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  139. package/pan-wizard-core/references/model-profiles.md +15 -10
  140. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  141. package/pan-wizard-core/references/planning-config.md +7 -7
  142. package/pan-wizard-core/references/tdd.md +10 -1
  143. package/pan-wizard-core/references/verdict-contract.md +6 -1
  144. package/pan-wizard-core/references/verification-patterns.md +5 -5
  145. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  146. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  147. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  148. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  149. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  150. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  151. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  152. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  153. package/pan-wizard-core/templates/config.json +2 -6
  154. package/pan-wizard-core/templates/continue-here.md +1 -1
  155. package/pan-wizard-core/templates/debug-subagent-prompt.md +5 -3
  156. package/pan-wizard-core/templates/debug.md +15 -1
  157. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  158. package/pan-wizard-core/templates/milestone.md +1 -1
  159. package/pan-wizard-core/templates/phase-prompt.md +31 -17
  160. package/pan-wizard-core/templates/planner-subagent-prompt.md +5 -5
  161. package/pan-wizard-core/templates/preview-report.md +1 -1
  162. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  163. package/pan-wizard-core/templates/research-project/features.md +1 -1
  164. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  165. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  166. package/pan-wizard-core/templates/research.md +5 -5
  167. package/pan-wizard-core/templates/retrospective.md +1 -1
  168. package/pan-wizard-core/templates/roadmap.md +15 -15
  169. package/pan-wizard-core/templates/state.md +4 -4
  170. package/pan-wizard-core/templates/summary-complex.md +1 -0
  171. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  172. package/pan-wizard-core/templates/summary-standard.md +2 -1
  173. package/pan-wizard-core/templates/summary.md +18 -2
  174. package/pan-wizard-core/templates/uat.md +3 -3
  175. package/pan-wizard-core/templates/user-setup.md +8 -8
  176. package/pan-wizard-core/templates/verification-report.md +53 -8
  177. package/pan-wizard-core/workflows/add-phase.md +1 -1
  178. package/pan-wizard-core/workflows/assumptions.md +1 -1
  179. package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
  180. package/pan-wizard-core/workflows/discuss-phase.md +10 -8
  181. package/pan-wizard-core/workflows/exec-phase.md +75 -95
  182. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  183. package/pan-wizard-core/workflows/health.md +13 -12
  184. package/pan-wizard-core/workflows/help.md +8 -9
  185. package/pan-wizard-core/workflows/learn.md +13 -11
  186. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  187. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  188. package/pan-wizard-core/workflows/milestone-gaps.md +18 -25
  189. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  190. package/pan-wizard-core/workflows/new-project.md +17 -25
  191. package/pan-wizard-core/workflows/optimize.md +11 -9
  192. package/pan-wizard-core/workflows/pause.md +6 -1
  193. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  194. package/pan-wizard-core/workflows/plan-phase.md +53 -42
  195. package/pan-wizard-core/workflows/profile.md +2 -2
  196. package/pan-wizard-core/workflows/progress.md +6 -8
  197. package/pan-wizard-core/workflows/quick.md +7 -5
  198. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  199. package/pan-wizard-core/workflows/research-phase.md +9 -5
  200. package/pan-wizard-core/workflows/resume-project.md +4 -6
  201. package/pan-wizard-core/workflows/retro.md +3 -1
  202. package/pan-wizard-core/workflows/settings.md +16 -24
  203. package/pan-wizard-core/workflows/todo-check.md +4 -3
  204. package/pan-wizard-core/workflows/transition.md +16 -12
  205. package/pan-wizard-core/workflows/update.md +25 -17
  206. package/pan-wizard-core/workflows/verify-phase.md +22 -20
  207. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  208. package/pan-zcode/README.md +7 -7
  209. package/pan-zcode/bin/install-zcode.js +1 -1
  210. package/scripts/build-hooks.js +1 -0
  211. package/scripts/build-plugin.js +3 -1
  212. package/scripts/generate-skills-docs.py +15 -6
  213. package/scripts/prompt-lint.cjs +167 -0
  214. package/scripts/test-surface.cjs +1 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { safeReadFile, output, error, escapeRegex } = require('./core.cjs');
7
+ const { safeReadFile, output, error, escapeRegex, toLf, dominantEol, withEol } = require('./core.cjs');
8
8
  const { FIELD_VALUE_RE, PRIORITY_LEVELS, EFFORT_SIZES } = require('./constants.cjs');
9
9
 
10
10
  // --- Inline array rendering thresholds -------------------------------------------
@@ -169,7 +169,21 @@ function renderYamlEntry(lines, key, value, depth) {
169
169
  lines.push(`${indent}${key}:`);
170
170
  const itemIndent = ' '.repeat(depth + 1);
171
171
  for (const item of value) {
172
- lines.push(`${itemIndent}- ${quoteIfNeeded(item)}`);
172
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
173
+ // A list of maps (a plan's must_haves artifacts and key_links): the first
174
+ // key on the dash line, the rest under it. Written as a scalar, each item
175
+ // came out "[object Object]".
176
+ const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined);
177
+ entries.forEach(([k, v], idx) => {
178
+ // An array value goes out as a flow sequence of JSON strings, which the
179
+ // parser reads back as an array (`exports: ["GET", "POST"]`).
180
+ const text = Array.isArray(v) ? `[${v.map((x) => JSON.stringify(String(x))).join(', ')}]`
181
+ : quoteIfNeeded(typeof v === 'object' ? JSON.stringify(v) : v);
182
+ lines.push(`${itemIndent}${idx === 0 ? '- ' : ' '}${k}: ${text}`);
183
+ });
184
+ } else {
185
+ lines.push(`${itemIndent}- ${quoteIfNeeded(item)}`);
186
+ }
173
187
  }
174
188
  }
175
189
  } else if (typeof value === 'object') {
@@ -201,34 +215,76 @@ function quoteIfNeeded(value) {
201
215
  */
202
216
  function spliceFrontmatter(content, newObj) {
203
217
  const yamlStr = reconstructFrontmatter(newObj);
204
- const match = content.match(/^---\n[\s\S]+?\n---/);
218
+ // Match on LF and give the file back in its own ending. The LF-only match
219
+ // missed the block in a CRLF file, so `frontmatter set` and `merge` stacked a
220
+ // second frontmatter on top of the first instead of replacing it.
221
+ const eol = dominantEol(content);
222
+ const text = toLf(content);
223
+ const match = text.match(/^---\n[\s\S]+?\n---/);
205
224
  if (match) {
206
- return `---\n${yamlStr}\n---` + content.slice(match[0].length);
225
+ return withEol(`---\n${yamlStr}\n---` + text.slice(match[0].length), eol);
226
+ }
227
+ return withEol(`---\n${yamlStr}\n---\n\n` + text, eol);
228
+ }
229
+
230
+ /**
231
+ * A YAML scalar as written in a must_haves value: a double-quoted one unescapes `\\`
232
+ * and `\"`, a single-quoted one `''`, a plain one is taken as written. The raw `\\.`
233
+ * of the shipped key-link example reached `new RegExp` in `verify key-links`, so
234
+ * `pattern: "prisma\\.message\\.(find|create)"` never matched.
235
+ * @param {string} raw - The value text after `key:` (or after `- `)
236
+ * @returns {string}
237
+ */
238
+ function yamlScalar(raw) {
239
+ const s = String(raw).trim();
240
+ // A flow sequence (`exports: ["GET", "POST"]`, as the plan template writes it) is an
241
+ // array of scalars: taken whole it was one string, and `verify artifacts` then
242
+ // reported every export missing.
243
+ if (s.length >= 2 && s.startsWith('[') && s.endsWith(']')) {
244
+ const items = [];
245
+ let cur = '';
246
+ let quote = null;
247
+ const inner = s.slice(1, -1);
248
+ for (let i = 0; i < inner.length; i++) {
249
+ const c = inner[i];
250
+ if (quote) {
251
+ cur += c;
252
+ if (c === '\\' && quote === '"' && i + 1 < inner.length) cur += inner[++i];
253
+ else if (c === quote) quote = null;
254
+ } else if (c === '"' || c === "'") { quote = c; cur += c; }
255
+ else if (c === ',') { items.push(cur); cur = ''; }
256
+ else cur += c;
257
+ }
258
+ items.push(cur);
259
+ return items.map((x) => yamlScalar(x)).filter((x) => x !== '');
207
260
  }
208
- return `---\n${yamlStr}\n---\n\n` + content;
261
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\(["\\])/g, '$1');
262
+ if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1).replace(/''/g, "'");
263
+ return s;
209
264
  }
210
265
 
211
266
  /**
212
267
  * Parse a specific block (artifacts, key_links, truths) from must_haves in raw YAML frontmatter.
213
268
  *
214
- * Block structure being parsed:
215
- * The must_haves section is a 3-level nested YAML structure:
216
- * must_haves: (level 1 -- 2-space indent)
217
- * artifacts: (level 2 -- 4-space indent, the blockName)
218
- * - path: foo (level 3 -- 6-space indent, list items)
219
- * provides: bar (level 3+ -- 8-space indent, continuation k-v pairs)
269
+ * Block structure being parsed (indents are relative, never fixed columns):
270
+ * must_haves:
271
+ * artifacts: (the blockName, deeper than must_haves)
272
+ * - path: foo (list items)
273
+ * provides: bar (continuation k-v pairs, deeper than the item's dash)
220
274
  *
221
- * This function finds the named block at the 4-space level, then parses
222
- * its list items (at 6-space indent) and their nested key-value pairs
223
- * (at 8+ space indent) into an array of objects or strings.
275
+ * This function finds the named block under must_haves, then parses its list
276
+ * items and their nested key-value pairs into an array of objects or strings.
277
+ * Values go through yamlScalar.
224
278
  *
225
279
  * @param {string} content - Full markdown content with frontmatter
226
280
  * @param {string} blockName - Block name to extract (e.g., "artifacts", "key_links")
227
281
  * @returns {Array} Parsed array of block items (objects or strings)
228
282
  */
229
283
  function parseMustHavesBlock(content, blockName) {
230
- // Extract raw YAML between --- delimiters
231
- const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
284
+ // Extract raw YAML between --- delimiters. On LF: a CRLF plan (every Windows
285
+ // field project) matched nothing here, so its must_haves read as empty and the
286
+ // reconcile gate below trusted the verdict again, as it did before the indent fix.
287
+ const fmMatch = toLf(content).match(/^---\n([\s\S]+?)\n---/);
232
288
  if (!fmMatch) return [];
233
289
 
234
290
  const yaml = fmMatch[1];
@@ -290,18 +346,18 @@ function parseMustHavesBlock(content, blockName) {
290
346
 
291
347
  const body = dashMatch[2];
292
348
  // A plain string item — no `key: value` shape.
293
- const kvMatch = body.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
349
+ const kvMatch = body.match(/^(\w+):\s*(.*?)\s*$/);
294
350
  if (kvMatch) {
295
351
  currentItem = {};
296
- currentItem[kvMatch[1]] = kvMatch[2];
352
+ currentItem[kvMatch[1]] = yamlScalar(kvMatch[2]);
297
353
  } else {
298
- currentItem = body.replace(/^"|"$/g, '').trim();
354
+ currentItem = yamlScalar(body);
299
355
  }
300
356
  } else if (currentItem && typeof currentItem === 'object') {
301
357
  // Continuation key-value: deeper than the item's dash (properties of it)
302
- const kvMatch = line.match(/^\s+(\w+):\s*"?([^"]*)"?\s*$/);
358
+ const kvMatch = line.match(/^\s+(\w+):\s*(.*?)\s*$/);
303
359
  if (kvMatch) {
304
- const val = kvMatch[2];
360
+ const val = yamlScalar(kvMatch[2]);
305
361
  // Coerce pure-integer strings to numbers for convenience
306
362
  currentItem[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
307
363
  }
@@ -309,7 +365,7 @@ function parseMustHavesBlock(content, blockName) {
309
365
  // already deeper than the list-item dash (a shallower one would have been
310
366
  // treated as a new item above), so relative depth alone identifies it —
311
367
  // no fixed column.
312
- const arrMatch = line.match(/^\s+-\s+"?([^"]+)"?\s*$/);
368
+ const arrMatch = line.match(/^\s+-\s+(.+?)\s*$/);
313
369
  if (arrMatch) {
314
370
  // Convert the most recently added key's scalar value into an array,
315
371
  // then append this item to that array
@@ -318,7 +374,7 @@ function parseMustHavesBlock(content, blockName) {
318
374
  if (lastKey && !Array.isArray(currentItem[lastKey])) {
319
375
  currentItem[lastKey] = currentItem[lastKey] ? [currentItem[lastKey]] : [];
320
376
  }
321
- if (lastKey) currentItem[lastKey].push(arrMatch[1]);
377
+ if (lastKey) currentItem[lastKey].push(yamlScalar(arrMatch[1]));
322
378
  }
323
379
  }
324
380
  }
@@ -354,6 +410,27 @@ const FRONTMATTER_SCHEMAS = {
354
410
  verification: { required: ['phase', 'verified', 'status', 'score'] },
355
411
  };
356
412
 
413
+ /**
414
+ * extractFrontmatter cannot read a list of maps: each must_haves artifact and key
415
+ * link came back as the string of its first line (`path: "src/x.ts`), so the plan
416
+ * checker and verify-phase, which read them through `frontmatter get`, got no
417
+ * objects. parseMustHavesBlock reads both blocks by relative indent.
418
+ * @param {object} fm - extractFrontmatter output
419
+ * @param {string} content - the file it came from
420
+ * @returns {object} fm, with must_haves.artifacts and .key_links as objects
421
+ */
422
+ function withParsedMustHaves(fm, content) {
423
+ const mh = fm && fm.must_haves;
424
+ if (!mh || typeof mh !== 'object' || Array.isArray(mh)) return fm;
425
+ const fixed = { ...mh };
426
+ for (const block of ['artifacts', 'key_links']) {
427
+ if (!(block in fixed)) continue;
428
+ const parsed = parseMustHavesBlock(content, block);
429
+ if (parsed.length) fixed[block] = parsed;
430
+ }
431
+ return { ...fm, must_haves: fixed };
432
+ }
433
+
357
434
  /**
358
435
  * Get frontmatter from a file, optionally filtered to a single field.
359
436
  * @param {string} cwd - Working directory path
@@ -367,7 +444,7 @@ function cmdFrontmatterGet(cwd, filePath, field, raw) {
367
444
  const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
368
445
  const content = safeReadFile(fullPath);
369
446
  if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
370
- const fm = extractFrontmatter(content);
447
+ const fm = withParsedMustHaves(extractFrontmatter(content), content);
371
448
  if (field) {
372
449
  const value = fm[field];
373
450
  if (value === undefined) { output({ error: 'Field not found', field }, raw); return; }
@@ -395,7 +472,9 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
395
472
  } catch {
396
473
  output({ error: 'File not found', path: filePath }, raw); return;
397
474
  }
398
- const fm = extractFrontmatter(content);
475
+ // The block is rewritten whole, so a plan's must_haves lists of maps must be read
476
+ // as maps: extractFrontmatter alone flattened each item to its first line.
477
+ const fm = withParsedMustHaves(extractFrontmatter(content), content);
399
478
  let parsedValue;
400
479
  // Attempt JSON parse so callers can pass structured values (arrays, objects);
401
480
  // fall back to raw string if the value is not valid JSON
@@ -427,7 +506,7 @@ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
427
506
  } catch {
428
507
  output({ error: 'File not found', path: filePath }, raw); return;
429
508
  }
430
- const fm = extractFrontmatter(content);
509
+ const fm = withParsedMustHaves(extractFrontmatter(content), content);
431
510
  let mergeData;
432
511
  // Parse the JSON data string; abort with error if the caller passed invalid JSON
433
512
  try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
@@ -40,10 +40,12 @@ const {
40
40
  HYGIENE_TMP_AGE_MS,
41
41
  CHARS_PER_TOKEN,
42
42
  STATE_FILE,
43
+ ROADMAP_FILE,
44
+ REQUIREMENTS_FILE,
43
45
  } = require('./constants.cjs');
44
46
  const { planningPath, planningRel, detectPlanningModel } = require('./utils.cjs');
45
47
  const { detectForeignPlanningTree } = require('./foreign-planning.cjs');
46
- const { listMemoryAgents, readMemory, compactMemory } = require('./memory.cjs');
48
+ const { listMemoryAgents, readMemory, compactMemory, pruneMemory, RESERVED_MEMORY_NAMES } = require('./memory.cjs');
47
49
  const { readRecords, isSuspectRecord, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
48
50
  const { assessCacheTtl } = require('./context-budget.cjs');
49
51
  const { planningRootRel, planningRoots, withPlanningRoot, describePlanningRoot, TRACKS_DIR } = require('./planning-root.cjs');
@@ -223,22 +225,137 @@ function checkTmpOrphans(cwd, now = Date.now()) {
223
225
  return { findings };
224
226
  }
225
227
 
226
- /** H-4: per-agent memory logs past the entry cap (compaction never ran). */
228
+ /**
229
+ * H-4: per-agent memory logs — past the entry cap (compaction never ran), holding
230
+ * entries `memory select` no longer returns (O4: cited code gone, or unused past the
231
+ * expiry window), and files in the folder that are not read as memory at all.
232
+ */
227
233
  function checkMemoryLogs(cwd) {
228
234
  const findings = [];
229
235
  const { agents } = listMemoryAgents(cwd);
230
236
  for (const a of agents) {
231
237
  const mem = readMemory(cwd, a.agent);
232
238
  if (!mem || !Array.isArray(mem.entries)) continue;
233
- if (mem.entries.length <= MEMORY_ENTRY_CAP) continue;
234
- findings.push(mkFinding('memory-bloat', 'warn',
235
- planningRel('memory', `${a.agent}.md`),
236
- `${mem.entries.length} entries exceeds cap ${MEMORY_ENTRY_CAP} — whole-file reads flood context`,
237
- { action: 'compact-memory', agent: a.agent }));
239
+ const rel = planningRel('memory', `${a.agent}.md`);
240
+ if (mem.entries.length > MEMORY_ENTRY_CAP) {
241
+ findings.push(mkFinding('memory-bloat', 'warn', rel,
242
+ `${mem.entries.length} entries exceeds cap ${MEMORY_ENTRY_CAP} — compaction keeps the newest`,
243
+ { action: 'compact-memory', agent: a.agent }));
244
+ }
245
+ const prune = pruneMemory(cwd, a.agent);
246
+ const stale = prune.agents ? prune.agents[0].archive : [];
247
+ if (stale.length) {
248
+ const gone = stale.filter(x => x.reason.startsWith('cited code gone')).length;
249
+ findings.push(mkFinding('memory-stale', 'warn', rel,
250
+ `${stale.length} entr${stale.length === 1 ? 'y is' : 'ies are'} stale or expired `
251
+ + `(${gone} cite code that is gone, ${stale.length - gone} unused for ${prune.expire_days} days) — `
252
+ + '`memory prune` archives them',
253
+ { action: 'prune-memory', agent: a.agent }));
254
+ }
255
+ }
256
+ // A file in the folder with no `## Entries` list is never read as memory (PAN's
257
+ // own archives aside): say so rather than let it look like memory.
258
+ for (const n of listMemoryAgents(cwd).not_loaded || []) {
259
+ if (RESERVED_MEMORY_NAMES.includes(n.file.slice(0, -3).toLowerCase())) continue;
260
+ findings.push(mkFinding('memory-format', 'info', planningRel('memory', n.file),
261
+ `${n.reason}, so \`memory select\` and \`/pan:knowledge\` do not read it as memory — move the rules that still hold into an agent log with \`memory append <agent> "<rule>" --cites <path>\``,
262
+ null));
238
263
  }
239
264
  return { findings };
240
265
  }
241
266
 
267
+ // ─── Host memory (memory optimisation O10) ───────────────────────────────────
268
+ //
269
+ // Cross-session memory accumulates in the host, not in PAN (F7): Claude Code's auto
270
+ // memory keeps an index, MEMORY.md, and loads only its first 200 lines or 25 KB at
271
+ // the start of every session. PAN does not own that store (MI-010) and never writes
272
+ // it; it reads the index and reports what the host will silently cut, and index
273
+ // lines that hold content where a one-line pointer belongs.
274
+
275
+ const HOST_INDEX_MAX_LINES = 200;
276
+ const HOST_INDEX_MAX_BYTES = 25000; // "25KB": the smaller reading, so a warning is never late
277
+ const HOST_INDEX_LINE_MAX_CHARS = 300; // a pointer line is ~100–150 characters
278
+
279
+ /** The directory Claude Code keys a project's auto memory by: the git repository root, the main one for a worktree. */
280
+ function hostMemoryProjectRoot(cwd) {
281
+ let dir = path.resolve(cwd);
282
+ for (;;) {
283
+ const dotGit = path.join(dir, '.git');
284
+ let st = null;
285
+ try { st = fs.statSync(dotGit); } catch { /* keep walking */ }
286
+ if (st && st.isDirectory()) return dir;
287
+ if (st && st.isFile()) {
288
+ // A worktree: `.git` is a file naming <main>/.git/worktrees/<name>.
289
+ const m = (safeReadFile(dotGit) || '').match(/^gitdir:\s*(.+?)\s*$/m);
290
+ if (m) {
291
+ const gitdir = path.resolve(dir, m[1]);
292
+ const i = gitdir.split(path.sep).lastIndexOf('.git');
293
+ if (i > 0 && gitdir.split(path.sep)[i + 1] === 'worktrees') return gitdir.split(path.sep).slice(0, i).join(path.sep);
294
+ }
295
+ return dir;
296
+ }
297
+ const up = path.dirname(dir);
298
+ if (up === dir) return path.resolve(cwd);
299
+ dir = up;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Claude Code's auto-memory directory for a project, and how it was found.
305
+ * `configDir` is Claude Code's user directory when CLAUDE_CONFIG_DIR moves it.
306
+ */
307
+ function hostMemoryDir(cwd, homeDir, configDir = '') {
308
+ const claudeDir = '.claude';
309
+ const userDir = configDir || path.join(homeDir, claudeDir);
310
+ for (const f of [path.join(cwd, claudeDir, 'settings.local.json'), path.join(cwd, claudeDir, 'settings.json'), path.join(userDir, 'settings.json')]) {
311
+ let s = null;
312
+ try { s = JSON.parse(safeReadFile(f) || 'null'); } catch { /* unreadable settings */ }
313
+ if (s && typeof s.autoMemoryDirectory === 'string' && s.autoMemoryDirectory.trim()) {
314
+ const v = s.autoMemoryDirectory.trim();
315
+ return { dir: path.resolve(v.startsWith('~/') ? path.join(homeDir, v.slice(2)) : v), display: v, source: 'autoMemoryDirectory' };
316
+ }
317
+ }
318
+ const key = hostMemoryProjectRoot(cwd).replace(/[^A-Za-z0-9]/g, '-');
319
+ if (configDir) return { dir: path.join(configDir, 'projects', key, 'memory'), display: toPosix(path.join(configDir, 'projects', key, 'memory')), source: 'default' };
320
+ return { dir: path.join(homeDir, claudeDir, 'projects', key, 'memory'), display: `~/${claudeDir}/projects/${key}/memory`, source: 'default' };
321
+ }
322
+
323
+ /**
324
+ * H-11: Claude Code's auto-memory index. Read-only; never fixable by PAN.
325
+ * @param {string} cwd
326
+ * @param {{homeDir?: string, configDir?: string}} [opts] - a given homeDir (a test seam) ignores CLAUDE_CONFIG_DIR
327
+ */
328
+ function checkHostMemory(cwd, opts = {}) {
329
+ const findings = [];
330
+ const home = opts.homeDir || require('os').homedir();
331
+ const configDir = opts.configDir !== undefined ? opts.configDir : (opts.homeDir ? '' : (process.env.CLAUDE_CONFIG_DIR || ''));
332
+ const loc = hostMemoryDir(cwd, home, configDir);
333
+ const text = safeReadFile(path.join(loc.dir, 'MEMORY.md'));
334
+ if (text == null) return { findings, host_memory: { dir: loc.display, index: false } };
335
+ const lines = text.replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n');
336
+ const bytes = Buffer.byteLength(text, 'utf8');
337
+ const rel = `${loc.display}/MEMORY.md`;
338
+ const overLines = lines.length > HOST_INDEX_MAX_LINES;
339
+ const overBytes = bytes > HOST_INDEX_MAX_BYTES;
340
+ if (overLines || overBytes) {
341
+ findings.push(mkFinding('host-memory', 'warn', rel,
342
+ `Claude Code's memory index is ${lines.length} lines / ${(bytes / 1024).toFixed(1)} KB; it loads only the first ${HOST_INDEX_MAX_LINES} lines or 25 KB at session start, so the rest is never seen. `
343
+ + 'Keep one line per memory there and move detail into topic files (PAN does not edit this store)',
344
+ null));
345
+ } else if (lines.length >= HOST_INDEX_MAX_LINES * 0.9 || bytes >= HOST_INDEX_MAX_BYTES * 0.9) {
346
+ findings.push(mkFinding('host-memory', 'info', rel,
347
+ `Claude Code's memory index is ${lines.length} lines / ${(bytes / 1024).toFixed(1)} KB, near the ${HOST_INDEX_MAX_LINES}-line / 25 KB load limit`,
348
+ null));
349
+ }
350
+ const long = lines.filter(l => l.length > HOST_INDEX_LINE_MAX_CHARS);
351
+ if (long.length) {
352
+ findings.push(mkFinding('host-memory', 'info', rel,
353
+ `${long.length} index line${long.length === 1 ? ' holds' : 's hold'} content instead of a pointer (longest ${Math.max(...long.map(l => l.length))} characters; a pointer is one short line to a topic file)`,
354
+ null));
355
+ }
356
+ return { findings, host_memory: { dir: loc.display, source: loc.source, index: true, lines: lines.length, bytes, long_lines: long.length } };
357
+ }
358
+
242
359
  /**
243
360
  * Thousands separator that does not depend on the host locale. `toLocaleString()`
244
361
  * emits a narrow no-break space in some locales and a comma in others, which
@@ -407,17 +524,18 @@ function checkCachedContext(cwd) {
407
524
  try { cached = buildCachedContext(cwd); } catch { return { findings }; }
408
525
  if (!cached || !Array.isArray(cached.blocks)) return { findings };
409
526
 
410
- // An empty block is not "small" — it means this project gets NO prompt
411
- // caching at all, which is worth saying out loud rather than reporting as a
412
- // healthy zero. But only for a tree that HAS planning content: a freshly
413
- // scaffolded `.planning/phases/` has nothing to cache yet, and reporting that
414
- // as a finding is noise on every new project. The signal is "you have
415
- // planning docs and none of them are cached", not "you have no docs".
527
+ // An empty block is not "small" — it means this check measures nothing, which
528
+ // is worth saying rather than reporting a healthy zero. (PAN primes no cache,
529
+ // ADR-0023 amended: the host caches each prompt prefix by itself, so this is
530
+ // about measurement, not about caching.) But only for a tree that HAS planning
531
+ // content: a freshly scaffolded `.planning/phases/` has nothing to measure yet,
532
+ // and reporting that is noise on every new project.
416
533
  if (cached.blocks.length === 0) {
417
534
  if (planningDocCount(cwd) > 0) {
418
535
  findings.push(mkFinding('cache-context', 'info', planningRel(),
419
- 'planning docs exist but none are cacheable — every agent call re-sends its context uncached. '
420
- + 'Add project.md/standards.md, or list stable docs under config.json cache.extra_files',
536
+ 'planning docs exist but none of the stable files this check measures (project.md, requirements.md, '
537
+ + 'roadmap.md, state.md, standards.md), so it has nothing to read. List stable docs under config.json '
538
+ + 'cache.extra_files to measure them',
421
539
  null));
422
540
  }
423
541
  return { findings };
@@ -429,7 +547,8 @@ function checkCachedContext(cwd) {
429
547
  findings.push(mkFinding('cache-context', severity, planningRel(),
430
548
  `cached context block is ~${fmtTokens(blockTokens)} tokens across ${cached.blocks.length} file(s) `
431
549
  + `(warn ${fmtTokens(CACHE_BLOCK_WARN_TOKENS)}, critical ${fmtTokens(CACHE_BLOCK_CRIT_TOKENS)}) — `
432
- + 're-read on every agent call, so this is the project\'s largest recurring cost',
550
+ + 'state.md is re-read on every agent call; roadmap.md and requirements.md whole by the roadmapper and milestone work '
551
+ + '(phase agents read `roadmap slice`)',
433
552
  null));
434
553
  }
435
554
 
@@ -437,6 +556,27 @@ function checkCachedContext(cwd) {
437
556
  const tokens = Math.ceil((b.content || '').length / CHARS_PER_TOKEN);
438
557
  if (tokens < CACHE_FILE_WARN_TOKENS) continue;
439
558
  const isState = String(b.path).endsWith(STATE_FILE);
559
+ const isRoadmap = String(b.path).endsWith(ROADMAP_FILE);
560
+ const isRequirements = String(b.path).endsWith(REQUIREMENTS_FILE);
561
+
562
+ // The roadmap and requirements are read whole only by the roadmapper and the
563
+ // milestone workflows now; phase agents get `roadmap slice` (O2). A roadmap
564
+ // with shipped phases past the most recent ones can be compacted (O3).
565
+ if (isRoadmap || isRequirements) {
566
+ let fix = null;
567
+ let detail = `~${fmtTokens(tokens)} tokens, read whole by the roadmapper and milestone work (phase agents read \`roadmap slice\`)`;
568
+ if (isRoadmap) {
569
+ const archivable = roadmapCompactionAvailable(cwd);
570
+ if (archivable > 0) {
571
+ fix = { action: 'compact-roadmap' };
572
+ detail += ` — ${archivable} shipped phase section(s) can move to roadmap-history.md (\`roadmap compact\`)`;
573
+ }
574
+ } else {
575
+ detail += ' — `/pan:milestone-done` archives a shipped milestone\'s requirements to `milestones/` and starts the next milestone\'s file fresh';
576
+ }
577
+ findings.push(mkFinding('cache-context', 'warn', b.path, detail, fix));
578
+ continue;
579
+ }
440
580
 
441
581
  // A finding may only advertise `auto-fixable` when running the fix would
442
582
  // actually change something. state.md stays over the threshold once its
@@ -491,6 +631,12 @@ function stateCompactionAvailable(cwd) {
491
631
  }
492
632
  }
493
633
 
634
+ /** Same rule for roadmap.md: offer `compact-roadmap` only when it would archive something. */
635
+ function roadmapCompactionAvailable(cwd) {
636
+ // Lazy for the same reason as state-compact: keep hygiene off a load-time cycle.
637
+ return require('./roadmap-compact.cjs').roadmapCompactionAvailable(cwd);
638
+ }
639
+
494
640
  /** H-7: fragment .planning — artifacts present but no project spine. Report-only. */
495
641
  function checkPlanningFragment(cwd) {
496
642
  const findings = [];
@@ -572,9 +718,11 @@ function scanHygiene(cwd, opts) {
572
718
  // matter how many trees we sweep, or a four-track repo reports the same
573
719
  // drift four times.
574
720
  const version = checkVersionAlignment(cwd);
721
+ // The host's memory index is one per project too, whatever the tracks.
722
+ const host = checkHostMemory(cwd, { homeDir: opts?.homeDir });
575
723
  const roots = planningRoots(cwd, { allTracks: !!opts?.allTracks });
576
724
 
577
- const findings = [...version.findings];
725
+ const findings = [...version.findings, ...host.findings];
578
726
  const scanned = [];
579
727
  let planningExists = false;
580
728
 
@@ -601,6 +749,7 @@ function scanHygiene(cwd, opts) {
601
749
  return {
602
750
  findings,
603
751
  installs: version.installs,
752
+ host_memory: host.host_memory,
604
753
  latest_version: version.latest_version,
605
754
  planning_exists: planningExists,
606
755
  // What was actually looked at. Present in every scan, not just --all-tracks:
@@ -653,6 +802,12 @@ function applyFix(cwd, finding) {
653
802
  if (r.error) return { applied: false, detail: r.error };
654
803
  return { applied: true, detail: `compacted to ${r.kept ?? r.entries ?? 'cap'} entries` };
655
804
  }
805
+ case 'prune-memory': {
806
+ const r = pruneMemory(cwd, fix.agent, { apply: true });
807
+ if (r.error) return { applied: false, detail: r.error };
808
+ if (!r.applied) return { applied: false, detail: 'nothing stale or expired any more' };
809
+ return { applied: true, detail: `archived ${r.archivable} entr${r.archivable === 1 ? 'y' : 'ies'} to ${r.archive_dir}/` };
810
+ }
656
811
  case 'compact-state': {
657
812
  // Required lazily: state-compact pulls in state.cjs, which pulls in core
658
813
  // — importing it at module load would put hygiene on that cycle.
@@ -665,6 +820,16 @@ function applyFix(cwd, finding) {
665
820
  detail: `archived ${r.archived.length} section(s) to ${r.history_path} — saves ~${r.tokens_saved_per_call} tokens per agent call`,
666
821
  };
667
822
  }
823
+ case 'compact-roadmap': {
824
+ const { compactRoadmap } = require('./roadmap-compact.cjs');
825
+ const r = compactRoadmap(cwd, { apply: true });
826
+ if (!r.found) return { applied: false, detail: 'roadmap.md not found' };
827
+ if (!r.applied) return { applied: false, detail: 'no shipped phase past the most recent ones' };
828
+ return {
829
+ applied: true,
830
+ detail: `archived ${r.archived.length} shipped phase section(s) to ${r.history_path} — roadmap.md ~${r.tokens_before - r.tokens_after} tokens smaller`,
831
+ };
832
+ }
668
833
  case 'quarantine-ledger': {
669
834
  const stamp = new Date().toISOString().slice(0, 10);
670
835
  const dest = `${abs}.quarantined-${stamp}`;
@@ -832,6 +997,8 @@ module.exports = {
832
997
  checkLegacyUppercase,
833
998
  checkTmpOrphans,
834
999
  checkMemoryLogs,
1000
+ checkHostMemory,
1001
+ hostMemoryDir,
835
1002
  checkCostLedger,
836
1003
  checkStaleTraces,
837
1004
  checkStaleReports,
@@ -8,6 +8,7 @@ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInte
8
8
  const { PHASES_DIR, CODEBASE_DIR, QUICK_DIR, MILESTONES_DIR, STATE_FILE, ROADMAP_FILE, CONFIG_FILE, PROJECT_FILE, REQUIREMENTS_FILE, isPlanFile, isSummaryFile, isResearchFile, isContextFile, isVerificationFile, PLAN_SUFFIX, SUMMARY_SUFFIX, CONTEXT_SUFFIX, RESEARCH_SUFFIX, VERIFICATION_SUFFIX, UAT_SUFFIX, MAX_SLUG_LENGTH } = require('./constants.cjs');
9
9
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, classifyPhaseStatus, hasBraveSearchKey, parsePhaseDir, planningRel } = require('./utils.cjs');
10
10
  const { classifyPlanTier } = require('./phase.cjs');
11
+ const { planCheckStatus, planDrift } = require('./verify-plan-set.cjs');
11
12
  const { extractFrontmatter } = require('./frontmatter.cjs');
12
13
  const { detectLanguages } = require('./codebase.cjs');
13
14
  const { detectForeignPlanningTreeAt } = require('./foreign-planning.cjs');
@@ -31,10 +32,11 @@ function planningRelPath(...segments) {
31
32
  /**
32
33
  * Extract requirement IDs from a ROADMAP phase section.
33
34
  * Looks for a **Requirements**: line, strips brackets, splits by comma,
34
- * and returns null when absent or set to 'TBD'.
35
+ * and returns null when absent or set to 'TBD'. Both bold forms count: the roadmap
36
+ * template writes `**Requirements:**`, which this used to miss (phase_req_ids null).
35
37
  */
36
38
  function extractReqIds(roadmapPhase) {
37
- const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
39
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements(?:\*\*:|:\*\*)[^\S\n]*([^\n]*)$/m);
38
40
  const reqExtracted = reqMatch
39
41
  ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(segment => segment.trim()).filter(Boolean).join(', ')
40
42
  : null;
@@ -204,6 +206,11 @@ function cmdInitExecutePhase(cwd, phase, raw, opts) {
204
206
  execution_mode: (config.execution && config.execution.default_mode) || 'wave_order',
205
207
  dry_run: dryRun,
206
208
  rollback_tag: null,
209
+
210
+ // Plan checks in code (V7): whether these plans are the ones that passed the
211
+ // plan check, and which planned files changed on the branch since planning.
212
+ plan_check: phaseInfo ? planCheckStatus(cwd, phase) : null,
213
+ plan_drift: phaseInfo ? planDrift(cwd, phase) : [],
207
214
  };
208
215
 
209
216
  output(result, raw);
@@ -246,7 +253,9 @@ function cmdInitPlanPhase(cwd, phase, raw) {
246
253
  phase_number: phaseInfo?.phase_number || roadmapPhase?.phase_number || null,
247
254
  phase_name: phaseInfo?.phase_name || roadmapPhase?.phase_name || null,
248
255
  phase_slug: phaseInfo?.phase_slug || (roadmapPhase?.phase_name ? generateSlugInternal(roadmapPhase.phase_name) : null),
249
- padded_phase: (phaseInfo?.phase_number || roadmapPhase?.phase_number)?.toString().padStart(2, '0') || null,
256
+ // normalizePhaseName pads the integer part, as directories are named (2.1 → 02.1);
257
+ // padStart(2, '0') left a decimal phase unpadded.
258
+ padded_phase: (phaseInfo?.phase_number || roadmapPhase?.phase_number) ? normalizePhaseName(String(phaseInfo?.phase_number || roadmapPhase?.phase_number)) : null,
250
259
  phase_req_ids: phaseReqIds,
251
260
 
252
261
  // Existing artifacts
@@ -592,7 +601,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
592
601
  phase_number: phaseInfo?.phase_number || null,
593
602
  phase_name: phaseInfo?.phase_name || null,
594
603
  phase_slug: phaseInfo?.phase_slug || null,
595
- padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
604
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(String(phaseInfo.phase_number)) : null,
596
605
 
597
606
  // Existing artifacts
598
607
  has_research: phaseInfo?.has_research || false,
@@ -16,7 +16,21 @@ const fs = require('fs');
16
16
  const path = require('path');
17
17
  const { output, error, safeReadFile, toPosix, escapeRegex } = require('./core.cjs');
18
18
  const { planningPath } = require('./utils.cjs');
19
- const { listMemoryAgents, readMemory } = require('./memory.cjs');
19
+ const { listMemoryAgents, readMemory, RESERVED_MEMORY_NAMES } = require('./memory.cjs');
20
+
21
+ /**
22
+ * PAN's own archives in `.planning/memory/` are never offered as sources: the
23
+ * quarantine holds directives PAN refused to follow (ADR-0040), the state archive
24
+ * is old state, and `archive/` holds the entries `memory prune` retired. Until
25
+ * 2026-10-05 `knowledge ask` ranked them like any other file, and a question about
26
+ * deploying returned the quarantine as its top source for the pan-knowledge agent.
27
+ */
28
+ function isMemoryArchive(relPosix) {
29
+ const m = String(relPosix).match(/^\.planning\/memory\/(.+)$/);
30
+ if (!m) return false;
31
+ if (m[1] === 'archive' || m[1].startsWith('archive/')) return true;
32
+ return !m[1].includes('/') && RESERVED_MEMORY_NAMES.includes(m[1].replace(/\.md$/i, '').toLowerCase());
33
+ }
20
34
 
21
35
  const CONVERSATIONS_DIR = 'conversations';
22
36
  const PLAYBOOK_FILE = 'playbook.md';
@@ -84,6 +98,7 @@ function gatherCandidates(cwd, question, recallCue) {
84
98
  try { entries = fs.readdirSync(abs); } catch { continue; }
85
99
  for (const entry of entries) {
86
100
  const entryAbs = path.join(abs, entry);
101
+ if (isMemoryArchive(toPosix(path.join(rel, entry)))) continue;
87
102
  let entryStat;
88
103
  try { entryStat = fs.statSync(entryAbs); } catch { continue; }
89
104
  if (entryStat.isFile() && entry.endsWith('.md')) {
@@ -359,6 +374,7 @@ module.exports = {
359
374
  cmdKnowledgeDiscuss,
360
375
  cmdKnowledgePlaybook,
361
376
  CITATION_ROOTS,
377
+ isMemoryArchive,
362
378
  CONVERSATIONS_DIR,
363
379
  PLAYBOOK_FILE,
364
380
  PLAYBOOK_CATEGORIES,