pan-wizard 3.33.0 → 3.34.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 (197) hide show
  1. package/README.md +23 -20
  2. package/agents/pan-conductor.md +17 -16
  3. package/agents/pan-counterfactual.md +2 -2
  4. package/agents/pan-debugger.md +27 -8
  5. package/agents/pan-design-checker.md +1 -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 +12 -8
  10. package/agents/pan-experiment-runner.md +8 -7
  11. package/agents/pan-integration-checker.md +2 -2
  12. package/agents/pan-knowledge.md +1 -1
  13. package/agents/pan-meta-reviewer.md +3 -3
  14. package/agents/pan-optimizer.md +39 -43
  15. package/agents/pan-phase-researcher.md +8 -8
  16. package/agents/pan-plan-checker.md +30 -28
  17. package/agents/pan-planner.md +54 -43
  18. package/agents/pan-previewer.md +1 -1
  19. package/agents/pan-project-researcher.md +11 -11
  20. package/agents/pan-release.md +1 -1
  21. package/agents/pan-research-synthesizer.md +2 -2
  22. package/agents/pan-reviewer.md +24 -2
  23. package/agents/pan-roadmapper.md +15 -14
  24. package/agents/pan-verifier.md +32 -24
  25. package/bin/install-lib.cjs +58 -23
  26. package/bin/install.js +47 -1
  27. package/commands/pan/army.md +15 -11
  28. package/commands/pan/assumptions.md +2 -2
  29. package/commands/pan/audit-deployment.md +11 -12
  30. package/commands/pan/cost.md +8 -4
  31. package/commands/pan/debug.md +10 -7
  32. package/commands/pan/design-phase.md +3 -3
  33. package/commands/pan/discuss-phase.md +2 -0
  34. package/commands/pan/exec-phase.md +21 -37
  35. package/commands/pan/experiment.md +9 -8
  36. package/commands/pan/focus-auto.md +22 -20
  37. package/commands/pan/focus-design.md +7 -6
  38. package/commands/pan/focus-doc-audit.md +4 -3
  39. package/commands/pan/focus-drift-walking.md +4 -3
  40. package/commands/pan/focus-exec.md +6 -6
  41. package/commands/pan/focus-plan.md +12 -11
  42. package/commands/pan/focus-scan.md +5 -3
  43. package/commands/pan/focus-sync.md +3 -2
  44. package/commands/pan/git.md +8 -8
  45. package/commands/pan/health.md +2 -2
  46. package/commands/pan/hud.md +3 -2
  47. package/commands/pan/hygiene.md +14 -7
  48. package/commands/pan/knowledge.md +9 -9
  49. package/commands/pan/learn.md +10 -7
  50. package/commands/pan/links.md +4 -1
  51. package/commands/pan/map-codebase.md +1 -1
  52. package/commands/pan/mcp-bridge.md +18 -18
  53. package/commands/pan/milestone-done.md +6 -3
  54. package/commands/pan/milestone-new.md +1 -1
  55. package/commands/pan/new-project.md +10 -10
  56. package/commands/pan/optimize.md +16 -14
  57. package/commands/pan/patches.md +2 -6
  58. package/commands/pan/pause.md +1 -1
  59. package/commands/pan/phase-tests.md +2 -1
  60. package/commands/pan/plan-phase.md +9 -19
  61. package/commands/pan/preview.md +1 -1
  62. package/commands/pan/profile.md +2 -2
  63. package/commands/pan/quick.md +1 -1
  64. package/commands/pan/remove-phase.md +1 -1
  65. package/commands/pan/report.md +2 -2
  66. package/commands/pan/research-phase.md +12 -11
  67. package/commands/pan/resume.md +9 -11
  68. package/commands/pan/retro.md +5 -3
  69. package/commands/pan/review-deep.md +7 -7
  70. package/commands/pan/settings.md +1 -1
  71. package/commands/pan/verify-phase.md +0 -11
  72. package/commands/pan/what-if.md +2 -2
  73. package/hooks/dist/pan-context-monitor.js +343 -44
  74. package/hooks/dist/pan-cost-logger.js +30 -0
  75. package/hooks/dist/pan-statusline.js +68 -22
  76. package/hooks/dist/pan-stop-guard.js +7 -2
  77. package/package.json +1 -1
  78. package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
  79. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  80. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  81. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  82. package/pan-wizard-core/bin/lib/commands.cjs +33 -4
  83. package/pan-wizard-core/bin/lib/config.cjs +2 -1
  84. package/pan-wizard-core/bin/lib/constants.cjs +18 -6
  85. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  86. package/pan-wizard-core/bin/lib/core.cjs +53 -19
  87. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  88. package/pan-wizard-core/bin/lib/cost.cjs +142 -13
  89. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  90. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  91. package/pan-wizard-core/bin/lib/findings.cjs +9 -2
  92. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  93. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  94. package/pan-wizard-core/bin/lib/hygiene.cjs +178 -17
  95. package/pan-wizard-core/bin/lib/init.cjs +7 -4
  96. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  97. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  98. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  99. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  100. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  101. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  102. package/pan-wizard-core/bin/lib/optimize.cjs +84 -16
  103. package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
  104. package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
  105. package/pan-wizard-core/bin/lib/phase.cjs +15 -13
  106. package/pan-wizard-core/bin/lib/preview.cjs +3 -1
  107. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  108. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  109. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  110. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  111. package/pan-wizard-core/bin/lib/state.cjs +43 -28
  112. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  113. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
  114. package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
  115. package/pan-wizard-core/bin/lib/verify-scope.cjs +5 -2
  116. package/pan-wizard-core/bin/lib/verify.cjs +30 -28
  117. package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
  118. package/pan-wizard-core/bin/pan-tools.cjs +101 -15
  119. package/pan-wizard-core/learnings/index.json +5 -5
  120. package/pan-wizard-core/opencode/pan-wizard.js +111 -0
  121. package/pan-wizard-core/references/checkpoints.md +3 -3
  122. package/pan-wizard-core/references/continuation-format.md +3 -3
  123. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  124. package/pan-wizard-core/references/design-methodology.md +1 -1
  125. package/pan-wizard-core/references/git-integration.md +4 -6
  126. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  127. package/pan-wizard-core/references/guardrails.md +2 -2
  128. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  129. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  130. package/pan-wizard-core/references/model-profiles.md +14 -9
  131. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  132. package/pan-wizard-core/references/planning-config.md +4 -4
  133. package/pan-wizard-core/references/tdd.md +10 -1
  134. package/pan-wizard-core/references/verification-patterns.md +5 -5
  135. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  136. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  137. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  138. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  139. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  140. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  141. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  142. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  143. package/pan-wizard-core/templates/continue-here.md +1 -1
  144. package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
  145. package/pan-wizard-core/templates/debug.md +15 -1
  146. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  147. package/pan-wizard-core/templates/milestone.md +1 -1
  148. package/pan-wizard-core/templates/phase-prompt.md +29 -17
  149. package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
  150. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  151. package/pan-wizard-core/templates/research-project/features.md +1 -1
  152. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  153. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  154. package/pan-wizard-core/templates/research.md +5 -5
  155. package/pan-wizard-core/templates/retrospective.md +1 -1
  156. package/pan-wizard-core/templates/roadmap.md +15 -15
  157. package/pan-wizard-core/templates/summary-complex.md +1 -0
  158. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  159. package/pan-wizard-core/templates/summary-standard.md +2 -1
  160. package/pan-wizard-core/templates/summary.md +18 -2
  161. package/pan-wizard-core/templates/uat.md +3 -3
  162. package/pan-wizard-core/templates/user-setup.md +8 -8
  163. package/pan-wizard-core/templates/verification-report.md +53 -8
  164. package/pan-wizard-core/workflows/add-phase.md +1 -1
  165. package/pan-wizard-core/workflows/assumptions.md +1 -1
  166. package/pan-wizard-core/workflows/discuss-phase.md +9 -8
  167. package/pan-wizard-core/workflows/exec-phase.md +26 -57
  168. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  169. package/pan-wizard-core/workflows/health.md +13 -12
  170. package/pan-wizard-core/workflows/help.md +8 -9
  171. package/pan-wizard-core/workflows/learn.md +13 -11
  172. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  173. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  174. package/pan-wizard-core/workflows/milestone-gaps.md +17 -24
  175. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  176. package/pan-wizard-core/workflows/new-project.md +9 -9
  177. package/pan-wizard-core/workflows/optimize.md +11 -9
  178. package/pan-wizard-core/workflows/pause.md +6 -1
  179. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  180. package/pan-wizard-core/workflows/plan-phase.md +31 -34
  181. package/pan-wizard-core/workflows/profile.md +2 -2
  182. package/pan-wizard-core/workflows/progress.md +5 -7
  183. package/pan-wizard-core/workflows/quick.md +5 -3
  184. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  185. package/pan-wizard-core/workflows/research-phase.md +9 -5
  186. package/pan-wizard-core/workflows/resume-project.md +4 -6
  187. package/pan-wizard-core/workflows/retro.md +3 -1
  188. package/pan-wizard-core/workflows/settings.md +16 -24
  189. package/pan-wizard-core/workflows/todo-check.md +1 -1
  190. package/pan-wizard-core/workflows/transition.md +12 -12
  191. package/pan-wizard-core/workflows/update.md +25 -17
  192. package/pan-wizard-core/workflows/verify-phase.md +17 -15
  193. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  194. package/pan-zcode/README.md +7 -7
  195. package/pan-zcode/bin/install-zcode.js +1 -1
  196. package/scripts/build-plugin.js +3 -1
  197. package/scripts/generate-skills-docs.py +13 -4
