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.
- package/README.md +29 -26
- package/agents/pan-conductor.md +18 -17
- package/agents/pan-counterfactual.md +2 -2
- package/agents/pan-debugger.md +30 -9
- package/agents/pan-design-checker.md +2 -1
- package/agents/pan-designer.md +2 -2
- package/agents/pan-distiller.md +3 -3
- package/agents/pan-document_code.md +14 -14
- package/agents/pan-executor.md +30 -9
- package/agents/pan-experiment-runner.md +9 -8
- package/agents/pan-hardener.md +1 -1
- package/agents/pan-integration-checker.md +2 -2
- package/agents/pan-knowledge.md +1 -1
- package/agents/pan-meta-reviewer.md +4 -4
- package/agents/pan-optimizer.md +39 -43
- package/agents/pan-phase-researcher.md +10 -10
- package/agents/pan-plan-checker.md +37 -34
- package/agents/pan-planner.md +57 -44
- package/agents/pan-previewer.md +1 -1
- package/agents/pan-project-researcher.md +12 -12
- package/agents/pan-release.md +1 -1
- package/agents/pan-research-synthesizer.md +3 -3
- package/agents/pan-reviewer.md +26 -3
- package/agents/pan-roadmapper.md +15 -14
- package/agents/pan-verifier.md +33 -25
- package/bin/install-lib.cjs +176 -38
- package/bin/install.js +81 -63
- package/commands/pan/army.md +15 -11
- package/commands/pan/assumptions.md +2 -2
- package/commands/pan/audit-deployment.md +11 -12
- package/commands/pan/cost.md +8 -8
- package/commands/pan/dashboard.md +0 -3
- package/commands/pan/debug.md +10 -7
- package/commands/pan/design-phase.md +5 -5
- package/commands/pan/discuss-phase.md +2 -0
- package/commands/pan/exec-phase.md +21 -37
- package/commands/pan/experiment.md +9 -8
- package/commands/pan/focus-auto.md +22 -20
- package/commands/pan/focus-design.md +7 -6
- package/commands/pan/focus-doc-audit.md +4 -3
- package/commands/pan/focus-drift-walking.md +4 -3
- package/commands/pan/focus-exec.md +6 -6
- package/commands/pan/focus-plan.md +12 -11
- package/commands/pan/focus-scan.md +5 -3
- package/commands/pan/focus-sync.md +3 -2
- package/commands/pan/git.md +8 -8
- package/commands/pan/health.md +2 -2
- package/commands/pan/hud.md +4 -7
- package/commands/pan/hygiene.md +14 -7
- package/commands/pan/knowledge.md +9 -10
- package/commands/pan/learn.md +10 -7
- package/commands/pan/links.md +4 -1
- package/commands/pan/map-codebase.md +1 -1
- package/commands/pan/mcp-bridge.md +19 -21
- package/commands/pan/milestone-done.md +6 -3
- package/commands/pan/milestone-new.md +1 -1
- package/commands/pan/new-project.md +10 -10
- package/commands/pan/optimize.md +18 -14
- package/commands/pan/patches.md +2 -6
- package/commands/pan/pause.md +1 -1
- package/commands/pan/phase-tests.md +3 -2
- package/commands/pan/plan-phase.md +9 -19
- package/commands/pan/preview.md +2 -3
- package/commands/pan/profile.md +2 -2
- package/commands/pan/quick.md +1 -1
- package/commands/pan/remove-phase.md +1 -1
- package/commands/pan/report.md +3 -7
- package/commands/pan/research-phase.md +16 -15
- package/commands/pan/resume.md +9 -11
- package/commands/pan/retro.md +5 -3
- package/commands/pan/review-deep.md +7 -9
- package/commands/pan/settings.md +1 -1
- package/commands/pan/verify-phase.md +0 -11
- package/commands/pan/what-if.md +3 -4
- package/hooks/dist/pan-context-monitor.js +352 -44
- package/hooks/dist/pan-cost-logger.js +30 -0
- package/hooks/dist/pan-guard.js +244 -0
- package/hooks/dist/pan-statusline.js +73 -24
- package/hooks/dist/pan-stop-guard.js +7 -2
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +44 -6
- package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
- package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
- package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
- package/pan-wizard-core/bin/lib/commands.cjs +57 -6
- package/pan-wizard-core/bin/lib/config.cjs +7 -1
- package/pan-wizard-core/bin/lib/constants.cjs +18 -6
- package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
- package/pan-wizard-core/bin/lib/core.cjs +53 -19
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
- package/pan-wizard-core/bin/lib/cost.cjs +142 -13
- package/pan-wizard-core/bin/lib/distill.cjs +6 -3
- package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
- package/pan-wizard-core/bin/lib/findings.cjs +135 -21
- package/pan-wizard-core/bin/lib/focus.cjs +13 -4
- package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
- package/pan-wizard-core/bin/lib/hygiene.cjs +184 -17
- package/pan-wizard-core/bin/lib/init.cjs +13 -4
- package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
- package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
- package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
- package/pan-wizard-core/bin/lib/memory.cjs +508 -54
- package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +97 -17
- package/pan-wizard-core/bin/lib/phase-gate.cjs +174 -0
- package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
- package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
- package/pan-wizard-core/bin/lib/phase.cjs +39 -13
- package/pan-wizard-core/bin/lib/preview.cjs +34 -31
- package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
- package/pan-wizard-core/bin/lib/runner.cjs +2 -1
- package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
- package/pan-wizard-core/bin/lib/state.cjs +43 -28
- package/pan-wizard-core/bin/lib/template.cjs +11 -4
- package/pan-wizard-core/bin/lib/verdict.cjs +140 -10
- package/pan-wizard-core/bin/lib/verify-integrity.cjs +181 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +230 -0
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
- package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
- package/pan-wizard-core/bin/lib/verify-scope.cjs +11 -7
- package/pan-wizard-core/bin/lib/verify.cjs +30 -28
- package/pan-wizard-core/bin/lib/whatif.cjs +53 -9
- package/pan-wizard-core/bin/pan-tools.cjs +114 -19
- package/pan-wizard-core/learnings/index.json +5 -5
- package/pan-wizard-core/mcp/merge-gate.cjs +14 -3
- package/pan-wizard-core/mcp/native-tools.cjs +3 -3
- package/pan-wizard-core/opencode/pan-wizard.js +139 -0
- package/pan-wizard-core/references/checkpoints.md +3 -3
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/git-integration.md +4 -6
- package/pan-wizard-core/references/git-planning-commit.md +2 -2
- package/pan-wizard-core/references/guardrails.md +2 -2
- package/pan-wizard-core/references/handoff-decisions.md +3 -3
- package/pan-wizard-core/references/model-profile-resolution.md +5 -5
- package/pan-wizard-core/references/model-profiles.md +15 -10
- package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
- package/pan-wizard-core/references/planning-config.md +7 -7
- package/pan-wizard-core/references/tdd.md +10 -1
- package/pan-wizard-core/references/verdict-contract.md +6 -1
- package/pan-wizard-core/references/verification-patterns.md +5 -5
- package/pan-wizard-core/templates/codebase/architecture.md +2 -2
- package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
- package/pan-wizard-core/templates/codebase/concerns.md +3 -3
- package/pan-wizard-core/templates/codebase/integrations.md +6 -6
- package/pan-wizard-core/templates/codebase/relationships.md +2 -2
- package/pan-wizard-core/templates/codebase/stack.md +2 -2
- package/pan-wizard-core/templates/codebase/structure.md +4 -4
- package/pan-wizard-core/templates/codebase/testing.md +4 -4
- package/pan-wizard-core/templates/config.json +2 -6
- package/pan-wizard-core/templates/continue-here.md +1 -1
- package/pan-wizard-core/templates/debug-subagent-prompt.md +5 -3
- package/pan-wizard-core/templates/debug.md +15 -1
- package/pan-wizard-core/templates/milestone-archive.md +6 -6
- package/pan-wizard-core/templates/milestone.md +1 -1
- package/pan-wizard-core/templates/phase-prompt.md +31 -17
- package/pan-wizard-core/templates/planner-subagent-prompt.md +5 -5
- package/pan-wizard-core/templates/preview-report.md +1 -1
- package/pan-wizard-core/templates/research-project/architecture.md +3 -3
- package/pan-wizard-core/templates/research-project/features.md +1 -1
- package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
- package/pan-wizard-core/templates/research-project/stack.md +3 -3
- package/pan-wizard-core/templates/research.md +5 -5
- package/pan-wizard-core/templates/retrospective.md +1 -1
- package/pan-wizard-core/templates/roadmap.md +15 -15
- package/pan-wizard-core/templates/state.md +4 -4
- package/pan-wizard-core/templates/summary-complex.md +1 -0
- package/pan-wizard-core/templates/summary-minimal.md +1 -0
- package/pan-wizard-core/templates/summary-standard.md +2 -1
- package/pan-wizard-core/templates/summary.md +18 -2
- package/pan-wizard-core/templates/uat.md +3 -3
- package/pan-wizard-core/templates/user-setup.md +8 -8
- package/pan-wizard-core/templates/verification-report.md +53 -8
- package/pan-wizard-core/workflows/add-phase.md +1 -1
- package/pan-wizard-core/workflows/assumptions.md +1 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +4 -4
- package/pan-wizard-core/workflows/discuss-phase.md +10 -8
- package/pan-wizard-core/workflows/exec-phase.md +75 -95
- package/pan-wizard-core/workflows/execute-plan.md +8 -7
- package/pan-wizard-core/workflows/health.md +13 -12
- package/pan-wizard-core/workflows/help.md +8 -9
- package/pan-wizard-core/workflows/learn.md +13 -11
- package/pan-wizard-core/workflows/map-codebase.md +10 -10
- package/pan-wizard-core/workflows/milestone-audit.md +10 -10
- package/pan-wizard-core/workflows/milestone-gaps.md +18 -25
- package/pan-wizard-core/workflows/milestone-new.md +7 -6
- package/pan-wizard-core/workflows/new-project.md +17 -25
- package/pan-wizard-core/workflows/optimize.md +11 -9
- package/pan-wizard-core/workflows/pause.md +6 -1
- package/pan-wizard-core/workflows/phase-tests.md +4 -4
- package/pan-wizard-core/workflows/plan-phase.md +53 -42
- package/pan-wizard-core/workflows/profile.md +2 -2
- package/pan-wizard-core/workflows/progress.md +6 -8
- package/pan-wizard-core/workflows/quick.md +7 -5
- package/pan-wizard-core/workflows/remove-phase.md +1 -1
- package/pan-wizard-core/workflows/research-phase.md +9 -5
- package/pan-wizard-core/workflows/resume-project.md +4 -6
- package/pan-wizard-core/workflows/retro.md +3 -1
- package/pan-wizard-core/workflows/settings.md +16 -24
- package/pan-wizard-core/workflows/todo-check.md +4 -3
- package/pan-wizard-core/workflows/transition.md +16 -12
- package/pan-wizard-core/workflows/update.md +25 -17
- package/pan-wizard-core/workflows/verify-phase.md +22 -20
- package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
- package/pan-zcode/README.md +7 -7
- package/pan-zcode/bin/install-zcode.js +1 -1
- package/scripts/build-hooks.js +1 -0
- package/scripts/build-plugin.js +3 -1
- package/scripts/generate-skills-docs.py +15 -6
- package/scripts/prompt-lint.cjs +167 -0
- 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
|
|
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
|
|
91
|
-
| `ok_with_minor` | Only low
|
|
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
|
|
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 |
|
|
118
|
-
| Copilot |
|
|
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
|
|
package/commands/pan/settings.md
CHANGED
|
@@ -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
|
|
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).
|
package/commands/pan/what-if.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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; //
|
|
96
|
-
const CRITICAL_THRESHOLD = 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,
|
|
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 }
|
|
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 =
|
|
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
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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
|
-
//
|
|
189
|
-
//
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|