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
@@ -24,8 +24,6 @@ Consolidates Spec B v1's X-4 (self-review) + X-12 (harden) into a single command
24
24
  </objective>
25
25
 
26
26
  <execution_context>
27
- @~/.claude/pan-wizard-core/bin/lib/review-deep.cjs
28
- @~/.claude/pan-wizard-core/bin/lib/bus.cjs
29
27
  @~/.claude/agents/pan-hardener.md
30
28
  @~/.claude/agents/pan-meta-reviewer.md
31
29
  </execution_context>
@@ -54,7 +52,7 @@ Runs the normal exec → reviewer pipeline, then runs this command's process inl
54
52
  /pan:focus-exec --deep-review
55
53
  ```
56
54
 
57
- Per-item deep review during focus campaigns. Useful for high-stakes batches.
55
+ Per-item deep review during focus campaigns: focus-exec runs the hardener and meta-reviewer on the item's changed files and merges them under the item's id (`review-deep merge <item-id>` with no `--reviewer-file`), since an item has no phase or first-pass review. Useful for high-stakes batches.
58
56
 
59
57
  </invocation_modes>
60
58
 
@@ -67,7 +65,7 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
67
65
  - Agent writes its findings to the output path, returns confirmation.
68
66
 
69
67
  3. **Spawn pan-meta-reviewer**:
70
- - Prompt includes: `<files_to_read>` with both reviewer.md AND hardener.md (and representative diff snippets); `<output_path>` = `.planning/reviews/<N>/meta.md`.
68
+ - Prompt includes: `<files_to_read>` with both `{directory}/{phase_number}-review.md` AND `.planning/reviews/<N>/hardener.md` (and representative diff snippets); `<output_path>` = `.planning/reviews/<N>/meta.md`.
71
69
  - Agent reads both first-pass reports, identifies missed patterns, disputes overstated severities, writes to output path.
72
70
 
73
71
  4. **Merge** — call:
@@ -87,13 +85,13 @@ Per-item deep review during focus campaigns. Useful for high-stakes batches.
87
85
 
88
86
  | Verdict | Meaning | Action |
89
87
  |---------|---------|--------|
90
- | `ok` | No findings at any severity | Merge freely |
91
- | `ok_with_minor` | Only low/info findings | Merge with noted follow-ups |
88
+ | `ok` | No findings, or info findings only | Merge freely |
89
+ | `ok_with_minor` | Only low findings (info alone gives `ok`) | Merge with noted follow-ups |
92
90
  | `fix_before_merge` | Medium findings present | Fix or document before merge |
93
91
  | `review_required` | High findings present | Human sign-off required |
94
92
  | `block` | At least one critical | Do not merge |
95
93
 
96
- Verdict is driven by the highest-severity finding across all three sources. Meta-reviewer disputes can downgrade severity on specific findings but don't change the headline verdict — the merger trusts the consensus of the explicit severity labels.
94
+ Verdict is driven by the highest-severity finding across all three sources. Meta-reviewer disputes are listed in the conflict table but change no severity, so they never lower the headline verdict; the meta-reviewer's own findings count toward it like the other two sources'.
97
95
 
98
96
  </verdict_semantics>
99
97
 
@@ -114,8 +112,8 @@ Verdict is driven by the highest-severity finding across all three sources. Meta
114
112
  | Claude Code | Full; reasoning depth from the agent's `effort:` frontmatter | Full | Full |
115
113
  | OpenCode | Prose "think step-by-step" preamble substitutes for thinking | Same | Full (runtime-agnostic CLI) |
116
114
  | Gemini | Same | Same | Full |
117
- | Codex | Same | Same | Full |
118
- | Copilot | Same | Same | Full |
115
+ | Codex | Native — `effort:` becomes the agent TOML's `model_reasoning_effort` | Same | Full |
116
+ | Copilot | Prose "think step-by-step" preamble, as on OpenCode | Same | Full |
119
117
 
120
118
  The merger CLI (`pan-tools review-deep merge`) is pure Node.js and works identically across runtimes. Only the *quality* of the hardener and meta-reviewer outputs varies with model capability.
121
119
 
@@ -15,7 +15,7 @@ Interactive configuration of PAN workflow agents and model profile via multi-que
15
15
  Routes to the settings workflow which handles:
16
16
  - Config existence ensuring
17
17
  - Current settings reading and parsing
18
- - Interactive 5-question prompt (model, research, plan_check, verifier, branching)
18
+ - Interactive multi-question prompt (model profile, research, plan_check, verifier, auto_advance, nyquist_validation, branching strategy, routing strategy)
19
19
  - Config merging and writing
20
20
  - Confirmation display with quick command references
21
21
  </objective>
@@ -74,17 +74,6 @@ After initial verification of each requirement:
74
74
  This prevents premature FAIL verdicts from incomplete investigation.
75
75
  </reflexion_loop>
76
76
 
77
- <cache_priming>
78
- **Before the verifier agent runs**, prime the prompt cache once. The verifier reads project.md / requirements.md / roadmap.md every run; caching avoids ~15-50K input tokens per invocation.
79
-
80
- Run once:
81
- ```
82
- pan-tools cache prime --summary
83
- ```
84
-
85
- See [plan-phase.md](plan-phase.md) or [exec-phase.md](exec-phase.md) for the full explanation. No-op on non-Claude runtimes.
86
- </cache_priming>
87
-
88
77
  <process>
89
78
  Execute the verify-phase workflow from @~/.claude/pan-wizard-core/workflows/verify-phase.md end-to-end.
90
79
  Preserve all workflow gates (the test-suite gate, the truth/artifact/wiring checks, status determination, the verification report).
@@ -20,7 +20,6 @@ Unchanged from Spec B v1's X-9. Already narrow enough to stand alone.
20
20
  </objective>
21
21
 
22
22
  <execution_context>
23
- @~/.claude/pan-wizard-core/bin/lib/whatif.cjs
24
23
  @~/.claude/agents/pan-counterfactual.md
25
24
  </execution_context>
26
25
 
@@ -56,7 +55,7 @@ If worktree creation fails (not a git repo, dirty tree blocking, etc.), abort wi
56
55
 
57
56
  ### Stage 2 — Spawn pan-counterfactual
58
57
 
59
- Spawn the agent with its working directory set to `worktree_path`. Prompt includes:
58
+ Spawn the agent. A spawn cannot set its working directory — it starts in the main project — so the prompt's `<worktree_path>` is where it must work. Prompt includes:
60
59
  - `<files_to_read>` — the phase plan, any existing summary, the main project's `CLAUDE.md` so the agent understands conventions.
61
60
  - `<scenario>` — the user's scenario text verbatim.
62
61
  - `<worktree_path>` — so the agent knows the safe boundary.
@@ -69,7 +68,7 @@ The agent explores, then returns a JSON payload with `{summary, differences, rec
69
68
  Run (from main tree, NOT worktree):
70
69
 
71
70
  ```
72
- pan-tools whatif report <phase> "<scenario>" --comparison '<agent-json>'
71
+ pan-tools whatif report <phase> "<scenario>" --comparison "$(cat <comparison-json-file>)"
73
72
  ```
74
73
 
75
74
  This writes `.planning/counterfactuals/<phase>-<slug>.md`. The file belongs to the main tree and survives worktree cleanup.
@@ -80,7 +79,7 @@ This writes `.planning/counterfactuals/<phase>-<slug>.md`. The file belongs to t
80
79
  pan-tools whatif cleanup --worktree <path> --branch <name> --force
81
80
  ```
82
81
 
83
- Removes the worktree directory and deletes the counterfactual branch. Best-effort: warnings are surfaced but don't block.
82
+ Removes the worktree directory and deletes the counterfactual branch. Pass the `worktree.worktree_path` and `worktree.branch` that `whatif prepare` returned, unchanged. Cleanup refuses (exit 1, nothing removed) a branch outside `pan-whatif/` and a worktree that is not checked out on that branch, so it can never delete your own work. Otherwise best-effort: warnings are surfaced but don't block.
84
83
 
85
84
  ### Stage 5 — Confirm
86
85
 
@@ -1,18 +1,43 @@
1
1
  #!/usr/bin/env node
2
2
  // Context Monitor - PostToolUse hook
3
- // Reads context metrics from the statusline bridge file and injects
4
- // warnings when context usage is high. This makes the AGENT aware of
5
- // context limits (the statusline only shows the user).
3
+ // Works out how full the agent's context is and injects a note when the host
4
+ // will compact it soon. This makes the AGENT aware of context limits (the
5
+ // statusline only shows the user).
6
6
  //
7
7
  // How it works:
8
8
  // 1. The statusline hook writes metrics to <os tmpdir>/pan-hooks-<uid>/claude-ctx-{session_id}.json
9
9
  // 2. This hook reads those metrics after each tool use
10
- // 3. When remaining context drops below thresholds, it injects a warning
11
- // as additionalContext, which the agent sees in its conversation
10
+ // 3. When the room left before the host compacts drops below thresholds, it
11
+ // injects a note as additionalContext, which the agent sees in its conversation
12
12
  //
13
- // Thresholds:
14
- // WARNING (remaining <= 35%): Agent should wrap up current task
15
- // CRITICAL (remaining <= 25%): Agent should stop immediately and save state
13
+ // Without the status line. `claude -p` never runs the status line (checked
14
+ // 2026-10-04), and a user may run their own, so the bridge is often missing. Then
15
+ // the hook reads the session transcript the payload names: the newest assistant
16
+ // record carries the usage of the API call that issued this tool call, and its
17
+ // input + cache-read + cache-write tokens are what the status line reports as
18
+ // `total_input_tokens` (code.claude.com/docs/en/statusline). The model id on that
19
+ // record gives the window. A tool call inside a subagent is measured against the
20
+ // subagent's own transcript, never the main session's: the payload carries
21
+ // `agent_id` and still names the MAIN transcript (Claude Code 2.1.288, captured
22
+ // 2026-10-04). Only a bounded tail of the file is read.
23
+ //
24
+ // What it measures (O7). Claude Code's percentages are against the full model
25
+ // window, but the host compacts earlier: about 33K short of the auto-compact window
26
+ // (CLAUDE_CODE_AUTO_COMPACT_WINDOW, then `autoCompactWindow` in settings, per model
27
+ // or for all, else the model window) — about 967K on a 1M model by default — or at
28
+ // CLAUDE_AUTOCOMPACT_PCT_OVERRIDE's share of the window when that is lower. With
29
+ // token counts, the room left is measured against that point; without them,
30
+ // against the model window.
31
+ //
32
+ // What it says (O7). No countdown: vendor guidance is that a visible remaining-
33
+ // context figure makes a model wrap up early and cut corners, and a note after a
34
+ // tool result that orders it to STOP reads like an injection. The note asks for a
35
+ // checkpoint in state.md at the next natural stopping point, says compaction is
36
+ // safe because PAN restores the state, and asks for no shortcuts.
37
+ //
38
+ // Thresholds (of the room before compaction):
39
+ // WARNING (left <= 35%): checkpoint at the next natural stopping point
40
+ // CRITICAL (left <= 25%): checkpoint before the next step
16
41
  //
17
42
  // Debounce: 5 tool uses between warnings to avoid spam
18
43
  // Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
@@ -92,23 +117,267 @@ function bridgeDir() {
92
117
  } catch { return null; }
93
118
  }
94
119
 
95
- const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
96
- const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
120
+ const WARNING_THRESHOLD = 35; // room left before compaction <= 35%
121
+ const CRITICAL_THRESHOLD = 25; // room left before compaction <= 25%
97
122
  const STALE_SECONDS = 60; // ignore metrics older than 60s
98
123
  const DEBOUNCE_CALLS = 5; // min tool uses between warnings
124
+ const MIN_COMPACT_WINDOW = 100000;
125
+ const MAX_COMPACT_WINDOW = 1000000;
126
+ // The host compacts this far short of its window, whatever the window: about 967K on
127
+ // a native 1M window (code.claude.com/docs/en/model-config), and on 2026-10-04 a 100K
128
+ // autoCompactWindow compacted after a call that measured 67,032 tokens and not after
129
+ // one that measured 54,371 (harness context-note-headless). A margin proportional to
130
+ // the window would have put that compaction at ~96.7K.
131
+ const COMPACT_MARGIN = 33000;
132
+ const STANDARD_WINDOW = 200000;
133
+ const TRANSCRIPT_TAIL_BYTES = 256 * 1024; // the first read from the end of the transcript
134
+ const TRANSCRIPT_MAX_BYTES = 4 * 1024 * 1024; // never read more than this of it
135
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/; // session and agent ids name files
136
+
137
+ /**
138
+ * The token count at which the host compacts this session, or null when it does
139
+ * not compact: the compaction window less COMPACT_MARGIN, or the percentage
140
+ * override's share of the window when that is lower. Pure over its inputs.
141
+ * modelWindow — context_window_size from the statusline
142
+ * modelId — the model id, for `modelSettings.<id>.autoCompactWindow`
143
+ * env — the environment (CLAUDE_CODE_AUTO_COMPACT_WINDOW,
144
+ * CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, DISABLE_AUTO_COMPACT)
145
+ * settings — settings objects, highest precedence first (local, project, user)
146
+ */
147
+ function compactTrigger({ modelWindow, modelId, env = {}, settings = [] }) {
148
+ if (!Number.isFinite(modelWindow) || modelWindow <= 0) return null;
149
+ if (env.DISABLE_AUTO_COMPACT === '1') return null;
150
+ for (const s of settings) if (s && s.autoCompactEnabled === false) return null;
151
+ const clamp = (n) => Math.min(modelWindow, Math.max(MIN_COMPACT_WINDOW, Math.min(MAX_COMPACT_WINDOW, n)));
152
+ let window = null;
153
+ if (/^\d+$/.test(String(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW || ''))) {
154
+ window = clamp(Number(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW));
155
+ } else {
156
+ for (const s of settings) {
157
+ if (!s || typeof s !== 'object') continue;
158
+ const perModel = modelId && s.modelSettings && s.modelSettings[modelId] ? s.modelSettings[modelId].autoCompactWindow : undefined;
159
+ const v = perModel !== undefined ? perModel : s.autoCompactWindow;
160
+ if (v === 'auto') break; // the tuned default, below
161
+ if (Number.isFinite(v)) { window = clamp(v); break; }
162
+ }
163
+ }
164
+ if (window == null) window = Math.min(modelWindow, MAX_COMPACT_WINDOW);
165
+ let trigger = window - COMPACT_MARGIN;
166
+ const pct = Number(env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE);
167
+ if (Number.isInteger(pct) && pct >= 1 && pct < 100) trigger = Math.min(trigger, Math.floor(window * pct / 100));
168
+ return trigger;
169
+ }
170
+
171
+ /**
172
+ * The room left before compaction, as a percentage, and what it was measured
173
+ * against. Falls back to the model window's remaining_percentage when the bridge
174
+ * carries no token counts or the host does not compact.
175
+ */
176
+ function roomLeft(metrics, opts = {}) {
177
+ const used = Number(metrics.total_input_tokens);
178
+ const trigger = Number.isFinite(used)
179
+ ? compactTrigger({ modelWindow: Number(metrics.context_window_size), modelId: metrics.model_id, env: opts.env || {}, settings: opts.settings || [] })
180
+ : null;
181
+ if (trigger) return { remaining: Math.max(0, Math.min(100, ((trigger - used) / trigger) * 100)), basis: 'compact-window' };
182
+ const r = Number(metrics.remaining_percentage);
183
+ return { remaining: r, basis: 'model-window' };
184
+ }
185
+
186
+ // ─── Without the status line: the transcript ─────────────────────────────────
187
+
188
+ /**
189
+ * Pure: the context of the newest API call in a transcript's lines, scanning back
190
+ * from the end. Returns `{ found: true, used, model }`, `{ found: false, compacted:
191
+ * true }` when a compaction boundary comes first (the context was just rebuilt and
192
+ * no call has measured it yet), or `{ found: false }` when these lines hold no
193
+ * assistant record, so the caller can read further back.
194
+ *
195
+ * `used` is input + cache-read + cache-write tokens, the status line's
196
+ * `total_input_tokens`. Output tokens are left out, as its `used_percentage` leaves
197
+ * them out. A record with no usage (a synthetic or error record) is skipped. In the
198
+ * main transcript a sidechain record (an older host's subagent turn) is skipped; in
199
+ * a subagent's own transcript every record is a sidechain one.
200
+ */
201
+ function lastContextFromLines(lines, { skipSidechain = true } = {}) {
202
+ for (let i = lines.length - 1; i >= 0; i--) {
203
+ const line = lines[i];
204
+ if (!line || (!line.includes('"assistant"') && !line.includes('"compact_boundary"'))) continue;
205
+ let e;
206
+ try { e = JSON.parse(line); } catch { continue; }
207
+ if (!e || typeof e !== 'object') continue;
208
+ if (e.type === 'system' && e.subtype === 'compact_boundary') return { found: false, compacted: true };
209
+ if (e.type !== 'assistant' || (skipSidechain && e.isSidechain === true)) continue;
210
+ const u = e.message && e.message.usage;
211
+ if (!u || typeof u !== 'object') continue;
212
+ const n = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0);
213
+ const used = n(u.input_tokens) + n(u.cache_read_input_tokens) + n(u.cache_creation_input_tokens);
214
+ if (used <= 0) continue;
215
+ return { found: true, used, model: typeof e.message.model === 'string' ? e.message.model : null };
216
+ }
217
+ return { found: false };
218
+ }
219
+
220
+ /**
221
+ * The last `bytes` of a file as text, without the partial line the cut starts in.
222
+ * Defensive only: in well-formed JSONL a line's tail never parses as an object (a
223
+ * nested object is always followed by its parent's closing brace), so keeping the
224
+ * fragment would change nothing; dropping it keeps a fragment from ever being read.
225
+ */
226
+ function readTail(file, bytes) {
227
+ // Read-only; the owner-only mode never applies to an open for reading, but keeps the
228
+ // call in the form PAN's temp-dir rule requires (the path may be under the temp dir).
229
+ const fd = fs.openSync(file, 'r', 0o600);
230
+ try {
231
+ const size = fs.fstatSync(fd).size;
232
+ const start = Math.max(0, size - bytes);
233
+ const buf = Buffer.alloc(size - start);
234
+ fs.readSync(fd, buf, 0, buf.length, start);
235
+ let text = buf.toString('utf8');
236
+ if (start > 0) {
237
+ const nl = text.indexOf('\n');
238
+ text = nl === -1 ? '' : text.slice(nl + 1);
239
+ }
240
+ return { text, whole: start === 0 };
241
+ } finally {
242
+ fs.closeSync(fd);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * The context of the newest API call in a transcript file, or null. Reads the
248
+ * tail, and further back (up to TRANSCRIPT_MAX_BYTES) only while a large tool
249
+ * result hides the last assistant record. Fails open: any error is null.
250
+ */
251
+ function contextFromTranscript(file, { skipSidechain = true, tailBytes = TRANSCRIPT_TAIL_BYTES, maxBytes = TRANSCRIPT_MAX_BYTES } = {}) {
252
+ if (typeof file !== 'string' || !file) return null;
253
+ let bytes = tailBytes;
254
+ for (;;) {
255
+ let tail;
256
+ try { tail = readTail(file, bytes); } catch { return null; }
257
+ const r = lastContextFromLines(tail.text.split('\n'), { skipSidechain });
258
+ if (r.found) return { used: r.used, model: r.model };
259
+ if (r.compacted || tail.whole || bytes >= maxBytes) return null;
260
+ bytes = Math.min(bytes * 4, maxBytes);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Pure: a Claude model id's family and version, or null. Accepts the API ids
266
+ * (`claude-opus-5-5`, `claude-haiku-4-5-20251001`, `claude-3-5-sonnet-20241022`)
267
+ * and marks the forms a third-party provider gives them (`us.anthropic.claude-…-v1:0`
268
+ * on Bedrock, `claude-…@date` on Google Cloud).
269
+ */
270
+ function parseModelId(id) {
271
+ if (typeof id !== 'string' || !id) return null;
272
+ const s = id.toLowerCase().trim().replace(/\[1m\]$/, '');
273
+ const thirdParty = /(^|\.)anthropic\.|@|-v\d+(:\d+)?$|^arn:/.test(s);
274
+ let m = s.match(/claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d{1,2}))?(?!\d)/);
275
+ if (m) return { family: m[1], major: Number(m[2]), minor: m[3] === undefined ? 0 : Number(m[3]), thirdParty };
276
+ m = s.match(/claude-(\d+)(?:-(\d{1,2}))?-(opus|sonnet|haiku)/);
277
+ if (m) return { family: m[3], major: Number(m[1]), minor: m[2] === undefined ? 0 : Number(m[2]), thirdParty };
278
+ return null;
279
+ }
280
+
281
+ /**
282
+ * Pure: the context window of the model that wrote a transcript record, or null
283
+ * when it cannot be known — then the hook says nothing rather than guess. From
284
+ * code.claude.com/docs/en/model-config (read 2026-10-04):
285
+ * - On the Anthropic API, Fable 5.x, Sonnet 5 and later and Opus 4.7 and later
286
+ * run a native 1M window. On Bedrock, Google Cloud and Foundry some of them run
287
+ * 200K, so there the window is unknown.
288
+ * - Opus 4.6 and Sonnet 4.6 (and older) run 200K unless their `[1m]` variant is
289
+ * configured; Haiku runs 200K.
290
+ * - CLAUDE_CODE_DISABLE_1M_CONTEXT=1 holds every model to 200K.
291
+ * - A context already past 200K can only be in a 1M window.
292
+ */
293
+ function modelWindowFor(modelId, { env = {}, settings = [], used = 0 } = {}) {
294
+ if (used > STANDARD_WINDOW) return MAX_COMPACT_WINDOW;
295
+ if (env.CLAUDE_CODE_DISABLE_1M_CONTEXT === '1') return STANDARD_WINDOW;
296
+ const m = parseModelId(modelId);
297
+ if (!m) return null;
298
+ const native1m = m.family === 'fable'
299
+ || (m.family === 'sonnet' && m.major >= 5)
300
+ || (m.family === 'opus' && (m.major > 4 || (m.major === 4 && m.minor >= 7)));
301
+ if (native1m) {
302
+ const thirdParty = m.thirdParty || ['CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY']
303
+ .some((k) => env[k] === '1' || env[k] === 'true');
304
+ return thirdParty ? null : MAX_COMPACT_WINDOW;
305
+ }
306
+ if (m.family === 'haiku') return STANDARD_WINDOW;
307
+ const extended = [env.ANTHROPIC_MODEL, ...settings.map((s) => (s && typeof s === 'object' ? s.model : undefined))]
308
+ .some((v) => typeof v === 'string' && /\[1m\]\s*$/i.test(v));
309
+ return extended ? MAX_COMPACT_WINDOW : STANDARD_WINDOW;
310
+ }
311
+
312
+ /**
313
+ * Metrics shaped like the bridge's, from a transcript: the tokens in context, the
314
+ * model window and the model id. Null when the transcript, its usage or the window
315
+ * cannot be known.
316
+ */
317
+ function metricsFromTranscript(file, { env = {}, settings = [], skipSidechain = true, tailBytes, maxBytes } = {}) {
318
+ const ctx = contextFromTranscript(file, { skipSidechain, tailBytes, maxBytes });
319
+ if (!ctx) return null;
320
+ const window = modelWindowFor(ctx.model, { env, settings, used: ctx.used });
321
+ if (!window) return null;
322
+ return {
323
+ total_input_tokens: ctx.used,
324
+ context_window_size: window,
325
+ ...(ctx.model ? { model_id: ctx.model } : {}),
326
+ remaining_percentage: Math.max(0, 100 - (ctx.used / window) * 100),
327
+ source: 'transcript',
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Which transcript measures this tool call. A call inside a subagent carries
333
+ * `agent_id`; the payload's `transcript_path` is still the main session's, and the
334
+ * subagent's own file is `<dir>/<session>/subagents/agent-<id>.jsonl` (an
335
+ * `agent_transcript_path`, where a host sends one, wins). A subagent the Workflow
336
+ * tool spawned (PAN's native `/pan-*` workflow scripts) is written one level down,
337
+ * under the run that spawned it: `subagents/workflows/<run>/agent-<id>.jsonl`, found
338
+ * the way pan-cost-logger.js resolveAgentTranscript finds it. The directory is read
339
+ * only for that search. Null when the ids are not safe to put in a path.
340
+ */
341
+ function transcriptForPayload(data) {
342
+ if (!data || typeof data !== 'object') return null;
343
+ const agentId = data.agent_id;
344
+ if (agentId === undefined || agentId === null || agentId === '') {
345
+ return typeof data.transcript_path === 'string' && data.transcript_path ? { file: data.transcript_path, subagent: false } : null;
346
+ }
347
+ if (typeof agentId !== 'string' || !SAFE_ID.test(agentId)) return null;
348
+ if (typeof data.agent_transcript_path === 'string' && data.agent_transcript_path) return { file: data.agent_transcript_path, subagent: true };
349
+ if (typeof data.transcript_path !== 'string' || !data.transcript_path) return null;
350
+ if (typeof data.session_id !== 'string' || !SAFE_ID.test(data.session_id)) return null;
351
+ const base = path.dirname(data.transcript_path);
352
+ const subagentsDir = path.join(base, data.session_id, 'subagents');
353
+ const direct = path.join(subagentsDir, `agent-${agentId}.jsonl`);
354
+ if (!fs.existsSync(direct)) {
355
+ const resolvedBase = path.resolve(base);
356
+ let runs = [];
357
+ try { runs = fs.readdirSync(path.join(subagentsDir, 'workflows')); } catch { /* no workflow runs */ }
358
+ for (const run of runs) {
359
+ const nested = path.join(subagentsDir, 'workflows', run, `agent-${agentId}.jsonl`);
360
+ if (path.resolve(nested).startsWith(resolvedBase + path.sep) && fs.existsSync(nested)) return { file: nested, subagent: true };
361
+ }
362
+ }
363
+ return { file: direct, subagent: true };
364
+ }
99
365
 
100
366
  // Pure decision function (exported for tests).
101
367
  //
102
- // metrics — { remaining_percentage, used_pct?, timestamp? } from the bridge
368
+ // metrics — { remaining_percentage, total_input_tokens?, context_window_size?,
369
+ // model_id?, timestamp? } from the bridge, or the same shape from
370
+ // metricsFromTranscript (no timestamp: measured now)
103
371
  // warnState — { callsSinceWarn, lastLevel } from the prior warn file, or null
104
372
  // on the first warning (no file yet / corrupt file)
105
373
  // nowSeconds — current unix time in seconds
374
+ // opts — { env, settings } for the compaction point (see compactTrigger)
106
375
  //
107
376
  // Returns one of:
108
377
  // { action: 'exit' } — nothing to do
109
378
  // { action: 'debounce', warnState } — persist counter, no warn
110
- // { action: 'emit', level, warnState, message } — persist + emit warning
111
- function buildContextWarning(metrics, warnState, nowSeconds) {
379
+ // { action: 'emit', level, warnState, message, basis } — persist + emit the note
380
+ function buildContextWarning(metrics, warnState, nowSeconds, opts = {}) {
112
381
  if (!metrics || typeof metrics !== 'object') return { action: 'exit' };
113
382
 
114
383
  // Ignore stale metrics (statusline stopped updating — e.g. session ended).
@@ -116,7 +385,7 @@ function buildContextWarning(metrics, warnState, nowSeconds) {
116
385
  return { action: 'exit' };
117
386
  }
118
387
 
119
- const remaining = Number(metrics.remaining_percentage);
388
+ const { remaining, basis } = roomLeft(metrics, opts);
120
389
  if (!Number.isFinite(remaining)) return { action: 'exit' };
121
390
 
122
391
  // No warning needed
@@ -140,33 +409,50 @@ function buildContextWarning(metrics, warnState, nowSeconds) {
140
409
  };
141
410
  }
142
411
 
143
- // L39: report Usage and Remaining on the SAME scale so they are internally
144
- // consistent (sum to 100). The bridge's used_pct is the statusline's
145
- // 80%-rescaled figure (80% real usage displays as 100%); pairing it with the
146
- // raw remaining produced self-contradictory numbers like
147
- // "Usage at 94%. Remaining: 25%". Derive used from remaining instead.
148
- const rem = Math.round(remaining);
149
- const used = 100 - rem;
150
-
151
- let message;
152
- if (isCritical) {
153
- message = `CONTEXT MONITOR CRITICAL: Usage at ${used}%. Remaining: ${rem}%. ` +
154
- 'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
155
- 'If using PAN, run /pan:pause to save execution state.';
156
- } else {
157
- message = `CONTEXT MONITOR WARNING: Usage at ${used}%. Remaining: ${rem}%. ` +
158
- 'Begin wrapping up current task. Do not start new complex work. ' +
159
- 'If using PAN, consider /pan:pause to save state.';
160
- }
412
+ // No figures (O7): a countdown makes a model wrap up early, and a percentage is
413
+ // the thing it would count down. The note names the checkpoint, says compaction
414
+ // is safe, and asks for no shortcuts.
415
+ const message = isCritical
416
+ ? 'PAN context note (from the context-monitor hook, not the user): the host will compact this session soon. ' +
417
+ 'Before your next step, make sure .planning/state.md records where you are — the phase, the plan and task, and what comes next; /pan:pause writes it. ' +
418
+ 'Then carry on with the current task as normal. After compaction PAN restores the planning state, so finish the task properly rather than quickly.'
419
+ : 'PAN context note (from the context-monitor hook, not the user): this session\'s context is filling up. ' +
420
+ 'At your next natural stopping point, such as a finished task or a commit, make sure .planning/state.md records where you are; /pan:pause writes it. ' +
421
+ 'Nothing needs to stop: the host compacts on its own and PAN restores the planning state afterwards, so keep working at full quality, with no shortcuts and no skipped verification.';
161
422
 
162
423
  return {
163
424
  action: 'emit',
164
425
  level: currentLevel,
165
426
  warnState: { callsSinceWarn: 0, lastLevel: currentLevel },
166
427
  message,
428
+ basis,
167
429
  };
168
430
  }
169
431
 
432
+ /**
433
+ * Claude Code's settings for a project, highest precedence first: local, project, user.
434
+ * The user file lives in CLAUDE_CONFIG_DIR when that is set, as Claude Code reads it.
435
+ * A given homeDir is a test seam and ignores CLAUDE_CONFIG_DIR (as the statusline and
436
+ * hygiene do), unless configDir is passed too.
437
+ */
438
+ function readClaudeSettings(projectDir, homeArg, configArg) {
439
+ const seam = homeArg !== undefined;
440
+ const homeDir = seam ? homeArg : os.homedir();
441
+ const configDir = configArg !== undefined ? configArg : (seam ? '' : (process.env.CLAUDE_CONFIG_DIR || ''));
442
+ // Assembled, not a literal: the installer rewrites quoted .claude literals in hook
443
+ // copies, and these are Claude Code's own settings (only it writes the bridge).
444
+ const dir = ['.', 'claude'].join('');
445
+ const files = [];
446
+ if (typeof projectDir === 'string' && projectDir) files.push(path.join(projectDir, dir, 'settings.local.json'), path.join(projectDir, dir, 'settings.json'));
447
+ if (configDir) files.push(path.join(configDir, 'settings.json'));
448
+ else if (homeDir) files.push(path.join(homeDir, dir, 'settings.json'));
449
+ const out = [];
450
+ for (const f of files) {
451
+ try { out.push(JSON.parse(fs.readFileSync(f, 'utf8'))); } catch { /* absent or unreadable */ }
452
+ }
453
+ return out;
454
+ }
455
+
170
456
  function main() {
171
457
  let input = '';
172
458
  process.stdin.setEncoding('utf8');
@@ -177,27 +463,37 @@ function main() {
177
463
  if (deferToClaudeRegistration(data.cwd || process.cwd())) process.exit(0);
178
464
  const sessionId = data.session_id;
179
465
 
180
- if (!sessionId) {
466
+ if (typeof sessionId !== 'string' || !SAFE_ID.test(sessionId)) {
181
467
  process.exit(0);
182
468
  }
183
469
 
184
470
  const tmpDir = bridgeDir();
185
471
  if (!tmpDir) process.exit(0); // insecure/unavailable bridge dir — fail closed (M60)
186
- const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
472
+ const now = Math.floor(Date.now() / 1000);
473
+ const settings = readClaudeSettings(data.cwd || process.cwd());
474
+ const source = transcriptForPayload(data);
475
+ const subagent = !!(source && source.subagent) || (data.agent_id !== undefined && data.agent_id !== null && data.agent_id !== '');
187
476
 
188
- // Read metrics directly; absence (subagent/fresh session) or a corrupt
189
- // file just means "nothing to warn about" — exit silently. No
477
+ // The bridge is the main session's context (the status line renders the main
478
+ // thread), so only a main-thread call reads it, and only while it is fresh.
479
+ // Absence or a corrupt file falls through to the transcript. No
190
480
  // existsSync-then-read gap.
191
- let metrics;
192
- try {
193
- metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
194
- } catch {
195
- process.exit(0);
481
+ let metrics = null;
482
+ if (!subagent) {
483
+ try {
484
+ const bridge = JSON.parse(fs.readFileSync(path.join(tmpDir, `claude-ctx-${sessionId}.json`), 'utf8'));
485
+ if (bridge && typeof bridge === 'object' && !(bridge.timestamp != null && (now - bridge.timestamp) > STALE_SECONDS)) metrics = bridge;
486
+ } catch { /* no bridge: no status line, or not PAN's */ }
196
487
  }
197
- const now = Math.floor(Date.now() / 1000);
488
+ if (!metrics && source) {
489
+ metrics = metricsFromTranscript(source.file, { env: process.env, settings, skipSidechain: !source.subagent });
490
+ }
491
+ if (!metrics) process.exit(0);
198
492
 
199
493
  // Load prior warn state (null when no file / corrupt → treated as first warn).
200
- const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
494
+ // A subagent debounces on its own: its context is not the main session's.
495
+ const warnKey = source && source.subagent ? `${sessionId}-${data.agent_id}` : sessionId;
496
+ const warnPath = path.join(tmpDir, `claude-ctx-${warnKey}-warned.json`);
201
497
  let warnData = null;
202
498
  try {
203
499
  warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
@@ -205,7 +501,10 @@ function main() {
205
501
  warnData = null;
206
502
  }
207
503
 
208
- const decision = buildContextWarning(metrics, warnData, now);
504
+ const decision = buildContextWarning(metrics, warnData, now, {
505
+ env: process.env,
506
+ settings: Number.isFinite(Number(metrics.total_input_tokens)) ? settings : [],
507
+ });
209
508
 
210
509
  if (decision.action === 'exit') {
211
510
  process.exit(0);
@@ -241,7 +540,16 @@ if (require.main === module) {
241
540
  module.exports = {
242
541
  deferToClaudeRegistration,
243
542
  buildContextWarning,
543
+ compactTrigger,
544
+ roomLeft,
545
+ readClaudeSettings,
244
546
  bridgeDir,
547
+ lastContextFromLines,
548
+ contextFromTranscript,
549
+ parseModelId,
550
+ modelWindowFor,
551
+ metricsFromTranscript,
552
+ transcriptForPayload,
245
553
  WARNING_THRESHOLD,
246
554
  CRITICAL_THRESHOLD,
247
555
  STALE_SECONDS,