pan-wizard 3.13.1 → 3.15.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 (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -5
  3. package/commands/pan/audit-deployment.md +384 -384
  4. package/commands/pan/focus-auto.md +683 -683
  5. package/commands/pan/focus-doc-audit.md +530 -530
  6. package/commands/pan/focus-drift-walking.md +525 -525
  7. package/commands/pan/git.md +1 -1
  8. package/commands/pan/hud.md +3 -2
  9. package/commands/pan/report.md +70 -0
  10. package/hooks/dist/pan-check-update.js +62 -62
  11. package/hooks/dist/pan-context-monitor.js +134 -122
  12. package/hooks/dist/pan-statusline.js +7 -1
  13. package/package.json +5 -5
  14. package/pan-wizard-core/bin/lib/config.cjs +14 -1
  15. package/pan-wizard-core/bin/lib/core.cjs +6 -2
  16. package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
  17. package/pan-wizard-core/bin/lib/focus.cjs +48 -2
  18. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
  19. package/pan-wizard-core/bin/lib/hud.cjs +202 -17
  20. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -2
  21. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  22. package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
  23. package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
  24. package/pan-wizard-core/bin/lib/phase.cjs +4 -4
  25. package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
  26. package/pan-wizard-core/bin/lib/utils.cjs +171 -171
  27. package/pan-wizard-core/bin/lib/verify.cjs +172 -61
  28. package/pan-wizard-core/bin/pan-tools.cjs +1499 -1463
  29. package/pan-wizard-core/references/checkpoints.md +776 -776
  30. package/pan-wizard-core/references/continuation-format.md +249 -249
  31. package/pan-wizard-core/references/questioning.md +145 -145
  32. package/pan-wizard-core/references/tdd.md +263 -263
  33. package/pan-wizard-core/references/ui-brand.md +160 -160
  34. package/pan-wizard-core/templates/config.json +38 -38
  35. package/pan-wizard-core/workflows/exec-phase.md +14 -0
  36. package/scripts/build-hooks.js +51 -51
  37. package/scripts/git-hooks/pre-commit +0 -0
  38. package/scripts/release-check.js +53 -47
  39. package/scripts/run-tests.cjs +44 -0
@@ -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}` — matches `phase_branch_template` in `.planning/config.json`
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
 
@@ -46,7 +46,7 @@ pan-tools hud [--out <file>] [--open] [--stdout]
46
46
 
47
47
  <panels>
48
48
 
49
- The dashboard is composed of up to ten panels. Panels render only when they have data — a plain (non-army) project still gets a complete, useful page.
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,122 +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
- const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
25
- const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
26
- const STALE_SECONDS = 60; // ignore metrics older than 60s
27
- const DEBOUNCE_CALLS = 5; // min tool uses between warnings
28
-
29
- let input = '';
30
- process.stdin.setEncoding('utf8');
31
- process.stdin.on('data', chunk => input += chunk);
32
- process.stdin.on('end', () => {
33
- try {
34
- const data = JSON.parse(input);
35
- const sessionId = data.session_id;
36
-
37
- if (!sessionId) {
38
- process.exit(0);
39
- }
40
-
41
- const tmpDir = os.tmpdir();
42
- const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
43
-
44
- // If no metrics file, this is a subagent or fresh session -- exit silently
45
- if (!fs.existsSync(metricsPath)) {
46
- process.exit(0);
47
- }
48
-
49
- const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
50
- const now = Math.floor(Date.now() / 1000);
51
-
52
- // Ignore stale metrics
53
- if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
54
- process.exit(0);
55
- }
56
-
57
- const remaining = metrics.remaining_percentage;
58
- const usedPct = metrics.used_pct;
59
-
60
- // No warning needed
61
- if (remaining > WARNING_THRESHOLD) {
62
- process.exit(0);
63
- }
64
-
65
- // Debounce: check if we warned recently
66
- const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
67
- let warnData = { callsSinceWarn: 0, lastLevel: null };
68
- let firstWarn = true;
69
-
70
- if (fs.existsSync(warnPath)) {
71
- try {
72
- warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
73
- firstWarn = false;
74
- } catch (e) {
75
- // Corrupted file, reset
76
- }
77
- }
78
-
79
- warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
80
-
81
- const isCritical = remaining <= CRITICAL_THRESHOLD;
82
- const currentLevel = isCritical ? 'critical' : 'warning';
83
-
84
- // Emit immediately on first warning, then debounce subsequent ones
85
- // Severity escalation (WARNING -> CRITICAL) bypasses debounce
86
- const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
87
- if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
88
- // Update counter and exit without warning
89
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
90
- process.exit(0);
91
- }
92
-
93
- // Reset debounce counter
94
- warnData.callsSinceWarn = 0;
95
- warnData.lastLevel = currentLevel;
96
- fs.writeFileSync(warnPath, JSON.stringify(warnData));
97
-
98
- // Build warning message
99
- let message;
100
- if (isCritical) {
101
- message = `CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
102
- 'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
103
- 'If using PAN, run /pan:pause to save execution state.';
104
- } else {
105
- message = `CONTEXT MONITOR WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
106
- 'Begin wrapping up current task. Do not start new complex work. ' +
107
- 'If using PAN, consider /pan:pause to save state.';
108
- }
109
-
110
- const output = {
111
- hookSpecificOutput: {
112
- hookEventName: "PostToolUse",
113
- additionalContext: message
114
- }
115
- };
116
-
117
- process.stdout.write(JSON.stringify(output));
118
- } catch (e) {
119
- // Silent fail -- never block tool execution
120
- process.exit(0);
121
- }
122
- });
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
+ });
@@ -39,7 +39,13 @@ function buildStatuslineOutput(data, deps) {
39
39
 
40
40
  if (session && d.skipBridge !== true) {
41
41
  try {
42
- const bridgePath = pathMod.join(tmpDir, `claude-ctx-${session}.json`);
42
+ // Write the bridge file into a per-user 0700 subdir so another user on
43
+ // a shared host can't symlink-attack the predictable session path.
44
+ // Mirrors bridgeDir() in pan-context-monitor.js (the reader).
45
+ const uid = (typeof process.getuid === 'function' ? process.getuid() : process.env.USERNAME || 'win');
46
+ const bridgeSubdir = pathMod.join(tmpDir, `pan-hooks-${uid}`);
47
+ try { fsMod.mkdirSync(bridgeSubdir, { recursive: true, mode: 0o700 }); } catch { /* best-effort */ }
48
+ const bridgePath = pathMod.join(bridgeSubdir, `claude-ctx-${session}.json`);
43
49
  fsMod.writeFileSync(bridgePath, JSON.stringify({
44
50
  session_id: session,
45
51
  remaining_percentage: remaining,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pan-wizard",
3
- "version": "3.13.1",
3
+ "version": "3.15.0",
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"
@@ -62,10 +62,10 @@
62
62
  "prepare": "node scripts/install-git-hooks.js",
63
63
  "release:check": "node scripts/release-check.js",
64
64
  "prepublishOnly": "node scripts/release-check.js",
65
- "test": "node --test tests/*.test.cjs",
66
- "test:scenarios": "node --test tests/scenarios/*.test.cjs",
67
- "test:all": "node --test tests/*.test.cjs tests/scenarios/*.test.cjs",
68
- "test:e2e": "node --test tests/scenarios/*.test.cjs",
65
+ "test": "node scripts/run-tests.cjs tests",
66
+ "test:scenarios": "node scripts/run-tests.cjs tests/scenarios",
67
+ "test:all": "node scripts/run-tests.cjs tests tests/scenarios",
68
+ "test:e2e": "node scripts/run-tests.cjs tests/scenarios",
69
69
  "test:vscode": "npx playwright test --config tests/e2e/playwright.config.mjs",
70
70
  "test:watch": "node --test --watch tests/*.test.cjs",
71
71
  "build:plugin": "node scripts/build-plugin.js"
@@ -154,10 +154,23 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
154
154
  // After the loop, `current` points to the parent object and the
155
155
  // final segment is used as the property key for assignment.
156
156
  const keys = keyPath.split('.');
157
+
158
+ // Reject prototype-polluting segments up front so a key path like
159
+ // "__proto__.x" or "constructor.prototype.y" can never walk into or mutate
160
+ // Object.prototype. Inline literal guard (no helper) so the check is
161
+ // unambiguous — every remaining assignment is on a vetted key.
162
+ for (const key of keys) {
163
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
164
+ error(`Invalid config key "${keyPath}": __proto__/constructor/prototype are not allowed`);
165
+ return;
166
+ }
167
+ }
168
+
169
+ // Every segment is now vetted; walk the path building intermediate objects.
157
170
  let current = config;
158
171
  for (let i = 0; i < keys.length - 1; i++) {
159
172
  const key = keys[i];
160
- if (current[key] === undefined || typeof current[key] !== 'object') {
173
+ if (!Object.prototype.hasOwnProperty.call(current, key) || typeof current[key] !== 'object' || current[key] === null) {
161
174
  current[key] = {};
162
175
  }
163
176
  current = current[key];
@@ -159,9 +159,13 @@ function output(result, raw, rawValue) {
159
159
  // Large payloads exceed Claude Code's Bash tool buffer (~50KB).
160
160
  // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
161
161
  if (json.length > MAX_JSON_SIZE) {
162
- const tmpPath = path.join(os.tmpdir(), `pan-${Date.now()}.json`);
162
+ // Create a fresh private directory (mkdtemp → unique, unguessable, owned
163
+ // by us) and write inside it, so a pre-planted file or symlink on a
164
+ // shared tmpdir can't be followed or overwritten.
163
165
  try {
164
- fs.writeFileSync(tmpPath, json, 'utf-8');
166
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-'));
167
+ const tmpPath = path.join(tmpDir, 'out.json');
168
+ fs.writeFileSync(tmpPath, json, { encoding: 'utf-8', flag: 'wx' });
165
169
  process.stdout.write('@file:' + tmpPath);
166
170
  } catch {
167
171
  // Tmpfile write failed (disk full, permissions) — truncate and write to stdout