pan-wizard 3.12.3 → 3.12.5
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/agents/pan-debugger.md +2 -2
- package/agents/pan-hardener.md +4 -2
- package/agents/pan-meta-reviewer.md +1 -1
- package/agents/pan-reviewer.md +1 -1
- package/bin/install.js +3 -2
- package/hooks/dist/pan-cost-logger.js +54 -6
- package/hooks/dist/pan-trace-logger.js +41 -5
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/cost.cjs +26 -1
- package/pan-wizard-core/bin/lib/hud.cjs +14 -2
- package/pan-wizard-core/references/model-profiles.md +20 -1
package/agents/pan-debugger.md
CHANGED
|
@@ -132,13 +132,13 @@ A good hypothesis can be proven wrong. If you can't design an experiment to disp
|
|
|
132
132
|
|
|
133
133
|
Before running any experiments, think through at least **three independent hypotheses** that could explain the observed failure. For each, write down a one-line Bayesian prior ("90% likely given the symptom", "30%", etc.) based on how well it fits the evidence and how common the failure class is in this codebase.
|
|
134
134
|
|
|
135
|
-
Then **
|
|
135
|
+
Then **investigate the top two in parallel**: emit the `Read`, `Grep`, and log-inspection tool calls for both hypotheses in a single turn. Only serialize when a hypothesis's next step strictly depends on data from a previous step.
|
|
136
136
|
|
|
137
137
|
- If the top hypothesis is confirmed, stop — don't also debug the lower-ranked ones.
|
|
138
138
|
- If the top two are both refuted, rank the remaining hypotheses and repeat.
|
|
139
139
|
- Record each hypothesis's prior and final verdict in the debug session file so later steps can see the tree.
|
|
140
140
|
|
|
141
|
-
Parallel exploration keeps investigation bounded: 3 priors × 2-parallel
|
|
141
|
+
Parallel exploration keeps investigation bounded: 3 priors × 2-parallel investigation = at most 3 rounds before you have a clear winner, rather than walking a depth-first chain of 10 dead ends.
|
|
142
142
|
|
|
143
143
|
## Experimental Design Framework
|
|
144
144
|
|
package/agents/pan-hardener.md
CHANGED
|
@@ -9,6 +9,8 @@ effort: high
|
|
|
9
9
|
<role>
|
|
10
10
|
You are the PAN hardener. You perform focused security review on files changed during phase execution, applying OWASP Top 10 (2025) and STRIDE threat modeling frameworks.
|
|
11
11
|
|
|
12
|
+
This is **authorized, defensive** secure-coding review of the user's own codebase — the goal is to find and fix weaknesses before shipping. You report findings for the user to remediate; you never write exploit code, attack tooling, or step-by-step intrusion instructions.
|
|
13
|
+
|
|
12
14
|
You are spawned by `/pan:review-deep <phase>` or `/pan:exec-phase --deep-review`. Your output is read by `pan-meta-reviewer` (cross-checks you) and merged by `review-deep.cjs` into `.planning/reviews/<phase>/deep-review.md`.
|
|
13
15
|
|
|
14
16
|
**You NEVER modify files.** You report findings; the user fixes them.
|
|
@@ -51,7 +53,7 @@ Before writing findings, think through:
|
|
|
51
53
|
|
|
52
54
|
1. **What changed in this phase?** Read the diff or plan.md files list. Map changes to OWASP categories — e.g. "new endpoint added" → A01+A03 scan; "new SQL query" → A03 scan.
|
|
53
55
|
2. **Does this touch auth, data, or secrets?** These categories get the most thorough STRIDE pass. Changes to `logger.js` or docs don't.
|
|
54
|
-
3. **
|
|
56
|
+
3. **How could this be reached and abused?** For every new surface, trace how it could be reached and what the impact would be, so you can prioritize the fix. If you can't identify a realistic path in 30 seconds, note the effort and move on — don't fabricate threats.
|
|
55
57
|
4. **Cross-check: did the reviewer already flag this?** You'll be merged with their output. Duplicating their `use parameterized queries` finding is OK but prefer adding severity (reviewer says INFO, you say HIGH because it's in an auth path).
|
|
56
58
|
|
|
57
59
|
</reasoning_protocol>
|
|
@@ -91,7 +93,7 @@ generated: <ISO timestamp>
|
|
|
91
93
|
```
|
|
92
94
|
|
|
93
95
|
**Severity scale:**
|
|
94
|
-
- `critical` —
|
|
96
|
+
- `critical` — remotely reachable with no prerequisites; use sparingly, only when one misuse leads to data loss or remote code execution.
|
|
95
97
|
- `high` — exploitable with typical user privileges; blocks merge by default.
|
|
96
98
|
- `medium` — defense-in-depth issue; fix before production but won't block merge if documented.
|
|
97
99
|
- `low` — best-practice deviation; nice to fix.
|
|
@@ -16,7 +16,7 @@ You are the PAN meta-reviewer. Your job is to check the first-pass reviewers (`p
|
|
|
16
16
|
|
|
17
17
|
You are spawned by `/pan:review-deep <phase>` after both the reviewer and hardener have written their reports. Your output is merged with theirs by `review-deep.cjs`.
|
|
18
18
|
|
|
19
|
-
**You NEVER modify source code.** You produce one findings file.
|
|
19
|
+
**You NEVER modify source code.** You produce one findings file. This is authorized, defensive review of the user's own codebase — you adjudicate security findings for remediation; never produce exploit code.
|
|
20
20
|
|
|
21
21
|
**CRITICAL: Mandatory Initial Read**
|
|
22
22
|
If the prompt contains a `<files_to_read>` block (it will contain the reviewer and hardener outputs + representative diff snippets), you MUST use the `Read` tool to load every file listed there before performing any other actions.
|
package/agents/pan-reviewer.md
CHANGED
|
@@ -9,7 +9,7 @@ effort: medium
|
|
|
9
9
|
<role>
|
|
10
10
|
You are a PAN code reviewer. You perform read-only code review on files changed during phase execution.
|
|
11
11
|
|
|
12
|
-
Your job: Check convention compliance, security patterns, and code quality. You do NOT modify files — you report findings.
|
|
12
|
+
Your job: Check convention compliance, security patterns, and code quality. You do NOT modify files — you report findings. This is authorized, defensive review of the user's own codebase — surface security-relevant findings for the user to remediate; never produce exploit code.
|
|
13
13
|
|
|
14
14
|
**CRITICAL: Mandatory Initial Read**
|
|
15
15
|
If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
|
package/bin/install.js
CHANGED
|
@@ -2490,8 +2490,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
2490
2490
|
!caps.has_thinking ? 'extended thinking (E-3, E-10, E-11)' : null,
|
|
2491
2491
|
].filter(Boolean).join(', ');
|
|
2492
2492
|
console.log(`
|
|
2493
|
-
${yellow}ℹ${reset} PAN
|
|
2494
|
-
Features degrade gracefully, but
|
|
2493
|
+
${yellow}ℹ${reset} PAN's multi-agent workflows are tuned for frontier reasoning models. Default model "${modelField}" lacks: ${missing}.
|
|
2494
|
+
Features degrade gracefully, but for best results select claude-fable-5 (PAN's recommended flagship — deepest
|
|
2495
|
+
long-horizon reasoning for the bot army) or claude-opus-4-8 (same 1M context at half the cost).`);
|
|
2495
2496
|
}
|
|
2496
2497
|
}
|
|
2497
2498
|
} catch {
|
|
@@ -18,6 +18,29 @@ const path = require('path');
|
|
|
18
18
|
|
|
19
19
|
const METRICS_DIR = 'metrics';
|
|
20
20
|
const TOKENS_FILE = 'tokens.jsonl';
|
|
21
|
+
const CURSOR_FILE = '.cost-cursor.json';
|
|
22
|
+
|
|
23
|
+
// Per-transcript high-water mark: the count of JSONL records already attributed
|
|
24
|
+
// to earlier SubagentStop events, keyed by transcript path. Each event then sums
|
|
25
|
+
// ONLY its own slice (records past the cursor) instead of re-summing the whole
|
|
26
|
+
// shared-session transcript every time — the latter multiplies cumulative-per-turn
|
|
27
|
+
// cache-read into the billions/trillions and stamps it onto every subagent record
|
|
28
|
+
// (field report 2026-06). Stored next to tokens.jsonl; best-effort, never blocks.
|
|
29
|
+
function cursorFilePath(cwd) {
|
|
30
|
+
return path.join(cwd, '.planning', METRICS_DIR, CURSOR_FILE);
|
|
31
|
+
}
|
|
32
|
+
function readCursor(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const c = JSON.parse(fs.readFileSync(cursorFilePath(cwd), 'utf-8'));
|
|
35
|
+
return c && typeof c === 'object' ? c : {};
|
|
36
|
+
} catch { return {}; }
|
|
37
|
+
}
|
|
38
|
+
function writeCursor(cwd, cursor) {
|
|
39
|
+
try {
|
|
40
|
+
fs.mkdirSync(path.dirname(cursorFilePath(cwd)), { recursive: true });
|
|
41
|
+
fs.writeFileSync(cursorFilePath(cwd), JSON.stringify(cursor), 'utf-8');
|
|
42
|
+
} catch { /* best-effort — never block the agent loop */ }
|
|
43
|
+
}
|
|
21
44
|
|
|
22
45
|
/**
|
|
23
46
|
* Extract what we can from the SubagentStop event payload.
|
|
@@ -49,7 +72,12 @@ function buildCostRecord(data, cwd) {
|
|
|
49
72
|
let model = typeof data.model === 'string' && data.model ? data.model : null;
|
|
50
73
|
const needUsage = (inputTokens + outputTokens + cacheRead + cacheWrite) === 0;
|
|
51
74
|
if ((needUsage || !model) && data.transcript_path) {
|
|
52
|
-
|
|
75
|
+
// Attribute only the transcript slice since the previous SubagentStop for
|
|
76
|
+
// this transcript, so a shared-session transcript is never re-summed on
|
|
77
|
+
// every event (field report 2026-06 — the billion-token cache-read bug).
|
|
78
|
+
const cursor = readCursor(cwd);
|
|
79
|
+
const since = cursor[data.transcript_path] || 0;
|
|
80
|
+
const fromTranscript = readUsageFromTranscript(data.transcript_path, data.session_id, since);
|
|
53
81
|
if (needUsage) {
|
|
54
82
|
inputTokens = fromTranscript.input_tokens;
|
|
55
83
|
outputTokens = fromTranscript.output_tokens;
|
|
@@ -57,6 +85,12 @@ function buildCostRecord(data, cwd) {
|
|
|
57
85
|
cacheWrite = fromTranscript.cache_creation_input_tokens;
|
|
58
86
|
}
|
|
59
87
|
if (!model) model = fromTranscript.model;
|
|
88
|
+
// Advance the cursor to the end of the transcript so the next subagent's
|
|
89
|
+
// record starts fresh (these slices partition the transcript — no overlap).
|
|
90
|
+
if (fromTranscript.lineCount > since) {
|
|
91
|
+
cursor[data.transcript_path] = fromTranscript.lineCount;
|
|
92
|
+
writeCursor(cwd, cursor);
|
|
93
|
+
}
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
const record = {
|
|
@@ -85,23 +119,36 @@ function extractNumber(obj, key) {
|
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
/**
|
|
88
|
-
* P-1805 (v3.7.8): read transcript JSONL and sum usage across
|
|
89
|
-
*
|
|
90
|
-
*
|
|
122
|
+
* P-1805 (v3.7.8): read transcript JSONL and sum usage across assistant messages.
|
|
123
|
+
*
|
|
124
|
+
* `sinceLine` (P-360, field report 2026-06): skip the first N non-empty records —
|
|
125
|
+
* the count already attributed to earlier SubagentStop events for this transcript.
|
|
126
|
+
* Summing only the slice past the cursor is what stops a shared-session transcript
|
|
127
|
+
* from being re-summed on every event (which multiplied cumulative-per-turn
|
|
128
|
+
* cache-read into the billions). Returns `lineCount` = total non-empty records seen
|
|
129
|
+
* so the caller can advance the cursor. Returns zeros if missing/unreadable.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} transcriptPath
|
|
132
|
+
* @param {string} sessionId
|
|
133
|
+
* @param {number} [sinceLine=0] - records already attributed (the cursor)
|
|
91
134
|
*/
|
|
92
|
-
function readUsageFromTranscript(transcriptPath, sessionId) {
|
|
135
|
+
function readUsageFromTranscript(transcriptPath, sessionId, sinceLine = 0) {
|
|
93
136
|
const totals = {
|
|
94
137
|
input_tokens: 0,
|
|
95
138
|
output_tokens: 0,
|
|
96
139
|
cache_read_input_tokens: 0,
|
|
97
140
|
cache_creation_input_tokens: 0,
|
|
98
141
|
model: null,
|
|
142
|
+
lineCount: 0,
|
|
99
143
|
};
|
|
100
144
|
if (!transcriptPath || typeof transcriptPath !== 'string') return totals;
|
|
101
145
|
let raw;
|
|
102
146
|
try { raw = fs.readFileSync(transcriptPath, 'utf-8'); } catch { return totals; }
|
|
147
|
+
let seen = 0; // count of non-empty JSONL records (the cursor unit)
|
|
103
148
|
for (const line of raw.split('\n')) {
|
|
104
149
|
if (!line) continue;
|
|
150
|
+
seen++;
|
|
151
|
+
if (seen <= sinceLine) continue; // already attributed to an earlier event
|
|
105
152
|
let entry;
|
|
106
153
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
107
154
|
if (sessionId && entry.session_id && entry.session_id !== sessionId) continue;
|
|
@@ -120,6 +167,7 @@ function readUsageFromTranscript(transcriptPath, sessionId) {
|
|
|
120
167
|
totals.cache_read_input_tokens += extractNumber(usage, 'cache_read_input_tokens');
|
|
121
168
|
totals.cache_creation_input_tokens += extractNumber(usage, 'cache_creation_input_tokens');
|
|
122
169
|
}
|
|
170
|
+
totals.lineCount = seen;
|
|
123
171
|
return totals;
|
|
124
172
|
}
|
|
125
173
|
|
|
@@ -164,4 +212,4 @@ if (require.main === module) {
|
|
|
164
212
|
});
|
|
165
213
|
}
|
|
166
214
|
|
|
167
|
-
module.exports = { buildCostRecord, appendRecord, readUsageFromTranscript, METRICS_DIR, TOKENS_FILE };
|
|
215
|
+
module.exports = { buildCostRecord, appendRecord, readUsageFromTranscript, readCursor, writeCursor, METRICS_DIR, TOKENS_FILE, CURSOR_FILE };
|
|
@@ -39,6 +39,24 @@ function getCurrentSessionId(cwd) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const TRACE_CURSOR_FILE = '.trace-cursor.json';
|
|
43
|
+
|
|
44
|
+
// Per-transcript high-water mark (see pan-cost-logger.js for the full rationale):
|
|
45
|
+
// sum only the transcript slice since this hook's previous SubagentStop, so a
|
|
46
|
+
// shared-session transcript isn't re-summed on every event (which inflates
|
|
47
|
+
// cumulative-per-turn cache-read into the billions — field report 2026-06).
|
|
48
|
+
// Trace-logger keeps its OWN cursor: cost-logger fires on the same event and the
|
|
49
|
+
// two must not consume each other's slice.
|
|
50
|
+
function traceCursorPath(cwd) { return path.join(getOptimizeDir(cwd), TRACE_CURSOR_FILE); }
|
|
51
|
+
function readTraceCursor(cwd) {
|
|
52
|
+
try { const c = JSON.parse(fs.readFileSync(traceCursorPath(cwd), 'utf-8')); return c && typeof c === 'object' ? c : {}; }
|
|
53
|
+
catch { return {}; }
|
|
54
|
+
}
|
|
55
|
+
function writeTraceCursor(cwd, cursor) {
|
|
56
|
+
try { fs.mkdirSync(path.dirname(traceCursorPath(cwd)), { recursive: true }); fs.writeFileSync(traceCursorPath(cwd), JSON.stringify(cursor), 'utf-8'); }
|
|
57
|
+
catch { /* best-effort — never block the agent loop */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/**
|
|
43
61
|
* Ensure a trace session exists. If none is active, create a day-scoped
|
|
44
62
|
* auto-session so tracing works across the whole flow without manual init.
|
|
@@ -99,12 +117,13 @@ function extractNumber(obj, key) {
|
|
|
99
117
|
* @param {string} [sessionId] - Optional subagent session_id to filter on
|
|
100
118
|
* @returns {Object} usage totals object
|
|
101
119
|
*/
|
|
102
|
-
function readUsageFromTranscript(transcriptPath, sessionId) {
|
|
120
|
+
function readUsageFromTranscript(transcriptPath, sessionId, sinceLine = 0) {
|
|
103
121
|
const totals = {
|
|
104
122
|
input_tokens: 0,
|
|
105
123
|
output_tokens: 0,
|
|
106
124
|
cache_read_input_tokens: 0,
|
|
107
125
|
cache_creation_input_tokens: 0,
|
|
126
|
+
lineCount: 0,
|
|
108
127
|
};
|
|
109
128
|
if (!transcriptPath || typeof transcriptPath !== 'string') return totals;
|
|
110
129
|
let raw;
|
|
@@ -113,8 +132,11 @@ function readUsageFromTranscript(transcriptPath, sessionId) {
|
|
|
113
132
|
} catch {
|
|
114
133
|
return totals;
|
|
115
134
|
}
|
|
135
|
+
let seen = 0; // count of non-empty JSONL records (the cursor unit)
|
|
116
136
|
for (const line of raw.split('\n')) {
|
|
117
137
|
if (!line) continue;
|
|
138
|
+
seen++;
|
|
139
|
+
if (seen <= sinceLine) continue; // already attributed to an earlier event
|
|
118
140
|
let entry;
|
|
119
141
|
try {
|
|
120
142
|
entry = JSON.parse(line);
|
|
@@ -136,17 +158,25 @@ function readUsageFromTranscript(transcriptPath, sessionId) {
|
|
|
136
158
|
totals.cache_read_input_tokens += extractNumber(usage, 'cache_read_input_tokens');
|
|
137
159
|
totals.cache_creation_input_tokens += extractNumber(usage, 'cache_creation_input_tokens');
|
|
138
160
|
}
|
|
161
|
+
totals.lineCount = seen;
|
|
139
162
|
return totals;
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
/**
|
|
143
166
|
* Build trace event(s) from a SubagentStop payload.
|
|
144
|
-
*
|
|
167
|
+
*
|
|
168
|
+
* When the payload lacks usage and `cwd` is supplied, this advances a
|
|
169
|
+
* per-transcript cursor (its only side effect) so each event is attributed
|
|
170
|
+
* just its own transcript slice — never the whole shared-session transcript
|
|
171
|
+
* re-summed every event (field report 2026-06). Without `cwd` it falls back to
|
|
172
|
+
* the legacy whole-transcript read (used only when a transcript_path is given).
|
|
145
173
|
*
|
|
146
174
|
* @param {Object} data - SubagentStop event payload
|
|
175
|
+
* @param {string} sessionId - active trace session id
|
|
176
|
+
* @param {string} [cwd] - project root, enables per-transcript delta attribution
|
|
147
177
|
* @returns {Object[]} Array of trace event records
|
|
148
178
|
*/
|
|
149
|
-
function buildTraceEvents(data, sessionId) {
|
|
179
|
+
function buildTraceEvents(data, sessionId, cwd) {
|
|
150
180
|
if (!data || typeof data !== 'object') return [];
|
|
151
181
|
if (data.hook_event_name && data.hook_event_name !== 'SubagentStop') return [];
|
|
152
182
|
|
|
@@ -160,10 +190,16 @@ function buildTraceEvents(data, sessionId) {
|
|
|
160
190
|
let outputTokens = extractNumber(data.usage, 'output_tokens');
|
|
161
191
|
let cacheRead = extractNumber(data.usage, 'cache_read_input_tokens');
|
|
162
192
|
if ((inputTokens + outputTokens + cacheRead) === 0 && data.transcript_path) {
|
|
163
|
-
const
|
|
193
|
+
const cursor = readTraceCursor(cwd);
|
|
194
|
+
const since = cursor[data.transcript_path] || 0;
|
|
195
|
+
const fromTranscript = readUsageFromTranscript(data.transcript_path, data.session_id, since);
|
|
164
196
|
inputTokens = fromTranscript.input_tokens;
|
|
165
197
|
outputTokens = fromTranscript.output_tokens;
|
|
166
198
|
cacheRead = fromTranscript.cache_read_input_tokens;
|
|
199
|
+
if (cwd && fromTranscript.lineCount > since) {
|
|
200
|
+
cursor[data.transcript_path] = fromTranscript.lineCount;
|
|
201
|
+
writeTraceCursor(cwd, cursor);
|
|
202
|
+
}
|
|
167
203
|
}
|
|
168
204
|
const totalTokens = inputTokens + outputTokens;
|
|
169
205
|
|
|
@@ -245,7 +281,7 @@ if (require.main === module) {
|
|
|
245
281
|
const cwd = data.cwd || data.workspace?.current_dir || process.cwd();
|
|
246
282
|
// Always ensure a session exists — creates a day-scoped auto-session if needed
|
|
247
283
|
const sessionId = ensureSessionId(cwd);
|
|
248
|
-
const events = buildTraceEvents(data, sessionId);
|
|
284
|
+
const events = buildTraceEvents(data, sessionId, cwd);
|
|
249
285
|
appendTraceEvents(cwd, events, sessionId);
|
|
250
286
|
} catch {
|
|
251
287
|
// Silent fail
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pan-wizard",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.5",
|
|
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"
|
|
@@ -186,6 +186,25 @@ function readRecords(cwd) {
|
|
|
186
186
|
* @param {Object} [opts] - {since, until, group_by}
|
|
187
187
|
* @returns {Object} Aggregation
|
|
188
188
|
*/
|
|
189
|
+
/**
|
|
190
|
+
* A record is "suspect" when its token counts are physically implausible for a
|
|
191
|
+
* single subagent — the signature of the pre-v3.12.4 transcript-oversum bug
|
|
192
|
+
* (billions of cache-read, cache-read dwarfing input, 100% cache-hit). Such
|
|
193
|
+
* records are quarantined from aggregates so a poisoned ledger can't report
|
|
194
|
+
* millions of dollars. See docs/FIELD-REPORT-army-2026-06.md.
|
|
195
|
+
* @param {Object} r - a cost record
|
|
196
|
+
* @returns {boolean}
|
|
197
|
+
*/
|
|
198
|
+
function isSuspectRecord(r) {
|
|
199
|
+
if (!r || typeof r !== 'object') return false;
|
|
200
|
+
const cr = r.cache_read_tokens || 0;
|
|
201
|
+
const io = (r.input_tokens || 0) + (r.output_tokens || 0);
|
|
202
|
+
if (cr > 5e8) return true; // no scoped subagent re-reads >500M cached tokens
|
|
203
|
+
if (cr > 1e7 && cr > 100 * (io + 1)) return true; // cache-read dwarfs input+output
|
|
204
|
+
if ((r.output_tokens || 0) > 1e7) return true; // ~10M output = cumulative oversum
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
189
208
|
function aggregate(cwd, opts) {
|
|
190
209
|
const records = readRecords(cwd);
|
|
191
210
|
const since = opts?.since ? new Date(opts.since).getTime() : null;
|
|
@@ -202,13 +221,14 @@ function aggregate(cwd, opts) {
|
|
|
202
221
|
});
|
|
203
222
|
|
|
204
223
|
const totals = {
|
|
205
|
-
calls:
|
|
224
|
+
calls: 0,
|
|
206
225
|
input_tokens: 0,
|
|
207
226
|
output_tokens: 0,
|
|
208
227
|
cache_read_tokens: 0,
|
|
209
228
|
cache_write_tokens: 0,
|
|
210
229
|
cost_usd: 0,
|
|
211
230
|
cost_unknown: 0,
|
|
231
|
+
suspect_excluded: 0,
|
|
212
232
|
};
|
|
213
233
|
|
|
214
234
|
const byAgent = {};
|
|
@@ -229,6 +249,10 @@ function aggregate(cwd, opts) {
|
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
for (const r of filtered) {
|
|
252
|
+
// Quarantine physically-impossible records (pre-v3.12.4 transcript-oversum
|
|
253
|
+
// bug) so a poisoned ledger doesn't poison the totals / HUD / /pan:cost.
|
|
254
|
+
if (isSuspectRecord(r)) { totals.suspect_excluded += 1; continue; }
|
|
255
|
+
totals.calls += 1;
|
|
232
256
|
totals.input_tokens += r.input_tokens || 0;
|
|
233
257
|
totals.output_tokens += r.output_tokens || 0;
|
|
234
258
|
totals.cache_read_tokens += r.cache_read_tokens || 0;
|
|
@@ -398,6 +422,7 @@ module.exports = {
|
|
|
398
422
|
appendRecord,
|
|
399
423
|
readRecords,
|
|
400
424
|
aggregate,
|
|
425
|
+
isSuspectRecord,
|
|
401
426
|
renderTable,
|
|
402
427
|
renderChart,
|
|
403
428
|
resolveRate,
|
|
@@ -267,7 +267,7 @@ function collectHudData(cwd, opts = {}) {
|
|
|
267
267
|
return {
|
|
268
268
|
generated_at: now.toISOString(),
|
|
269
269
|
army_active: armyActive,
|
|
270
|
-
project: { name, version, milestone: milestone ? { version: milestone.version, name: milestone.name } : null, core_value: coreValue },
|
|
270
|
+
project: { name, version, dir_name: path.basename(cwd.replace(/[\\/]+$/, '')), milestone: milestone ? { version: milestone.version, name: milestone.name } : null, core_value: coreValue },
|
|
271
271
|
state,
|
|
272
272
|
progress: phaseScan,
|
|
273
273
|
army,
|
|
@@ -425,7 +425,7 @@ function renderMission(d) {
|
|
|
425
425
|
<div class="mhead">
|
|
426
426
|
<div>
|
|
427
427
|
<div class="kicker">pan army · mission control</div>
|
|
428
|
-
<div class="title">${esc(p.name || 'Untitled project')}</div>
|
|
428
|
+
<div class="title">${esc(p.name || p.dir_name || 'Untitled project')}</div>
|
|
429
429
|
${p.core_value ? `<div class="sub">${esc(p.core_value)}</div>` : ''}
|
|
430
430
|
</div>
|
|
431
431
|
<div class="mmeta">
|
|
@@ -586,6 +586,17 @@ function renderHarness(d) {
|
|
|
586
586
|
|
|
587
587
|
function renderTelemetry(d) {
|
|
588
588
|
const t = d.telemetry;
|
|
589
|
+
// A ledger where most records are implausible is the pre-v3.12.4 capture bug —
|
|
590
|
+
// don't present salvaged numbers as if trustworthy; tell the user to reset it.
|
|
591
|
+
if (t.totals.suspect_excluded > t.totals.calls) {
|
|
592
|
+
const total = t.totals.suspect_excluded + t.totals.calls;
|
|
593
|
+
return `
|
|
594
|
+
<section class="panel">
|
|
595
|
+
<div class="ph">telemetry</div>
|
|
596
|
+
<div class="row noborder"><span class="rl">Status</span>${pill('legacy ledger — unreliable', 'warn')}</div>
|
|
597
|
+
<div class="amono dim" style="margin-top:10px;line-height:1.6;">${t.totals.suspect_excluded} of ${total} cost records are implausible (the pre-v3.12.4 telemetry capture bug). Reset the ledger with <b>pan-tools cost clear</b> — records captured after the fix are accurate.</div>
|
|
598
|
+
</section>`;
|
|
599
|
+
}
|
|
589
600
|
const keys = Object.keys(t.by_squad).sort((a, b) => t.by_squad[b].cost - t.by_squad[a].cost);
|
|
590
601
|
const max = keys.reduce((m, k) => Math.max(m, t.by_squad[k].cost), 0) || 1;
|
|
591
602
|
const bars = keys.length
|
|
@@ -602,6 +613,7 @@ function renderTelemetry(d) {
|
|
|
602
613
|
<div class="row"><span class="rl">Total spend</span><span class="amono">${fmtUsd(t.totals.cost_usd)}</span></div>
|
|
603
614
|
<div class="row"><span class="rl">Tokens</span><span class="amono">${fmtTokens(t.totals.input_tokens + t.totals.output_tokens)}</span></div>
|
|
604
615
|
<div class="row"><span class="rl">Cache hit</span><span class="amono okc">${t.cache_hit_rate_pct == null ? 'n/a' : t.cache_hit_rate_pct + '%'}</span></div>
|
|
616
|
+
${t.totals.suspect_excluded ? `<div class="row"><span class="rl">Excluded</span>${pill(t.totals.suspect_excluded + ' implausible records', 'warn')}</div>` : ''}
|
|
605
617
|
<div class="sqbars">${bars}</div>
|
|
606
618
|
</section>`;
|
|
607
619
|
}
|
|
@@ -10,7 +10,7 @@ PAN uses three abstract tiers instead of hardcoded model names:
|
|
|
10
10
|
|
|
11
11
|
| Tier | Purpose | Anthropic | OpenAI | Google |
|
|
12
12
|
|------|---------|-----------|--------|--------|
|
|
13
|
-
| `reasoning` | Architecture, planning, complex decisions | inherit (Opus) | inherit | inherit |
|
|
13
|
+
| `reasoning` | Architecture, planning, complex decisions | inherit (Fable/Opus) | inherit | inherit |
|
|
14
14
|
| `mid` | Execution, research, verification | Sonnet | mid | mid |
|
|
15
15
|
| `fast` | Read-only extraction, budget tasks | Haiku | fast | fast |
|
|
16
16
|
|
|
@@ -28,6 +28,25 @@ For backward compatibility, legacy Anthropic model names still work:
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
+
## Recommended Models (Claude)
|
|
32
|
+
|
|
33
|
+
PAN never selects your host model — it recommends one. Because the `reasoning` tier is `inherit`, whichever top-tier model you configure in your runtime (Claude Code, etc.) runs every reasoning-tier agent (planner, conductor, executor, hardener…). `mid` and `fast` agents stay on Sonnet/Haiku regardless, so the whole fleet is never on one expensive model.
|
|
34
|
+
|
|
35
|
+
| Model | Role in PAN | Context | Relative cost | Notes |
|
|
36
|
+
|-------|-------------|---------|---------------|-------|
|
|
37
|
+
| `claude-fable-5` | **Recommended flagship** — deepest long-horizon reasoning; best for the bot army's Mission Control + planning | 1M | ~2× Opus | Runs input safety classifiers (see caveat below); requires 30-day data retention |
|
|
38
|
+
| `claude-opus-4-8` | **Cost-conscious default** — same 1M context + thinking, half the cost, no cyber classifier | 1M | 1× | The safe pick when you want Opus behavior without Fable's refusal surface |
|
|
39
|
+
|
|
40
|
+
**Why Fable is the recommended flagship.** It is Anthropic's most capable widely released model for demanding, long-horizon agentic work — exactly what PAN's hierarchical bot army (Mission Control → squads → workers) asks of its reasoning tier. Select it in your host runtime and `inherit` routes the reasoning-tier agents to it automatically.
|
|
41
|
+
|
|
42
|
+
**Fable caveat — the cyber-classifier refusal.** Fable is the only current Claude model that runs input safety classifiers targeting cybersecurity and biology content, and benign *defensive* security tooling can trigger false positives. A block surfaces as a successful response with `stop_reason: "refusal"` and a `stop_details.category` (e.g. `cyber`). In PAN this only realistically bites the deep-review flow (`/pan:review-deep`, `exec-phase --deep-review`), where the security agents (`pan-hardener`, `pan-reviewer`, `pan-meta-reviewer`) inspect auth/crypto/injection code. Those agents are framed as **authorized, defensive review** to avoid tripping the classifier. If a review step is still refused, re-run that step on `claude-opus-4-8` — Opus does not gate requests this way.
|
|
43
|
+
|
|
44
|
+
**Fable data-retention requirement.** Fable is not available under zero data retention; an org whose retention is below 30 days gets a hard `400` on every request. If Fable 400s on every call with an otherwise-valid request, check the org's retention setting before debugging anything else.
|
|
45
|
+
|
|
46
|
+
**Prompting note for Fable.** Fable prefers *less-prescriptive* prompts than earlier models and runs longer per turn. PAN's autonomous-loop guidance (anti-overplanning, grounded progress claims, act-when-you-have-enough) already aligns with this; avoid piling on `CRITICAL: YOU MUST` scaffolding, which can reduce Fable's output quality.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
31
50
|
## Profile Definitions
|
|
32
51
|
|
|
33
52
|
| Agent | `quality` | `balanced` | `budget` |
|