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
@@ -3,7 +3,7 @@
3
3
  */
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveModelDetailed, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix } = require('./core.cjs');
6
+ const { safeReadFile, loadConfig, isGitIgnored, isGitRepo, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, resolveModelDetailed, resolveEffortInternal, detectProvider, resolveTierToModel, estimateCostMultiplier, MODEL_PROFILES, output, error, findPhaseInternal, scanPendingTodos, toPosix, toLf } = require('./core.cjs');
7
7
  const { extractFrontmatter } = require('./frontmatter.cjs');
8
8
  const { PHASES_DIR, MILESTONES_DIR, QUICK_DIR, STATE_FILE, ROADMAP_FILE, PROJECT_FILE, PATTERNS_FILE, SESSION_HISTORY_FILE, LEARNINGS_FILE, CONTEXT_SUFFIX, UAT_SUFFIX, VERIFICATION_SUFFIX, isPlanFile, isSummaryFile, ARCHIVE_DIR_RE, PHASE_DIR_RE, CONTEXT_WINDOW, WARNING_THRESHOLD, CRITICAL_THRESHOLD, VALID_COMMIT_TYPES, DEFAULT_SENSITIVE_PATTERNS, STATE_CONTRACT } = require('./constants.cjs');
9
9
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, planningRel } = require('./utils.cjs');
@@ -153,9 +153,12 @@ function buildDigest(summaries, techStack) {
153
153
  frontmatter.provides.forEach(item => phases[phaseNum].provides.add(item));
154
154
  }
155
155
 
156
- // Merge affects from nested dependency-graph
156
+ // Merge affects from either nested dependency-graph or the flat `affects:` list the
157
+ // summary templates write (only the nested form was read, so `affects` stayed empty)
157
158
  if (frontmatter['dependency-graph'] && Array.isArray(frontmatter['dependency-graph'].affects)) {
158
159
  frontmatter['dependency-graph'].affects.forEach(item => phases[phaseNum].affects.add(item));
160
+ } else if (Array.isArray(frontmatter.affects)) {
161
+ frontmatter.affects.forEach(item => phases[phaseNum].affects.add(item));
159
162
  }
160
163
 
161
164
  // Merge established patterns
