pan-wizard 3.14.0 → 3.15.1
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/LICENSE +21 -21
- package/README.md +2 -2
- package/commands/pan/audit-deployment.md +384 -384
- package/commands/pan/focus-auto.md +683 -683
- package/commands/pan/focus-doc-audit.md +530 -530
- package/commands/pan/focus-drift-walking.md +525 -525
- package/commands/pan/git.md +1 -1
- package/commands/pan/hud.md +3 -2
- package/commands/pan/report.md +70 -0
- package/hooks/dist/pan-check-update.js +62 -62
- package/hooks/dist/pan-context-monitor.js +134 -134
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
- package/pan-wizard-core/bin/lib/hud.cjs +183 -14
- package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
- package/pan-wizard-core/bin/lib/utils.cjs +171 -171
- package/pan-wizard-core/bin/pan-tools.cjs +1499 -1473
- package/pan-wizard-core/references/checkpoints.md +776 -776
- package/pan-wizard-core/references/continuation-format.md +249 -249
- package/pan-wizard-core/references/questioning.md +145 -145
- package/pan-wizard-core/references/tdd.md +263 -263
- package/pan-wizard-core/references/ui-brand.md +160 -160
- package/pan-wizard-core/templates/config.json +38 -38
- package/scripts/build-hooks.js +51 -51
- package/scripts/git-hooks/pre-commit +0 -0
- package/scripts/release-check.js +27 -36
package/commands/pan/git.md
CHANGED
|
@@ -102,7 +102,7 @@ node ~/.claude/pan-wizard-core/bin/pan-tools.cjs git branch delete --name featur
|
|
|
102
102
|
node ~/.claude/pan-wizard-core/bin/pan-tools.cjs git branch current
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
**Phase naming convention:** `pan/phase-{N}` —
|
|
105
|
+
**Phase naming convention:** `pan/phase-{N}` — the branch is named `pan/phase-{N}` directly (hardcoded; not derived from any config template)
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
package/commands/pan/hud.md
CHANGED
|
@@ -46,7 +46,7 @@ pan-tools hud [--out <file>] [--open] [--stdout]
|
|
|
46
46
|
|
|
47
47
|
<panels>
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Panels render only when they have data — a plain (non-army) project still gets a complete, useful page.
|
|
50
50
|
|
|
51
51
|
| Panel | What it shows | Source |
|
|
52
52
|
|-------|---------------|--------|
|
|
@@ -56,11 +56,12 @@ The dashboard is composed of up to ten panels. Panels render only when they have
|
|
|
56
56
|
| **Safety harness** *(army)* | Merge gate, abort switch (pause), active worktrees, daily budget, concurrency | config + pause file + worktrees + schedule |
|
|
57
57
|
| **Worktrees** *(army)* | Active `army/*` branches and their paths | `git worktree list` |
|
|
58
58
|
| **Roadmap** | Every phase with status + completion | phases on disk |
|
|
59
|
+
| **Planning activity** *(fallback)* | Document count by `.planning/` folder + most-recently-updated docs — for projects with no phase/roadmap layout | `.planning/**/*.md` |
|
|
59
60
|
| **Telemetry** | Total spend, tokens, cache-hit rate, by-squad breakdown | cost ledger |
|
|
60
61
|
| **Requirements & quality** | Requirements done/open + last verification artifacts | `requirements.md`, phase `*-verification.md` / `*-uat.md` |
|
|
61
62
|
| **Recent activity** | Last commits (the army's committed output) | `git log` |
|
|
62
63
|
|
|
63
|
-
*(army)* panels appear only when a campaign is scheduled or army worktrees exist — graceful degradation per ADR-0035.
|
|
64
|
+
*(army)* panels appear only when a campaign is scheduled or army worktrees exist — graceful degradation per ADR-0035. The *(fallback)* **Planning activity** panel appears only when there is no phase/roadmap layout, so it fills the gap for focus-auto and imported projects instead of leaving a bare page. Telemetry shows an honest advisory (never a fabricated `$0.00`) when the cost ledger is poisoned or every record is unpriced.
|
|
64
65
|
|
|
65
66
|
</panels>
|
|
66
67
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pan:report
|
|
3
|
+
group: Observability
|
|
4
|
+
description: Generate a self-contained HTML report for one phase, or a project-level timeline index linking every phase report
|
|
5
|
+
argument-hint: "phase <N> | index | all [--out <file>] [--open] [--stdout]"
|
|
6
|
+
allowed-tools:
|
|
7
|
+
- Read
|
|
8
|
+
- Bash
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<objective>
|
|
12
|
+
Render **self-contained HTML reports** — no server, no network, no external CSS or JS — for the phases of a project. A per-phase report captures one phase's story (objective, roadmap position, what changed, verification and gaps); the timeline index rolls every phase into one navigable page that links to each report.
|
|
13
|
+
|
|
14
|
+
Like `pan:hud`, these are *views*, not a new source of truth: every value is read from what PAN already tracks on disk (the phase's `plan`/`summary`/`verification` artifacts and their frontmatter, `roadmap.md`, and the cost ledger). The command only writes the rendered file(s), so it can never corrupt planning data. Reports reuse the HUD's visual language, so they look like part of the same product.
|
|
15
|
+
|
|
16
|
+
Reports are honest by construction: the `verify reconcile` verdict is shown beside the (rubber-stampable) self-reported verification status, status is framed as a current-disk snapshot, and any spend is gated so a poisoned or unpriced ledger never renders a fake `$0`.
|
|
17
|
+
</objective>
|
|
18
|
+
|
|
19
|
+
<execution_context>
|
|
20
|
+
@~/.claude/pan-wizard-core/bin/lib/phase-report.cjs
|
|
21
|
+
</execution_context>
|
|
22
|
+
|
|
23
|
+
<usage>
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
pan-tools report phase <N> [--out <file>] [--open] [--stdout]
|
|
27
|
+
pan-tools report index [--out <file>] [--open] [--stdout]
|
|
28
|
+
pan-tools report all [--open]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Sub-actions:**
|
|
32
|
+
- `phase <N>` — one phase report, written to `.planning/phases/<NN-slug>/<NN>-report.html` (a sibling of that phase's `verification.md`).
|
|
33
|
+
- `index` — the project timeline, written to `.planning/report-index.html`; each row links to a phase report.
|
|
34
|
+
- `all` — regenerate every phase report plus the index in one pass.
|
|
35
|
+
|
|
36
|
+
**Flags:**
|
|
37
|
+
- `--out <file>` — write to a custom path instead of the default (relative paths resolve against the project root).
|
|
38
|
+
- `--open` — best-effort: launch the written file in the default browser (cross-platform; silently no-ops if no opener is available, and never opens when nothing was written).
|
|
39
|
+
- `--stdout` — print the HTML to stdout instead of writing a file (for `phase`/`index`).
|
|
40
|
+
|
|
41
|
+
**Behaviour worth knowing:**
|
|
42
|
+
- **Deterministic writes.** Re-running with unchanged phase data rewrites nothing (the only volatile value, the generated-at timestamp, is ignored when comparing) — so reports produce no git churn.
|
|
43
|
+
- **Phase-less projects.** A project with no phase/roadmap layout (a focus-auto project) has nothing to report; `index` exits with a message pointing you to `pan:hud`, whose planning-activity view covers those projects.
|
|
44
|
+
|
|
45
|
+
**JSON result shape** (`report phase`, when not `--stdout`):
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"action": "phase",
|
|
49
|
+
"phase": "03",
|
|
50
|
+
"path": ".planning/phases/03-auth-sessions/03-report.html",
|
|
51
|
+
"bytes": 16183,
|
|
52
|
+
"status": "complete",
|
|
53
|
+
"written": true,
|
|
54
|
+
"opened": false
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</usage>
|
|
59
|
+
|
|
60
|
+
<workflow>
|
|
61
|
+
|
|
62
|
+
**Review a phase:** run `pan-tools report phase <N> --open` to see one phase's objective, what changed, and its verification verdict at a glance.
|
|
63
|
+
|
|
64
|
+
**Share the project:** `pan-tools report index` builds the timeline entry point — send `.planning/report-index.html` and the linked phase files, or open the index and click through.
|
|
65
|
+
|
|
66
|
+
**Refresh everything:** `pan-tools report all` after a milestone regenerates every report; unchanged ones are skipped, so only what actually moved is rewritten.
|
|
67
|
+
|
|
68
|
+
**Pipe it:** `pan-tools report phase <N> --stdout > phase.html`, or feed the JSON result into another tool.
|
|
69
|
+
|
|
70
|
+
</workflow>
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Check for PAN updates in background, write result to cache
|
|
3
|
-
// Called by SessionStart hook - runs once per session
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const { spawn } = require('child_process');
|
|
9
|
-
|
|
10
|
-
const homeDir = os.homedir();
|
|
11
|
-
const cwd = process.cwd();
|
|
12
|
-
const cacheDir = path.join(homeDir, '.claude', 'cache');
|
|
13
|
-
const cacheFile = path.join(cacheDir, 'pan-update-check.json');
|
|
14
|
-
|
|
15
|
-
// VERSION file locations (check project first, then global)
|
|
16
|
-
const projectVersionFile = path.join(cwd, '.claude', 'pan-wizard-core', 'VERSION');
|
|
17
|
-
const globalVersionFile = path.join(homeDir, '.claude', 'pan-wizard-core', 'VERSION');
|
|
18
|
-
|
|
19
|
-
// Ensure cache directory exists
|
|
20
|
-
if (!fs.existsSync(cacheDir)) {
|
|
21
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Run check in background (spawn background process, windowsHide prevents console flash)
|
|
25
|
-
const child = spawn(process.execPath, ['-e', `
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
const { execSync } = require('child_process');
|
|
28
|
-
|
|
29
|
-
const cacheFile = ${JSON.stringify(cacheFile)};
|
|
30
|
-
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
|
|
31
|
-
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
|
|
32
|
-
|
|
33
|
-
// Check project directory first (local install), then global
|
|
34
|
-
let installed = '0.0.0';
|
|
35
|
-
try {
|
|
36
|
-
if (fs.existsSync(projectVersionFile)) {
|
|
37
|
-
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
|
|
38
|
-
} else if (fs.existsSync(globalVersionFile)) {
|
|
39
|
-
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
|
|
40
|
-
}
|
|
41
|
-
} catch (e) {}
|
|
42
|
-
|
|
43
|
-
let latest = null;
|
|
44
|
-
try {
|
|
45
|
-
latest = execSync('npm view pan-wizard version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
|
46
|
-
} catch (e) {}
|
|
47
|
-
|
|
48
|
-
const result = {
|
|
49
|
-
update_available: latest && installed !== latest,
|
|
50
|
-
installed,
|
|
51
|
-
latest: latest || 'unknown',
|
|
52
|
-
checked: Math.floor(Date.now() / 1000)
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
fs.writeFileSync(cacheFile, JSON.stringify(result));
|
|
56
|
-
`], {
|
|
57
|
-
stdio: 'ignore',
|
|
58
|
-
windowsHide: true,
|
|
59
|
-
detached: true // Required on Windows for proper process detachment
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
child.unref();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Check for PAN updates in background, write result to cache
|
|
3
|
+
// Called by SessionStart hook - runs once per session
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const homeDir = os.homedir();
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const cacheDir = path.join(homeDir, '.claude', 'cache');
|
|
13
|
+
const cacheFile = path.join(cacheDir, 'pan-update-check.json');
|
|
14
|
+
|
|
15
|
+
// VERSION file locations (check project first, then global)
|
|
16
|
+
const projectVersionFile = path.join(cwd, '.claude', 'pan-wizard-core', 'VERSION');
|
|
17
|
+
const globalVersionFile = path.join(homeDir, '.claude', 'pan-wizard-core', 'VERSION');
|
|
18
|
+
|
|
19
|
+
// Ensure cache directory exists
|
|
20
|
+
if (!fs.existsSync(cacheDir)) {
|
|
21
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Run check in background (spawn background process, windowsHide prevents console flash)
|
|
25
|
+
const child = spawn(process.execPath, ['-e', `
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const { execSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const cacheFile = ${JSON.stringify(cacheFile)};
|
|
30
|
+
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
|
|
31
|
+
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
|
|
32
|
+
|
|
33
|
+
// Check project directory first (local install), then global
|
|
34
|
+
let installed = '0.0.0';
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(projectVersionFile)) {
|
|
37
|
+
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
|
|
38
|
+
} else if (fs.existsSync(globalVersionFile)) {
|
|
39
|
+
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
|
|
43
|
+
let latest = null;
|
|
44
|
+
try {
|
|
45
|
+
latest = execSync('npm view pan-wizard version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
|
46
|
+
} catch (e) {}
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
update_available: latest && installed !== latest,
|
|
50
|
+
installed,
|
|
51
|
+
latest: latest || 'unknown',
|
|
52
|
+
checked: Math.floor(Date.now() / 1000)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(cacheFile, JSON.stringify(result));
|
|
56
|
+
`], {
|
|
57
|
+
stdio: 'ignore',
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
detached: true // Required on Windows for proper process detachment
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.unref();
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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).
|
|
6
|
-
//
|
|
7
|
-
// How it works:
|
|
8
|
-
// 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
|
|
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
|
|
12
|
-
//
|
|
13
|
-
// Thresholds:
|
|
14
|
-
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
15
|
-
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
|
16
|
-
//
|
|
17
|
-
// Debounce: 5 tool uses between warnings to avoid spam
|
|
18
|
-
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const os = require('os');
|
|
22
|
-
const path = require('path');
|
|
23
|
-
|
|
24
|
-
// Per-user bridge directory inside tmpdir, created 0700 so another user on a
|
|
25
|
-
// shared host can't pre-plant a symlink at a predictable session path or read
|
|
26
|
-
// the bridge files. Both hooks derive the same dir from the same uid, so the
|
|
27
|
-
// statusline→context-monitor IPC channel is preserved.
|
|
28
|
-
function bridgeDir() {
|
|
29
|
-
const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
|
|
30
|
-
const dir = path.join(os.tmpdir(), `pan-hooks-${uid}`);
|
|
31
|
-
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* best-effort */ }
|
|
32
|
-
return dir;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
36
|
-
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
37
|
-
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
38
|
-
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
39
|
-
|
|
40
|
-
let input = '';
|
|
41
|
-
process.stdin.setEncoding('utf8');
|
|
42
|
-
process.stdin.on('data', chunk => input += chunk);
|
|
43
|
-
process.stdin.on('end', () => {
|
|
44
|
-
try {
|
|
45
|
-
const data = JSON.parse(input);
|
|
46
|
-
const sessionId = data.session_id;
|
|
47
|
-
|
|
48
|
-
if (!sessionId) {
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const tmpDir = bridgeDir();
|
|
53
|
-
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
54
|
-
|
|
55
|
-
// Read metrics directly; absence (subagent/fresh session) or a corrupt
|
|
56
|
-
// file just means "nothing to warn about" — exit silently. No
|
|
57
|
-
// existsSync-then-read gap.
|
|
58
|
-
let metrics;
|
|
59
|
-
try {
|
|
60
|
-
metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
|
61
|
-
} catch {
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}
|
|
64
|
-
const now = Math.floor(Date.now() / 1000);
|
|
65
|
-
|
|
66
|
-
// Ignore stale metrics
|
|
67
|
-
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const remaining = metrics.remaining_percentage;
|
|
72
|
-
const usedPct = metrics.used_pct;
|
|
73
|
-
|
|
74
|
-
// No warning needed
|
|
75
|
-
if (remaining > WARNING_THRESHOLD) {
|
|
76
|
-
process.exit(0);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Debounce: check if we warned recently
|
|
80
|
-
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
81
|
-
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
82
|
-
let firstWarn = true;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
|
86
|
-
firstWarn = false;
|
|
87
|
-
} catch {
|
|
88
|
-
// No prior warning file (or corrupted) — treat as first warning.
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
92
|
-
|
|
93
|
-
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
94
|
-
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
95
|
-
|
|
96
|
-
// Emit immediately on first warning, then debounce subsequent ones
|
|
97
|
-
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
|
98
|
-
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
99
|
-
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
100
|
-
// Update counter and exit without warning
|
|
101
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
102
|
-
process.exit(0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Reset debounce counter
|
|
106
|
-
warnData.callsSinceWarn = 0;
|
|
107
|
-
warnData.lastLevel = currentLevel;
|
|
108
|
-
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
109
|
-
|
|
110
|
-
// Build warning message
|
|
111
|
-
let message;
|
|
112
|
-
if (isCritical) {
|
|
113
|
-
message = `CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
114
|
-
'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
|
|
115
|
-
'If using PAN, run /pan:pause to save execution state.';
|
|
116
|
-
} else {
|
|
117
|
-
message = `CONTEXT MONITOR WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
118
|
-
'Begin wrapping up current task. Do not start new complex work. ' +
|
|
119
|
-
'If using PAN, consider /pan:pause to save state.';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const output = {
|
|
123
|
-
hookSpecificOutput: {
|
|
124
|
-
hookEventName: "PostToolUse",
|
|
125
|
-
additionalContext: message
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
process.stdout.write(JSON.stringify(output));
|
|
130
|
-
} catch (e) {
|
|
131
|
-
// Silent fail -- never block tool execution
|
|
132
|
-
process.exit(0);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
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).
|
|
6
|
+
//
|
|
7
|
+
// How it works:
|
|
8
|
+
// 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
|
|
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
|
|
12
|
+
//
|
|
13
|
+
// Thresholds:
|
|
14
|
+
// WARNING (remaining <= 35%): Agent should wrap up current task
|
|
15
|
+
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
|
|
16
|
+
//
|
|
17
|
+
// Debounce: 5 tool uses between warnings to avoid spam
|
|
18
|
+
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Per-user bridge directory inside tmpdir, created 0700 so another user on a
|
|
25
|
+
// shared host can't pre-plant a symlink at a predictable session path or read
|
|
26
|
+
// the bridge files. Both hooks derive the same dir from the same uid, so the
|
|
27
|
+
// statusline→context-monitor IPC channel is preserved.
|
|
28
|
+
function bridgeDir() {
|
|
29
|
+
const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
|
|
30
|
+
const dir = path.join(os.tmpdir(), `pan-hooks-${uid}`);
|
|
31
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* best-effort */ }
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
|
|
36
|
+
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
|
|
37
|
+
const STALE_SECONDS = 60; // ignore metrics older than 60s
|
|
38
|
+
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
|
|
39
|
+
|
|
40
|
+
let input = '';
|
|
41
|
+
process.stdin.setEncoding('utf8');
|
|
42
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
43
|
+
process.stdin.on('end', () => {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(input);
|
|
46
|
+
const sessionId = data.session_id;
|
|
47
|
+
|
|
48
|
+
if (!sessionId) {
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tmpDir = bridgeDir();
|
|
53
|
+
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
|
|
54
|
+
|
|
55
|
+
// Read metrics directly; absence (subagent/fresh session) or a corrupt
|
|
56
|
+
// file just means "nothing to warn about" — exit silently. No
|
|
57
|
+
// existsSync-then-read gap.
|
|
58
|
+
let metrics;
|
|
59
|
+
try {
|
|
60
|
+
metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
|
|
61
|
+
} catch {
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
const now = Math.floor(Date.now() / 1000);
|
|
65
|
+
|
|
66
|
+
// Ignore stale metrics
|
|
67
|
+
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const remaining = metrics.remaining_percentage;
|
|
72
|
+
const usedPct = metrics.used_pct;
|
|
73
|
+
|
|
74
|
+
// No warning needed
|
|
75
|
+
if (remaining > WARNING_THRESHOLD) {
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Debounce: check if we warned recently
|
|
80
|
+
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
|
|
81
|
+
let warnData = { callsSinceWarn: 0, lastLevel: null };
|
|
82
|
+
let firstWarn = true;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
|
|
86
|
+
firstWarn = false;
|
|
87
|
+
} catch {
|
|
88
|
+
// No prior warning file (or corrupted) — treat as first warning.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
|
|
92
|
+
|
|
93
|
+
const isCritical = remaining <= CRITICAL_THRESHOLD;
|
|
94
|
+
const currentLevel = isCritical ? 'critical' : 'warning';
|
|
95
|
+
|
|
96
|
+
// Emit immediately on first warning, then debounce subsequent ones
|
|
97
|
+
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
|
|
98
|
+
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
|
|
99
|
+
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
|
|
100
|
+
// Update counter and exit without warning
|
|
101
|
+
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Reset debounce counter
|
|
106
|
+
warnData.callsSinceWarn = 0;
|
|
107
|
+
warnData.lastLevel = currentLevel;
|
|
108
|
+
fs.writeFileSync(warnPath, JSON.stringify(warnData));
|
|
109
|
+
|
|
110
|
+
// Build warning message
|
|
111
|
+
let message;
|
|
112
|
+
if (isCritical) {
|
|
113
|
+
message = `CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
114
|
+
'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
|
|
115
|
+
'If using PAN, run /pan:pause to save execution state.';
|
|
116
|
+
} else {
|
|
117
|
+
message = `CONTEXT MONITOR WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
|
|
118
|
+
'Begin wrapping up current task. Do not start new complex work. ' +
|
|
119
|
+
'If using PAN, consider /pan:pause to save state.';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const output = {
|
|
123
|
+
hookSpecificOutput: {
|
|
124
|
+
hookEventName: "PostToolUse",
|
|
125
|
+
additionalContext: message
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
process.stdout.write(JSON.stringify(output));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Silent fail -- never block tool execution
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.1",
|
|
4
4
|
"description": "Command a bot army for your codebase: an Opus Mission Control delegates whole-project goals to specialist squads and ships behind a human merge gate. Five AI CLIs, zero context rot.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pan-wizard": "bin/install.js"
|