@@ -73,6 +73,10 @@ const fs = require('fs');
73
73
  const os = require('os');
74
74
  const path = require('path');
75
75
 
76
+ // The CLI the block reason tells the agent to run. Installs and the plugin put the
77
+ // core beside hooks/; no `pan-tools` bin is on PATH.
78
+ const PAN_TOOLS = `node "${path.join(__dirname, '..', 'pan-wizard-core', 'bin', 'pan-tools.cjs').split(path.sep).join('/')}"`;
79
+
76
80
  // ─── R39: one run per hook when Claude and Copilot share a project ───────────
77
81
  // Copilot CLI also runs the hooks in a repository's .claude/settings.json and
78
82
  // .claude/settings.local.json. Measured 2026-09-26 (Copilot CLI 1.0.88, repository
@@ -205,8 +209,9 @@ function buildStopDecision({ stopHookActive, config, stateContent, roadmapConten
205
209
  `Continue the chain now: if the current phase is finished, follow transition.md's ` +
206
210
  `offer_next_phase Route A and spawn Phase ${nextPhase} as a Task subagent ` +
207
211
  `(plan-phase orchestrator, ARGUMENTS='${nextPhase} --auto'); if the current phase is ` +
208
- `mid-flight, resume it instead. If the user explicitly asked to stop, first run: ` +
209
- `pan-tools config-set workflow.auto_advance false — then stop. ` +
212
+ `mid-flight, resume it instead. If the user explicitly asked to stop, first run ` +
213
+ `\`${PAN_TOOLS} config-set workflow.auto_advance false\` (under mode: yolo, which arms this ` +
214
+ `guard by itself, set \`workflow.stop_guard false\` the same way instead) — then stop. ` +
210
215
  `(Disable this guard permanently with workflow.stop_guard: false.)`
211
216
  };
212
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "3.33.0",
3
+ "version": "3.34.0",
4
4
  "description": "Command a bot army for your codebase: a reasoning-tier Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
5
5
  "bin": {
6
6
  "pan-wizard": "bin/install.js"
@@ -82,19 +82,56 @@ function removeAgentsMdSection(existing) {
82
82
  }
83
83
 
84
84
  /**
85
- * Ensure CLAUDE.md bridges to AGENTS.md via a marker-fenced @AGENTS.md import
86
- * (Claude Code's documented pattern for adopting the universal rules file).
87
- * Idempotent; preserves all user content.
85
+ * PAN's block in CLAUDE.md: the @AGENTS.md import, and Claude Code's "Compact
86
+ * instructions" section (memory optimisation O8). Claude Code reads a section of
87
+ * that name in the project-root CLAUDE.md when it summarises a conversation, and
88
+ * re-reads CLAUDE.md from disk after compaction. The section names what the summary
89
+ * must keep for PAN work to resume where it stopped.
90
+ *
91
+ * Codex is deliberately left out: its `compact_prompt` replaces the built-in
92
+ * compaction prompt rather than adding to it, and the default OpenAI provider
93
+ * ignores it (read 2026-10-04), so PAN would be swapping a tuned prompt for one
94
+ * that may never run. PAN's state re-injection after compaction covers Codex.
95
+ */
96
+ function buildClaudeMdBridge() {
97
+ return [
98
+ PAN_AGENTS_BEGIN,
99
+ '@AGENTS.md',
100
+ '',
101
+ '# Compact instructions',
102
+ '',
103
+ 'When this conversation is compacted, keep what PAN work needs to resume where it stopped:',
104
+ '- the current phase and plan, and the task in progress with its stopping point (`.planning/state.md` records them; keep that path);',
105
+ '- decisions made in this session that `.planning/state.md` does not yet hold;',
106
+ '- the files changed since the last commit.',
107
+ 'Leave out file contents and command output: they can be read again.',
108
+ PAN_AGENTS_END,
109
+ ].join('\n');
110
+ }
111
+
112
+ /**
113
+ * Ensure CLAUDE.md carries PAN's block: the marker-fenced @AGENTS.md import
114
+ * (Claude Code's documented pattern for adopting the universal rules file) and
115
+ * the compact instructions. Idempotent; preserves all user content. An older PAN
116
+ * bridge (the import alone) is replaced in place, so a reinstall or `memory
117
+ * rebuild` brings existing projects up to date.
88
118
  * @param {string|null} existing - Current CLAUDE.md content, or null if absent
89
119
  * @returns {string} New file content
90
120
  */
91
121
  function ensureClaudeMdImport(existing) {
92
- const block = `${PAN_AGENTS_BEGIN}\n@AGENTS.md\n${PAN_AGENTS_END}`;
122
+ const block = buildClaudeMdBridge();
93
123
  if (!existing || !existing.trim()) {
94
124
  return block + '\n';
95
125
  }
96
- if (existing.includes(PAN_AGENTS_BEGIN)) {
97
- return existing; // bridge (or another PAN block) already present
126
+ const beginIdx = existing.indexOf(PAN_AGENTS_BEGIN);
127
+ const endIdx = existing.indexOf(PAN_AGENTS_END);
128
+ if (beginIdx !== -1) {
129
+ // PAN's own bridge: bring it up to date. Any other PAN block is left alone.
130
+ if (endIdx > beginIdx && /^@AGENTS\.md\s*$/m.test(existing.slice(beginIdx, endIdx))) {
131
+ const eol = existing.includes('\r\n') ? '\r\n' : '\n';
132
+ return existing.slice(0, beginIdx) + block.replace(/\n/g, eol) + existing.slice(endIdx + PAN_AGENTS_END.length);
133
+ }
134
+ return existing;
98
135
  }
99
136
  if (/^@AGENTS\.md\s*$/m.test(existing)) {
100
137
  return existing; // user already imports AGENTS.md themselves
@@ -118,6 +155,7 @@ module.exports = {
118
155
  buildAgentsMdSection,
119
156
  upsertAgentsMdSection,
120
157
  removeAgentsMdSection,
158
+ buildClaudeMdBridge,
121
159
  ensureClaudeMdImport,
122
160
  removeClaudeMdImport,
123
161
  };
@@ -23,8 +23,9 @@
23
23
  * ]
24
24
  * }
25
25
  *
26
- * Populating this file is the host runtime's responsibility (Claude Code's
27
- * MCP list API, etc.). This module reads the cache and reasons over it.
26
+ * Nothing populates this file on its own: no hook or host writes it.
27
+ * `bridge cache --servers <json>` (or an external script) seeds it; this module
28
+ * reads the cache and reasons over it.
28
29
  */
29
30
 
30
31
  const fs = require('fs');
@@ -184,7 +185,7 @@ function recommendForPhase(cwd, phaseNum, opts) {
184
185
  phase: String(phaseNum),
185
186
  runtime: cache.runtime,
186
187
  recommendations: [],
187
- reason: 'no MCP tools cached — run `pan-tools bridge cache` or ensure host runtime populates .planning/bridge/available-tools.json',
188
+ reason: 'no MCP tools cached — seed .planning/bridge/available-tools.json with `pan-tools bridge cache --servers <json>`; no hook or host writes it',
188
189
  };
189
190
  }
190
191
 
@@ -240,8 +241,8 @@ function cmdBridgeRecommend(cwd, phaseNum, opts, raw) {
240
241
  }
241
242
 
242
243
  function cmdBridgeCache(cwd, serversJson, runtime, raw) {
243
- // For scripted cache writes. Normally the host runtime writes the file,
244
- // but this CLI path lets users seed it for testing or from external scripts.
244
+ // The cache's only writer: no hook or host runtime populates the file, so this
245
+ // CLI path (or an external script) is how users seed it.
245
246
  if (!serversJson) {
246
247
  // No payload — just echo the current cache path/state.
247
248
  output(listTools(cwd), raw);
@@ -66,12 +66,14 @@ function writeScheduleFile(cwd, schedule) {
66
66
  * @returns {object|{error}} the written descriptor
67
67
  */
68
68
  function writeSchedule(cwd, opts, now) {
69
- const cadence = opts.cadence || 'daily';
69
+ // An update keeps the stored cadence like every other field: `--pause` on an
70
+ // hourly campaign used to set it back to daily.
71
+ const existing = readSchedule(cwd) || {};
72
+ const cadence = opts.cadence || existing.cadence || 'daily';
70
73
  if (parseCadence(cadence) === null) {
71
74
  return { error: `Invalid cadence "${cadence}". Use hourly | daily | weekly | Nh | Nd.` };
72
75
  }
73
76
  const at = now || new Date();
74
- const existing = readSchedule(cwd) || {};
75
77
  const schedule = {
76
78
  goal: opts.goal ?? existing.goal ?? null,
77
79
  source: opts.source ?? existing.source ?? 'backlog',
@@ -299,6 +299,23 @@ function formatLearningEntry(learning) {
299
299
  return lines.join('\n');
300
300
  }
301
301
 
302
+ /**
303
+ * A summary's `key-files` as one list. The summary templates write `{created, modified}`;
304
+ * older summaries carry a flat list. Only the flat list was read, so summaries PAN writes
305
+ * gave no co-change learnings, and one beside a flat-list summary crashed on `.includes`.
306
+ * @param {Object} fm - Summary frontmatter
307
+ * @returns {string[]}
308
+ */
309
+ function keyFileList(fm) {
310
+ const kf = fm['key-files'] || fm.key_files;
311
+ if (Array.isArray(kf)) return kf.map(String);
312
+ if (kf && typeof kf === 'object') {
313
+ const list = (v) => (Array.isArray(v) ? v : (v ? [v] : []));
314
+ return [...new Set([...list(kf.created), ...list(kf.modified)].map(String))];
315
+ }
316
+ return [];
317
+ }
318
+
302
319
  /**
303
320
  * Extract learnings from session summaries and error patterns.
304
321
  * Reads session history + error patterns, extracts file co-change patterns
@@ -347,7 +364,7 @@ function cmdLearningsExtract(cwd, raw) {
347
364
  const fileCoChanges = new Map(); // file -> Set of co-changed files
348
365
 
349
366
  for (const { frontmatter } of summaries) {
350
- const keyFiles = Array.isArray(frontmatter['key-files']) ? frontmatter['key-files'] : [];
367
+ const keyFiles = keyFileList(frontmatter);
351
368
  if (keyFiles.length < 2) continue;
352
369
  for (const file of keyFiles) {
353
370
  if (!fileCoChanges.has(file)) fileCoChanges.set(file, new Set());
@@ -368,7 +385,7 @@ function cmdLearningsExtract(cwd, raw) {
368
385
  // Count co-occurrences
369
386
  let count = 0;
370
387
  for (const { frontmatter } of summaries) {
371
- const kf = frontmatter['key-files'] || [];
388
+ const kf = keyFileList(frontmatter);
372
389
  if (kf.includes(file) && kf.includes(coFile)) count++;
373
390
  }
374
391
  if (count < 2) continue;
@@ -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'] || [],
@@ -1131,6 +1159,7 @@ module.exports = {
1131
1159
  cmdEstimateCost,
1132
1160
  cmdCommit,
1133
1161
  cmdSummaryExtract,
1162
+ summaryOneLiner,
1134
1163
  cmdWebsearch,
1135
1164
  cmdProgressRender,
1136
1165
  cmdTodoComplete,
@@ -365,7 +365,8 @@ function cmdConfigGet(cwd, keyPath, raw) {
365
365
  error(`Key not found: ${keyPath}`);
366
366
  }
367
367
 
368
- output(current, raw, String(current));
368
+ // An object or array prints as JSON under --raw: String() gave "[object Object]".
369
+ output(current, raw, current !== null && typeof current === 'object' ? JSON.stringify(current) : String(current));
369
370
  }
370
371
 
371
372
  // ─── 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,