@@ -315,7 +318,9 @@ function runCommitSafetyChecks(cwd, config, force) {
315
318
  if (statusResult.exitCode === 0 && statusResult.stdout) {
316
319
  for (const line of statusResult.stdout.split('\n').filter(Boolean)) {
317
320
  if (line.startsWith(' D') || line.startsWith('D ') || line.startsWith('D')) {
318
- const fileName = line.slice(3).trim();
321
+ // execGit trims stdout, so a first line of ` D gone.txt` arrives as `D gone.txt`:
322
+ // strip the status code by pattern, not by a fixed three-character slice
323
+ const fileName = line.replace(/^\s*[A-Z?!]{1,2}\s+/, '').trim();
319
324
  if (fileName) safetyChecks.deleted_files.push(fileName);
320
325
  }
321
326
  }
@@ -333,7 +338,13 @@ function runCommitSafetyChecks(cwd, config, force) {
333
338
  if (patterns.length > 0) {
334
339
  const stagedFiles = stagedResult.stdout.split('\n').filter(Boolean);
335
340
  const regexes = patterns.map(p => { try { return new RegExp(p, 'i'); } catch { return null; } }).filter(Boolean);
341
+ // PAN's own cost ledger matches the bare `token` pattern, which blocked every
342
+ // commit that staged .planning/ once the cost logger had written to it. It holds
343
+ // token counts, never a credential. The hook always writes the root tree's copy.
344
+ const { METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
345
+ const ownLedger = new Set([planningRel(METRICS_DIR, TOKENS_FILE), `.planning/${METRICS_DIR}/${TOKENS_FILE}`]);
336
346
  for (const f of stagedFiles) {
347
+ if (ownLedger.has(f)) continue;
337
348
  if (regexes.some(re => re.test(f))) safetyChecks.sensitive_files_blocked.push(f);
338
349
  }
339
350
  }
@@ -463,6 +474,22 @@ function cmdCommit(cwd, message, files, raw, amend, opts) {
463
474
  output({ committed: true, hash, reason: 'committed', type: commitType || null, safety_checks: safety.safetyChecks }, raw, hash || 'committed');
464
475
  }
465
476
 
477
+ /**
478
+ * A summary's one-liner: the `one-liner` frontmatter key, else the bold line under the
479
+ * `# Phase … Summary` title, which is where the summary templates put it (none writes
480
+ * the key, so reading only the key gave `one_liner: null` for every summary PAN writes).
481
+ * Read on LF: the body match failed on a CRLF summary.
482
+ * @param {string} content - Whole summary.md
483
+ * @returns {string|null}
484
+ */
485
+ function summaryOneLiner(content) {
486
+ const text = toLf(String(content || ''));
487
+ const fm = extractFrontmatter(text);
488
+ if (fm['one-liner']) return fm['one-liner'];
489
+ const m = text.replace(/^?---\n[\s\S]*?\n---\n/, '').match(/^#\s[^\n]*\n+\*\*([^\n*][^\n]*?)\*\*\s*$/m);
490
+ return m ? m[1].trim() : null;
491
+ }
492
+
466
493
  /**
467
494
  * Extract structured data from a summary.md frontmatter with optional field filtering.
468
495
  * @param {string} cwd - Working directory path
@@ -502,10 +529,11 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
502
529
  });
503
530
  };
504
531
 
532
+
505
533
  // Build full result
506
534
  const fullResult = {
507
535
  path: summaryPath,
508
- one_liner: frontmatter['one-liner'] || null,
536
+ one_liner: summaryOneLiner(content),
509
537
  key_files: frontmatter['key-files'] || [],
510
538
  tech_added: (frontmatter['tech-stack'] && frontmatter['tech-stack'].added) || [],
511
539
  patterns: frontmatter['patterns-established'] || [],
@@ -528,6 +556,21 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
528
556
  output(fullResult, raw);
529
557
  }
530
558
 
559
+ /**
560
+ * Map a --freshness value to Brave's `freshness` parameter, which accepts only
561
+ * pd/pw/pm/py or a YYYY-MM-DDtoYYYY-MM-DD range. Returns null for anything else.
562
+ * @param {string} value - day|week|month|year, or a value Brave accepts as is
563
+ * @returns {string|null}
564
+ */
565
+ function braveFreshness(value) {
566
+ const v = String(value || '').trim().toLowerCase();
567
+ const named = { day: 'pd', week: 'pw', month: 'pm', year: 'py' };
568
+ if (named[v]) return named[v];
569
+ if (/^p[dwmy]$/.test(v)) return v;
570
+ if (/^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$/.test(v)) return v;
571
+ return null;
572
+ }
573
+
531
574
  /**
532
575
  * Search the web using Brave Search API and return structured results.
533
576
  *
@@ -560,6 +603,12 @@ async function cmdWebsearch(query, options, raw) {
560
603
  return;
561
604
  }
562
605
 
606
+ const freshness = options.freshness ? braveFreshness(options.freshness) : null;
607
+ if (options.freshness && !freshness) {
608
+ output({ available: false, error: `Invalid --freshness "${options.freshness}": use day, week, month, year, pd, pw, pm, py or YYYY-MM-DDtoYYYY-MM-DD` }, raw, '');
609
+ return;
610
+ }
611
+
563
612
  const params = new URLSearchParams({
564
613
  q: query,
565
614
  count: String(options.limit || 10),
@@ -568,8 +617,8 @@ async function cmdWebsearch(query, options, raw) {
568
617
  text_decorations: 'false'
569
618
  });
570
619
 
571
- if (options.freshness) {
572
- params.set('freshness', options.freshness);
620
+ if (freshness) {
621
+ params.set('freshness', freshness);
573
622
  }
574
623
 
575
624
  try {
@@ -1131,7 +1180,9 @@ module.exports = {
1131
1180
  cmdEstimateCost,
1132
1181
  cmdCommit,
1133
1182
  cmdSummaryExtract,
1183
+ summaryOneLiner,
1134
1184
  cmdWebsearch,
1185
+ braveFreshness,
1135
1186
  cmdProgressRender,
1136
1187
  cmdTodoComplete,
1137
1188
  cmdScaffold,
@@ -53,10 +53,15 @@ function buildConfigDefaults(hasBraveSearch, userDefaults) {
53
53
  verifier: true,
54
54
  nyquist_validation: false,
55
55
  phase_record_compact: false,
56
+ // V1: the phase gate. `warn` reports refusals and completes; `enforce` refuses.
57
+ phase_gate: 'warn',
56
58
  // M2: opt-in per-phase HTML reports at the verify→complete gate.
57
59
  phase_reports: { enabled: false, open: false, theme: 'auto', index: true },
58
60
  },
59
61
  parallelization: true,
62
+ // A7: the pre-tool guard. `report` logs to <planning>/guard.jsonl; `enforce` also
63
+ // denies a write that puts a credential in a file; `off` disables it.
64
+ hooks: { guard: 'report' },
60
65
  brave_search: hasBraveSearch,
61
66
  budget: {
62
67
  default_points: 50,
@@ -365,7 +370,8 @@ function cmdConfigGet(cwd, keyPath, raw) {
365
370
  error(`Key not found: ${keyPath}`);
366
371
  }
367
372
 
368
- output(current, raw, String(current));
373
+ // An object or array prints as JSON under --raw: String() gave "[object Object]".
374
+ output(current, raw, current !== null && typeof current === 'object' ? JSON.stringify(current) : String(current));
369
375
  }
370
376
 
371
377
  // ─── Standards commands ─────────────────────────────────────────────────────
@@ -34,6 +34,9 @@ const RESEARCH_SUFFIX = '-research.md';
34
34
  const VERIFICATION_SUFFIX = '-verification.md';
35
35
  const UAT_SUFFIX = '-uat.md';
36
36
  const VALIDATION_SUFFIX = '-validation.md';
37
+ // A phase's roadmap slice (roadmap slice --write), which phase agents and plans read
38
+ // in place of the whole roadmap.md and requirements.md.
39
+ const ROADMAP_SLICE_SUFFIX = '-roadmap-slice.md';
37
40
 
38
41
  // ─── File matching helpers ───────────────────────────────────────────────────
39
42
 
@@ -576,17 +579,23 @@ const TOTAL_BUDGET_MIN = 5;
576
579
  const TOTAL_BUDGET_MAX = 5000;
577
580
 
578
581
  // Memory read/budget (ADR-0036 FW-2/FW-3): distill-and-select on the memory axis.
579
- const MEMORY_SELECT_BUDGET_TOKENS = 2000; // per-agent cap for cue-scoped memory injection
582
+ const MEMORY_SELECT_BUDGET_TOKENS = 2000; // per-agent cap for a cue-scoped `memory select`
580
583
  const MEMORY_RECENCY_FLOOR = 5; // always keep this many newest entries (recall never empty)
581
584
  const MEMORY_SOFT_CAP_MULT = 2; // soft auto-compaction trigger = DEFAULT_MAX_ENTRIES × this
582
585
  const MEMORY_LOAD_WARN_TOKENS = 4000; // memory-budget telemetry: warn threshold (absolute tokens)
583
586
  const MEMORY_LOAD_CRIT_TOKENS = 8000; // memory-budget telemetry: critical threshold (absolute tokens)
584
587
  const MEMORY_LOAD_MAX_FRACTION = 0.15; // memory-budget telemetry: max fraction of median agent input
585
-
586
- // Cached prompt context (ADR-0044). The files in CACHEABLE_CONTEXT_FILES are
587
- // re-read into EVERY agent call, so their combined size is the single largest
588
- // recurring cost in a PAN project — cache reads dominate token traffic by roughly
589
- // two orders of magnitude over generation. These thresholds mirror the
588
+ // Cited, verified, expiring memory (memory optimisation O4; the Copilot Memory
589
+ // pattern). An entry not used for this many days is no longer selected and
590
+ // `memory prune` archives it; `memory select --mark-used` refreshes it.
591
+ const MEMORY_EXPIRE_DAYS = 60;
592
+ const MEMORY_CITED_FILE_MAX_BYTES = 2 * 1024 * 1024; // larger cited files: existence is the evidence
593
+
594
+ // Stable planning files (ADR-0044): what `cache prime`, `context-budget` and
595
+ // hygiene's cache-context check measure. state.md among them is re-read on every
596
+ // agent call, the largest recurring cost in a PAN project; roadmap.md and
597
+ // requirements.md are read whole only by the roadmapper and milestone work, since
598
+ // phase agents read `roadmap slice` (O2). These thresholds mirror the
590
599
  // MEMORY_LOAD_* pattern above: measured, classified, and surfaced.
591
600
  const CACHE_BLOCK_WARN_TOKENS = 15000; // cached context block: warn threshold (absolute tokens)
592
601
  const CACHE_BLOCK_CRIT_TOKENS = 25000; // cached context block: critical threshold
@@ -780,6 +789,7 @@ module.exports = {
780
789
  VERIFICATION_SUFFIX,
781
790
  UAT_SUFFIX,
782
791
  VALIDATION_SUFFIX,
792
+ ROADMAP_SLICE_SUFFIX,
783
793
  // File matchers
784
794
  isPlanFile,
785
795
  isSummaryFile,
@@ -862,6 +872,8 @@ module.exports = {
862
872
  MEMORY_LOAD_WARN_TOKENS,
863
873
  MEMORY_LOAD_CRIT_TOKENS,
864
874
  MEMORY_LOAD_MAX_FRACTION,
875
+ MEMORY_EXPIRE_DAYS,
876
+ MEMORY_CITED_FILE_MAX_BYTES,
865
877
  // Hygiene
866
878
  CACHE_BLOCK_WARN_TOKENS,
867
879
  CACHE_BLOCK_CRIT_TOKENS,
@@ -267,8 +267,8 @@ function cmdContextBudget(cwd, raw) {
267
267
  }
268
268
  }
269
269
 
270
- // E-8: cache metrics — surface how much of the total context would be
271
- // served from prompt cache when Opus 4.7 cache_control is active.
270
+ // E-8: cache metrics — how much of the total context is the stable set that
271
+ // agents re-read, which the host serves from its prompt cache when it can.
272
272
  const { buildCachedContext } = require('./core.cjs');
273
273
  let cache = null;
274
274
  try {
@@ -277,10 +277,10 @@ function cmdContextBudget(cwd, raw) {
277
277
  const eligiblePct = totalTokens > 0
278
278
  ? Math.round((cacheTokens / totalTokens) * 1000) / 10
279
279
  : 0;
280
- // The cached block is re-read into EVERY agent call, so its size is the
281
- // project's largest recurring cost. This used to be measured and reported
282
- // with no threshold attached, which meant a block that had grown to ~28k
283
- // tokens of mostly closed history looked exactly like a healthy one.
280
+ // The stable planning files; state.md among them is re-read on every agent
281
+ // call, the project's largest recurring cost. This used to be measured and
282
+ // reported with no threshold attached, which meant a block that had grown to
283
+ // ~28k tokens of mostly closed history looked exactly like a healthy one.
284
284
  // Classifying it is what turns the measurement into a signal.
285
285
  const { CACHE_BLOCK_WARN_TOKENS, CACHE_BLOCK_CRIT_TOKENS, CACHE_FILE_WARN_TOKENS } = require('./constants.cjs');
286
286
  const largest = cached.blocks
@@ -292,12 +292,15 @@ function cmdContextBudget(cwd, raw) {
292
292
  else if (cacheTokens >= CACHE_BLOCK_CRIT_TOKENS) cacheStatus = 'critical';
293
293
  else if (cacheTokens >= CACHE_BLOCK_WARN_TOKENS) cacheStatus = 'warn';
294
294
 
295
+ // PAN primes no cache (ADR-0023, amended): an absent block means nothing to
296
+ // measure, not that the host stops caching.
295
297
  const advice = cacheStatus === 'absent'
296
- ? 'no cacheable context files — every agent call re-sends its context uncached'
298
+ ? 'none of the stable planning files exist yet, so there is nothing to measure'
297
299
  : cacheStatus === 'ok'
298
300
  ? null
299
- : `cached context is re-read on every agent call; largest file ${largest[0].path} (~${largest[0].tokens} tokens)`
300
- + (largest[0].path.endsWith('state.md') ? ' — run `pan-tools state compact`' : '');
301
+ : `stable planning files past the budget; largest file ${largest[0].path} (~${largest[0].tokens} tokens)`
302
+ + (largest[0].path.endsWith('state.md') ? ' — re-read on every agent call; run `pan-tools state compact`'
303
+ : largest[0].path.endsWith('roadmap.md') ? ' — read whole by the roadmapper and milestone work; run `pan-tools roadmap compact`' : '');
301
304
 
302
305
  // Lifetime signal from the ledger (suspect rows excluded — they carry
303
306
  // poisoned counters, not real writes). Absent ledger → zero rows, no advice.
@@ -65,12 +65,14 @@ const PROVIDER_MODELS = {
65
65
  // PROVIDER_MODELS, provider-qualified; `default` is Anthropic as it is there.
66
66
  // Reality check R41. The Anthropic mid tier follows the `sonnet` alias, which
67
67
  // resolves to Sonnet 5.5 on the Anthropic API from Claude Code 2.1.284;
68
- // models.dev lists `anthropic/claude-sonnet-5-5` (api.json, read 2026-10-03).
68
+ // models.dev lists `anthropic/claude-sonnet-5-5` (api.json, read 2026-10-03). The fast
69
+ // tier follows the `haiku` alias, which resolves to Haiku 5.5 on the Anthropic API from
70
+ // Claude Code 2.1.293; models.dev lists `anthropic/claude-haiku-5-5` (read 2026-10-10).
69
71
  const OPENCODE_MODELS = {
70
- anthropic: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-4-5' },
72
+ anthropic: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-5-5' },
71
73
  openai: { reasoning: 'inherit', mid: 'openai/gpt-6-sol', fast: 'openai/gpt-6-luna' },
72
74
  google: { reasoning: 'inherit', mid: 'google/gemini-3.8-flash', fast: 'google/gemini-3.5-flash-lite' },
73
- default: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-4-5' },
75
+ default: { reasoning: 'inherit', mid: 'anthropic/claude-sonnet-5-5', fast: 'anthropic/claude-haiku-5-5' },
74
76
  };
75
77
 
76
78
  /**
@@ -389,6 +391,32 @@ function safeReadFile(filePath) {
389
391
  }
390
392
  }
391
393
 
394
+ // ─── Line endings ───────────────────────────────────────────────────────────
395
+ // A Windows working tree (core.autocrlf=true) holds planning files with CRLF
396
+ // endings, and a parser written for '\n' finds nothing in them without saying so:
397
+ // on 2026-10-04 every Windows field project measured was CRLF throughout, and the
398
+ // memory optimizer, the must-haves parser and `frontmatter set` had all been
399
+ // silently no-op (or worse) there. Parse on LF; write back in the file's own ending.
400
+
401
+ /** LF text: CRLF → LF and a leading BOM dropped. */
402
+ function toLf(text) {
403
+ return String(text == null ? '' : text).replace(/^/, '').replace(/\r\n/g, '\n');
404
+ }
405
+
406
+ /** The ending a text mostly uses: '\r\n' when its CRLF lines outnumber its bare LF ones. */
407
+ function dominantEol(text) {
408
+ const s = String(text == null ? '' : text);
409
+ const crlf = (s.match(/\r\n/g) || []).length;
410
+ const lf = (s.match(/\n/g) || []).length - crlf;
411
+ return crlf > lf ? '\r\n' : '\n';
412
+ }
413
+
414
+ /** LF text given back in the ending `eol` names. */
415
+ function withEol(text, eol) {
416
+ const s = String(text == null ? '' : text);
417
+ return eol === '\r\n' ? s.replace(/\r?\n/g, '\r\n') : s;
418
+ }
419
+
392
420
  /**
393
421
  * Load project config from .planning/config.json, merging with defaults.
394
422
  * Handles nested config sections (planning.*, workflow.*, git.*) and flat keys.
@@ -462,10 +490,10 @@ function loadConfig(cwd) {
462
490
  // Cost dashboard config: `cost.rates` per-model overrides (surfaced so the
463
491
  // documented override actually reaches cost.cjs — it was dropped before).
464
492
  cost: parsed.cost || {},
465
- // Prompt-cache config: `cache.extra_files` lets a project add its own
466
- // stable documents to the cached context block. Needed because the
467
- // built-in list is the phase-model spine, so a focus-model project had
468
- // an empty block and therefore no prompt caching at all.
493
+ // Cache config: `cache.extra_files` lets a project add its own stable
494
+ // documents to the block `cache prime` and the size checks measure. Needed
495
+ // because the built-in list is the phase-model spine, so a focus-model
496
+ // project had an empty block and nothing measured.
469
497
  cache: parsed.cache || {},
470
498
  // ADR-0031: project build/verification commands. null = not configured
471
499
  // (focus-auto --clean-seal then asks or skips rather than guessing).
@@ -634,7 +662,7 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
634
662
  try {
635
663
  const entries = fs.readdirSync(baseDir, { withFileTypes: true });
636
664
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
637
- const match = dirs.find(d => d.startsWith(normalized));
665
+ const match = dirs.find(d => d === normalized || d.startsWith(normalized + '-'));
638
666
  if (!match) return null;
639
667
 
640
668
  const dirMatch = match.match(PHASE_DIR_RE);
@@ -785,7 +813,11 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
785
813
  const restOfContent = content.slice(headerIndex);
786
814
  const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
787
815
  const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index : content.length;
788
- const section = content.slice(headerIndex, sectionEnd).trim();
816
+ let section = content.slice(headerIndex, sectionEnd).trim();
817
+ // A phase moved out by `roadmap compact` leaves a stub; the full section
818
+ // (and any model_tier override with it) is in roadmap-history.md.
819
+ const compacted = require('./roadmap-compact.cjs').compactedPhaseSection(cwd, unpadded, section);
820
+ if (compacted && compacted.section) section = compacted.section;
789
821
 
790
822
  const goalMatch = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
791
823
  const goal = goalMatch ? goalMatch[1].trim() : null;
@@ -796,6 +828,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
796
828
  phase_name: phaseName,
797
829
  goal,
798
830
  section,
831
+ ...(compacted ? { compacted: true } : {}),
799
832
  };
800
833
  } catch {
801
834
  return null;
@@ -1362,12 +1395,12 @@ function scanPendingTodos(cwd, area) {
1362
1395
  *
1363
1396
  * Reads files from .planning/ that are stable across agent calls within a phase
1364
1397
  * (project.md, requirements.md, roadmap.md, state.md, standards.md). Each block
1365
- * is tagged `cache: true` so the host runtime (or installer) can translate to
1366
- * the appropriate per-runtime caching syntax (Anthropic cache_control, etc.).
1398
+ * is tagged `cache: true`. Nothing translates that tag into a cache marker: the
1399
+ * host caches each agent's prompt prefix by itself, so this list is what PAN
1400
+ * measures (context-budget, hygiene's cache-context check), not what it primes.
1367
1401
  *
1368
1402
  * Files that don't exist are skipped silently. The order matches the file list
1369
- * in constants.cjs to keep prompt prefixes byte-stable across calls (which is
1370
- * what cache key matching requires).
1403
+ * in constants.cjs, so the `sha` of an unchanged set stays the same.
1371
1404
  *
1372
1405
  * @param {string} cwd - Project root
1373
1406
  * @returns {{blocks: Array<{path: string, content: string, cache: true}>, total_bytes: number, sha: string}}
@@ -1380,14 +1413,12 @@ function buildCachedContext(cwd) {
1380
1413
  const hasher = crypto.createHash('sha256');
1381
1414
 
1382
1415
  // The built-in list is the PHASE-model spine. A focus-model project has none
1383
- // of those files, so its cached block came out empty and it silently received
1384
- // no prompt caching at all. `cache.extra_files` lets such a project name its
1385
- // own stable documents rather than PAN inventing a convention it doesn't
1386
- // otherwise define.
1416
+ // of those files, so its measured block came out empty and the size checks had
1417
+ // nothing to read. `cache.extra_files` lets such a project name its own stable
1418
+ // documents rather than PAN inventing a convention it doesn't otherwise define.
1387
1419
  //
1388
1420
  // Entries are planning-root-relative, must stay inside it, and are appended
1389
- // after the built-ins so the prefix stays byte-stable for projects that set
1390
- // nothing — changing the prefix would invalidate every existing cache key.
1421
+ // after the built-ins, so the `sha` of a project that sets nothing does not change.
1391
1422
  const extra = [];
1392
1423
  try {
1393
1424
  const configured = loadConfig(cwd)?.cache?.extra_files;
@@ -1468,6 +1499,9 @@ module.exports = {
1468
1499
  error,
1469
1500
  verbose,
1470
1501
  safeReadFile,
1502
+ toLf,
1503
+ dominantEol,
1504
+ withEol,
1471
1505
  loadConfig,
1472
1506
  isGitIgnored,
1473
1507
  isGitRepo,
@@ -39,7 +39,7 @@ const os = require('os');
39
39
  const path = require('path');
40
40
  const { output, error, loadConfig } = require('./core.cjs');
41
41
  const { planningPath } = require('./utils.cjs');
42
- const { readRecords, computeCost, effectiveRates, isSuspectRecord, isEmptyRecord, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
42
+ const { readRecords, computeCost, effectiveRates, isSuspectRecord, isEmptyRecord, LONG_PROMPT_THRESHOLD, METRICS_DIR, TOKENS_FILE } = require('./cost.cjs');
43
43
 
44
44
  const SCHEMA_V = 4; // matches hooks/pan-cost-logger.js SCHEMA_V
45
45
  const MAIN_THREAD_AGENT = '(main thread)';
@@ -144,6 +144,17 @@ function sumTranscriptUsage(file) {
144
144
  totals.cache_write_1h_tokens = (totals.cache_write_1h_tokens || 0) + num(cc, 'ephemeral_1h_input_tokens');
145
145
  totals.cache_write_5m_tokens = (totals.cache_write_5m_tokens || 0) + num(cc, 'ephemeral_5m_input_tokens');
146
146
  }
147
+ // A request whose prompt is over LONG_PROMPT_THRESHOLD bills at a model's long
148
+ // rate where it has one (Haiku 5.5); the same rule as the cost-logger hook.
149
+ const prompt = num(u, 'input_tokens') + num(u, 'cache_read_input_tokens') + num(u, 'cache_creation_input_tokens');
150
+ if (prompt > LONG_PROMPT_THRESHOLD) {
151
+ const lp = totals.long_prompt || (totals.long_prompt = { above: LONG_PROMPT_THRESHOLD, input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, cache_write_1h_tokens: 0 });
152
+ lp.input_tokens += num(u, 'input_tokens');
153
+ lp.output_tokens += num(u, 'output_tokens');
154
+ lp.cache_read_tokens += num(u, 'cache_read_input_tokens');
155
+ lp.cache_write_tokens += num(u, 'cache_creation_input_tokens');
156
+ if (cc && typeof cc === 'object') lp.cache_write_1h_tokens += num(cc, 'ephemeral_1h_input_tokens');
157
+ }
147
158
  }
148
159
  totals.turns = byMessage.size;
149
160
  let best = null;
@@ -304,6 +315,7 @@ function makeRow(fields) {
304
315
  ...(typeof fields.usage.cache_write_1h_tokens === 'number'
305
316
  ? { cache_write_1h_tokens: fields.usage.cache_write_1h_tokens, cache_write_5m_tokens: fields.usage.cache_write_5m_tokens || 0 }
306
317
  : {}),
318
+ ...(fields.usage.long_prompt ? { long_prompt: fields.usage.long_prompt } : {}),
307
319
  cost_usd: null,
308
320
  duration_ms: fields.usage.first_ts && fields.usage.last_ts ? Date.parse(fields.usage.last_ts) - Date.parse(fields.usage.first_ts) : null,
309
321
  phase: null,