pan-wizard 3.29.0 → 3.31.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 +40 -40
- package/bin/install-lib.cjs +186 -83
- package/bin/install.js +213 -195
- package/commands/pan/army.md +4 -4
- package/commands/pan/audit-deployment.md +2 -2
- package/commands/pan/cost.md +5 -5
- package/commands/pan/exec-phase.md +2 -0
- package/commands/pan/focus-auto.md +5 -5
- package/commands/pan/verify-phase.md +8 -9
- package/hooks/dist/pan-check-update.js +53 -2
- package/hooks/dist/pan-context-monitor.js +41 -0
- package/hooks/dist/pan-cost-logger.js +71 -2
- package/hooks/dist/pan-state-reinject.js +158 -0
- package/hooks/dist/pan-stop-guard.js +127 -2
- package/hooks/dist/pan-trace-logger.js +42 -1
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
- package/pan-wizard-core/bin/lib/commands.cjs +20 -4
- package/pan-wizard-core/bin/lib/config.cjs +2 -0
- package/pan-wizard-core/bin/lib/constants.cjs +3 -1
- package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
- package/pan-wizard-core/bin/lib/core.cjs +249 -27
- package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
- package/pan-wizard-core/bin/lib/cost.cjs +234 -88
- package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
- package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
- package/pan-wizard-core/bin/lib/memory-rebuild.cjs +12 -5
- package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
- package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
- package/pan-wizard-core/bin/lib/state.cjs +86 -22
- package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
- package/pan-wizard-core/bin/lib/template.cjs +6 -3
- package/pan-wizard-core/bin/lib/verify-deploy.cjs +20 -3
- package/pan-wizard-core/bin/pan-tools.cjs +10 -1
- package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
- package/pan-wizard-core/references/model-profiles.md +4 -4
- package/pan-wizard-core/references/planning-config.md +19 -23
- package/pan-wizard-core/workflows/exec-phase.md +8 -0
- package/pan-wizard-core/workflows/help.md +2 -2
- package/pan-wizard-core/workflows/phase-tests.md +1 -1
- package/pan-wizard-core/workflows/progress.md +1 -1
- package/pan-wizard-core/workflows/settings.md +2 -4
- package/pan-wizard-core/workflows/todo-add.md +1 -1
- package/pan-wizard-core/workflows/todo-check.md +7 -6
- package/scripts/build-agent-plugin.js +4 -0
- package/scripts/build-hooks.js +2 -1
- package/scripts/build-plugin.js +10 -0
- package/scripts/npm-dist-tag.js +47 -0
- package/scripts/plugin-path.js +6 -1
- package/scripts/test-surface.cjs +11 -9
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
* - hit rate: cache_read / (cache_read + input - cache_write) if any cache activity
|
|
31
31
|
*
|
|
32
32
|
* Rate table is approximate — real pricing comes from the provider's API.
|
|
33
|
-
* Rates are US dollars per million tokens,
|
|
34
|
-
*
|
|
33
|
+
* Rates are US dollars per million tokens, verified against each provider's
|
|
34
|
+
* pricing page on the date in RATES_VERIFIED_AT (below). Users can override with
|
|
35
|
+
* `.planning/config.json` → `cost.rates`.
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
38
|
const fs = require('fs');
|
|
@@ -45,66 +46,120 @@ const TOKENS_FILE = 'tokens.jsonl';
|
|
|
45
46
|
/**
|
|
46
47
|
* Default rate table ($ per million tokens).
|
|
47
48
|
* Override per-model in config.json → cost.rates.
|
|
49
|
+
*
|
|
50
|
+
* `cache_write` is the five-minute cache write. `cache_write_1h` — Anthropic rows
|
|
51
|
+
* only — is the one-hour write, 2x base input on every current Claude model
|
|
52
|
+
* (pricing table "1h cache writes", read 2026-09-26). It prices the
|
|
53
|
+
* `cache_write_1h_tokens` a ledger row carries when the transcript recorded the
|
|
54
|
+
* lifetime split; a row without it, or a rate without `cache_write_1h`, bills every
|
|
55
|
+
* write at `cache_write` (market-ideas queue M13). OpenAI and Google report no
|
|
56
|
+
* lifetime split, so their rows carry none.
|
|
48
57
|
*/
|
|
49
58
|
const DEFAULT_RATES = {
|
|
50
|
-
// Anthropic —
|
|
51
|
-
// $5/$25 (the old $15/$75 Opus pricing ended with
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
'claude-mythos-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5 },
|
|
65
|
-
'claude-mythos-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Opus
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
'claude-opus-
|
|
59
|
+
// Anthropic — platform.claude.com/docs/en/about-claude/pricing, every row read
|
|
60
|
+
// 2026-09-23. Opus 4.6+/Opus 5 are $5/$25 (the old $15/$75 Opus pricing ended with
|
|
61
|
+
// the 4.5 generation). cache_write is the 5-minute write rate (1.25× input; the
|
|
62
|
+
// 1-hour write bills 2×, which the ledger does not split yet). Cache reads follow
|
|
63
|
+
// the 0.1× convention EXCEPT on three models the page footnotes: Fable 5.1 and
|
|
64
|
+
// Mythos 5.1 at 0.025× input, Opus 5.5 at 0.05×. Each needs its own row, because
|
|
65
|
+
// the family-prefix fallback would otherwise price it at its predecessor's reads.
|
|
66
|
+
// Fable 5.1 is what the `fable`/`best` aliases resolve to (model-config: neither
|
|
67
|
+
// Fable model is any plan's default); cached re-reads are the bulk of PAN's
|
|
68
|
+
// traffic (ADR-0044).
|
|
69
|
+
'claude-fable-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5, cache_write_1h: 20.0 },
|
|
70
|
+
'claude-fable-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5, cache_write_1h: 20.0 },
|
|
71
|
+
// Mythos 5.1 / Mythos 5 (limited availability): $10/$50; Mythos 5.1 reads at
|
|
72
|
+
// 0.025× input like Fable 5.1, Mythos 5 follows the 0.1× rule (R7).
|
|
73
|
+
'claude-mythos-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5, cache_write_1h: 20.0 },
|
|
74
|
+
'claude-mythos-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5, cache_write_1h: 20.0 },
|
|
75
|
+
// Opus 5.5 (claude-opus-5-5, released 2026-09-22): $4 input / $20 output, 5-minute
|
|
76
|
+
// writes $5, cache hits $0.20 — the page prices its hits at 0.05× input. Claude
|
|
77
|
+
// Code 2.1.280 made it the default model on every plan (model-config, read
|
|
78
|
+
// 2026-09-23), so PAN's inherit tier now runs on it. Without this row the family
|
|
79
|
+
// prefix priced it as Opus 5: cache reads 2.5× high, the rest a quarter high
|
|
80
|
+
// (reality check 2026-09-22, R25). The models overview lists no dated snapshot —
|
|
81
|
+
// from the 4.6 generation on, the dateless id is canonical.
|
|
82
|
+
'claude-opus-5-5': { input: 4.0, output: 20.0, cache_read: 0.20, cache_write: 5.0, cache_write_1h: 8.0 },
|
|
83
|
+
'claude-opus-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25, cache_write_1h: 10.0 },
|
|
84
|
+
'claude-opus-4-8': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25, cache_write_1h: 10.0 },
|
|
85
|
+
'claude-opus-4-7': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25, cache_write_1h: 10.0 },
|
|
86
|
+
'claude-opus-4-6': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25, cache_write_1h: 10.0 },
|
|
87
|
+
// Opus 4.5 (dated id claude-opus-4-5-20251101): without this row the dated id had
|
|
88
|
+
// no family prefix to land on and priced as null (R7).
|
|
89
|
+
'claude-opus-4-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25, cache_write_1h: 10.0 },
|
|
74
90
|
// Sonnet 5 is $2/$10: the launch price announced as introductory through
|
|
75
|
-
// 2026-08-31 was made permanent and the scheduled rise to $3/$15 cancelled
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
'claude-sonnet-5': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50 },
|
|
79
|
-
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
80
|
-
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
84
|
-
|
|
85
|
-
// OpenAI —
|
|
86
|
-
// input
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// 2026-08-31 was made permanent and the scheduled rise to $3/$15 cancelled.
|
|
92
|
+
// Lesson: never write down a pre-announced price — this row carried the future
|
|
93
|
+
// rate for a month and over-billed by half.
|
|
94
|
+
'claude-sonnet-5': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50, cache_write_1h: 4.0 },
|
|
95
|
+
'claude-sonnet-4-6': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, cache_write_1h: 6.0 },
|
|
96
|
+
// Sonnet 4.5 (dated id claude-sonnet-4-5-20250929): $3/$15/$0.30/$3.75 (R7).
|
|
97
|
+
'claude-sonnet-4-5': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, cache_write_1h: 6.0 },
|
|
98
|
+
// Haiku 4.5: API id claude-haiku-4-5-20251001, alias claude-haiku-4-5.
|
|
99
|
+
'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25, cache_write_1h: 2.0 },
|
|
100
|
+
|
|
101
|
+
// OpenAI — developers.openai.com/api/docs/pricing, Standard tier, short context
|
|
102
|
+
// (≤272K input tokens), read 2026-09-23. On the GPT-5.6 and GPT-6 families a cache
|
|
103
|
+
// write bills at 1.25× input IN PLACE of the input rate ("writes are not an
|
|
104
|
+
// additive fee"); GPT-5.5 charges no cache write ("-"), so its write tokens bill
|
|
105
|
+
// as plain input. Until 2026-09-22 this block said "no separate write charge" for
|
|
106
|
+
// every row, which had stopped being true — Terra and Luna writes were a fifth
|
|
107
|
+
// low (R27). Longest-prefix match lets each tier key win over a shorter one.
|
|
108
|
+
// GPT-6: Astra is the flagship; Sol and Luna were released 2026-09-22. Codex's
|
|
109
|
+
// config docs use gpt-6-sol as the default model and gpt-6-luna for lower-cost
|
|
110
|
+
// subagents (learn.chatgpt.com/docs/config-file/config-basic, /agent-configuration/subagents).
|
|
111
|
+
'gpt-6-astra': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
|
|
112
|
+
'gpt-6-sol': { input: 2.0, output: 10.0, cache_read: 0.2, cache_write: 2.5 },
|
|
113
|
+
'gpt-6-luna': { input: 0.10, output: 0.50, cache_read: 0.01, cache_write: 0.125 },
|
|
114
|
+
// GPT-5.6. The bare `gpt-5.6` id is an alias that "routes requests to GPT-5.6 Sol"
|
|
115
|
+
// (its model page, read 2026-09-23), so it carries Sol's row — not the $5/$30 it
|
|
116
|
+
// carried before, which no page listed. Sol's price is promotional "at least
|
|
117
|
+
// through November 21, 2026": re-read it after that date.
|
|
118
|
+
'gpt-5.6': { input: 4.0, output: 20.0, cache_read: 0.4, cache_write: 5.0 },
|
|
119
|
+
'gpt-5.6-sol': { input: 4.0, output: 20.0, cache_read: 0.4, cache_write: 5.0 },
|
|
120
|
+
'gpt-5.6-terra': { input: 2.0, output: 12.0, cache_read: 0.2, cache_write: 2.5 },
|
|
121
|
+
'gpt-5.6-luna': { input: 0.20, output: 1.20, cache_read: 0.02, cache_write: 0.25 },
|
|
122
|
+
// Priced only in the page's separate "Cyber models" table, not in the Standard tiers.
|
|
123
|
+
'gpt-5.6-cyber': { input: 12.5, output: 75.0, cache_read: 1.25, cache_write: 15.625 },
|
|
93
124
|
'gpt-5.5': { input: 5.0, output: 30.0, cache_read: 0.5, cache_write: 5.0 },
|
|
94
125
|
|
|
95
|
-
// Google Gemini —
|
|
96
|
-
//
|
|
126
|
+
// Google Gemini — ai.google.dev/gemini-api/docs/pricing, paid tier, text input,
|
|
127
|
+
// the ≤200K-token prompt tier where the page splits by length, read 2026-09-23.
|
|
128
|
+
// cache_read is the page's "context caching" price: 0.1× input on every row, not
|
|
129
|
+
// the 0.25× this block carried until 2026-09-22 (cache reads 2.5× high, R26). The
|
|
130
|
+
// tokens that CREATE a cache bill at the standard input price (Google Cloud's
|
|
131
|
+
// context-cache overview; the Gemini API pages list only cached-token and storage
|
|
132
|
+
// charges), so cache_write = input. Cache STORAGE bills per million tokens per
|
|
133
|
+
// HOUR, which a per-token table cannot carry: PAN's Gemini totals exclude it.
|
|
134
|
+
// The 3.6/3.7/3.8 Flash prices run "through December 31, 2026" and double from
|
|
135
|
+
// January 1, 2027 (the page states both) — re-read them after that date.
|
|
136
|
+
'gemini-3.8-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
137
|
+
'gemini-3.7-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
138
|
+
'gemini-3.6-flash': { input: 0.75, output: 3.75, cache_read: 0.075, cache_write: 0.75 },
|
|
139
|
+
'gemini-3.5-flash': { input: 1.50, output: 9.00, cache_read: 0.15, cache_write: 1.50 },
|
|
140
|
+
'gemini-3.5-flash-lite': { input: 0.30, output: 2.50, cache_read: 0.03, cache_write: 0.30 },
|
|
141
|
+
// Shutdown announced for 2027-05-07 (deprecations page); replacement gemini-3.5-flash-lite.
|
|
142
|
+
'gemini-3.1-flash-lite': { input: 0.25, output: 1.50, cache_read: 0.025, cache_write: 0.25 },
|
|
143
|
+
'gemini-3-flash-preview': { input: 0.50, output: 3.00, cache_read: 0.05, cache_write: 0.50 },
|
|
144
|
+
// The page lists 3.1 Pro only as gemini-3.1-pro-preview; the bare id keeps the
|
|
145
|
+
// preview's row so records written under it stay priced.
|
|
146
|
+
'gemini-3.1-pro': { input: 2.00, output: 12.0, cache_read: 0.20, cache_write: 2.00 },
|
|
147
|
+
'gemini-3.1-pro-preview': { input: 2.00, output: 12.0, cache_read: 0.20, cache_write: 2.00 },
|
|
148
|
+
// The 2.5 family is "not deprecated", but the models page limits access to users
|
|
149
|
+
// who have used it before — new projects cannot count on it (PAN's Google routing
|
|
150
|
+
// tiers moved off it, R28). Rows kept for the ledgers that recorded it.
|
|
151
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.0, cache_read: 0.125, cache_write: 1.25 },
|
|
152
|
+
'gemini-2.5-flash': { input: 0.30, output: 2.50, cache_read: 0.03, cache_write: 0.30 },
|
|
153
|
+
'gemini-2.5-flash-lite': { input: 0.10, output: 0.40, cache_read: 0.01, cache_write: 0.10 },
|
|
97
154
|
// (gemini-1.5-pro removed 2026-06: retired model; records for it fall back to tier rates.)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
'mid': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
|
|
107
|
-
'fast': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
|
|
155
|
+
|
|
156
|
+
// Tier fallbacks when the model id is unknown: reasoning tracks the default Opus
|
|
157
|
+
// (Opus 5.5 since 2026-09-22 — the model the inherit tier runs on for most users),
|
|
158
|
+
// mid the Sonnet the `sonnet` alias resolves to on the Anthropic API (Sonnet 5),
|
|
159
|
+
// fast Haiku 4.5.
|
|
160
|
+
'reasoning': { input: 4.0, output: 20.0, cache_read: 0.20, cache_write: 5.0, cache_write_1h: 8.0 },
|
|
161
|
+
'mid': { input: 2.0, output: 10.0, cache_read: 0.20, cache_write: 2.50, cache_write_1h: 4.0 },
|
|
162
|
+
'fast': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25, cache_write_1h: 2.0 },
|
|
108
163
|
};
|
|
109
164
|
|
|
110
165
|
function metricsDir(cwd) {
|
|
@@ -128,29 +183,80 @@ function familyPrefixRate(rates, model) {
|
|
|
128
183
|
|
|
129
184
|
// ─── Claude Code `modelPricing` as a rate source (2026-09) ──────────────────
|
|
130
185
|
//
|
|
131
|
-
// Claude Code ≥2.1.
|
|
132
|
-
// MANAGED
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
186
|
+
// Claude Code ≥2.1.242 lets an organisation report spend at contracted rates
|
|
187
|
+
// through a MANAGED setting, and prices its own /usage, status line and
|
|
188
|
+
// telemetry with it. Honouring the same block keeps PAN's ledger on the numbers
|
|
189
|
+
// the organisation actually pays. The documented shape (settings-reference
|
|
190
|
+
// `modelPricing`, read raw 2026-09-26):
|
|
191
|
+
//
|
|
192
|
+
// modelPricing: {
|
|
193
|
+
// multiplier: 0.85, // optional, > 0 and ≤ 10
|
|
194
|
+
// overrides: { // optional
|
|
195
|
+
// "claude-sonnet-4-6": { input, output, cacheRead, cacheWrite } // $/1M, each 0–10000, all four required
|
|
196
|
+
// }
|
|
197
|
+
// }
|
|
198
|
+
//
|
|
199
|
+
// `multiplier` "scales every cost Claude Code computes, whether or not an
|
|
200
|
+
// overrides row covers it" — below 1 a discount, above 1 a chargeback markup
|
|
201
|
+
// (2.1.271). It is applied on top of an override row's rates, and to the list
|
|
202
|
+
// price of every model no row covers. Claude Code drops a row or a multiplier it
|
|
203
|
+
// cannot parse and keeps the rest; PAN does the same.
|
|
204
|
+
//
|
|
205
|
+
// Until 2026-09-26 PAN parsed a different shape — `{ "<modelId>":
|
|
206
|
+
// { inputCostPer1MTokens, outputCostPer1MTokens } }` with cache rates derived —
|
|
207
|
+
// that the documentation does not describe; a real policy file matched nothing
|
|
208
|
+
// and PAN priced at list price. (Market-ideas queue M2 found it while adding the
|
|
209
|
+
// multiplier.)
|
|
139
210
|
|
|
140
211
|
const round6 = (n) => Number(n.toFixed(6));
|
|
141
212
|
|
|
213
|
+
const MODEL_PRICING_MAX_MULTIPLIER = 10;
|
|
214
|
+
const MODEL_PRICING_MAX_RATE = 10000;
|
|
215
|
+
|
|
216
|
+
// resolveRate reads this off a rates map to scale its built-in fallback. A
|
|
217
|
+
// symbol, so Object.keys (the family-prefix match, `models check`) never sees it.
|
|
218
|
+
const BUILTIN_MULTIPLIER = Symbol('pan.builtinRateMultiplier');
|
|
219
|
+
|
|
220
|
+
/** The block's multiplier when it is a valid one, otherwise null. */
|
|
221
|
+
function modelPricingMultiplier(modelPricing) {
|
|
222
|
+
if (!modelPricing || typeof modelPricing !== 'object' || !('multiplier' in modelPricing)) return null;
|
|
223
|
+
const raw = modelPricing.multiplier;
|
|
224
|
+
if (raw === null || raw === '' || typeof raw === 'boolean') return null;
|
|
225
|
+
const m = Number(raw);
|
|
226
|
+
return Number.isFinite(m) && m > 0 && m <= MODEL_PRICING_MAX_MULTIPLIER ? m : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const scaleRate = (rate, m) => ({
|
|
230
|
+
input: round6(rate.input * m),
|
|
231
|
+
output: round6(rate.output * m),
|
|
232
|
+
cache_read: round6(rate.cache_read * m),
|
|
233
|
+
cache_write: round6(rate.cache_write * m),
|
|
234
|
+
...(typeof rate.cache_write_1h === 'number' ? { cache_write_1h: round6(rate.cache_write_1h * m) } : {}),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* PAN rate rows for a managed `modelPricing` block's `overrides`, with the
|
|
239
|
+
* block's multiplier applied. A row missing any of the four rates, or holding
|
|
240
|
+
* one outside 0–10000, is dropped.
|
|
241
|
+
* @param {object|null} modelPricing - `{ multiplier?, overrides? }`
|
|
242
|
+
* @returns {Object<string, {input:number, output:number, cache_read:number, cache_write:number}>}
|
|
243
|
+
*/
|
|
142
244
|
function ratesFromModelPricing(modelPricing) {
|
|
143
245
|
const out = {};
|
|
144
246
|
if (!modelPricing || typeof modelPricing !== 'object' || Array.isArray(modelPricing)) return out;
|
|
145
|
-
|
|
247
|
+
const overrides = modelPricing.overrides;
|
|
248
|
+
if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) return out;
|
|
249
|
+
const m = modelPricingMultiplier(modelPricing) || 1;
|
|
250
|
+
const valid = (v) => v !== null && v !== '' && typeof v !== 'boolean'
|
|
251
|
+
&& Number.isFinite(Number(v)) && Number(v) >= 0 && Number(v) <= MODEL_PRICING_MAX_RATE;
|
|
252
|
+
for (const [id, p] of Object.entries(overrides)) {
|
|
146
253
|
if (!p || typeof p !== 'object') continue;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
out[id] = { input, output, cache_read: round6(input * readMult), cache_write: round6(input * writeMult) };
|
|
254
|
+
if (![p.input, p.output, p.cacheRead, p.cacheWrite].every(valid)) continue;
|
|
255
|
+
// "cacheWrite covers both five-minute and one-hour cache writes."
|
|
256
|
+
out[id] = scaleRate({
|
|
257
|
+
input: Number(p.input), output: Number(p.output),
|
|
258
|
+
cache_read: Number(p.cacheRead), cache_write: Number(p.cacheWrite), cache_write_1h: Number(p.cacheWrite),
|
|
259
|
+
}, m);
|
|
154
260
|
}
|
|
155
261
|
return out;
|
|
156
262
|
}
|
|
@@ -182,25 +288,35 @@ function loadManagedModelPricing(dir = managedSettingsDir()) {
|
|
|
182
288
|
.sort()
|
|
183
289
|
.map(f => path.join(dropIns, f)));
|
|
184
290
|
} catch { /* no drop-in directory */ }
|
|
291
|
+
// Later files win: a later `multiplier` replaces an earlier one, and
|
|
292
|
+
// `overrides` merge per model id.
|
|
185
293
|
let merged = null;
|
|
186
294
|
for (const f of files) {
|
|
187
295
|
let parsed;
|
|
188
296
|
try { parsed = JSON.parse(fs.readFileSync(f, 'utf8')); } catch { continue; }
|
|
189
297
|
const mp = parsed && typeof parsed === 'object' ? parsed.modelPricing : null;
|
|
190
|
-
if (mp
|
|
298
|
+
if (!mp || typeof mp !== 'object' || Array.isArray(mp)) continue;
|
|
299
|
+
merged = merged || {};
|
|
300
|
+
if ('multiplier' in mp) merged.multiplier = mp.multiplier;
|
|
301
|
+
if (mp.overrides && typeof mp.overrides === 'object' && !Array.isArray(mp.overrides)) {
|
|
302
|
+
merged.overrides = { ...(merged.overrides || {}), ...mp.overrides };
|
|
303
|
+
}
|
|
191
304
|
}
|
|
192
305
|
return merged;
|
|
193
306
|
}
|
|
194
307
|
|
|
195
308
|
// The rate table a cost computation actually sees. Precedence, highest first:
|
|
196
|
-
// 1. `.planning/config.json → cost.rates` — PAN's explicit per-project override
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
309
|
+
// 1. `.planning/config.json → cost.rates` — PAN's explicit per-project override,
|
|
310
|
+
// used exactly as written (the managed multiplier does not touch it)
|
|
311
|
+
// 2. managed `modelPricing.overrides` — contracted rates, × the multiplier
|
|
312
|
+
// 3. DEFAULT_RATES — resolveRate's own fallback, × the managed multiplier
|
|
313
|
+
// Returns undefined (not {}) when nothing overrides and no multiplier applies, so
|
|
314
|
+
// callers keep the exact pre-2026-09 behaviour of passing no config rates.
|
|
201
315
|
function effectiveRates(config, managedPricing = loadManagedModelPricing()) {
|
|
202
316
|
const rates = { ...ratesFromModelPricing(managedPricing), ...(config?.cost?.rates || {}) };
|
|
203
|
-
|
|
317
|
+
const m = modelPricingMultiplier(managedPricing);
|
|
318
|
+
if (m !== null && m !== 1) rates[BUILTIN_MULTIPLIER] = m;
|
|
319
|
+
return Object.keys(rates).length > 0 || rates[BUILTIN_MULTIPLIER] ? rates : undefined;
|
|
204
320
|
}
|
|
205
321
|
|
|
206
322
|
function resolveRate(model, tier, configRates) {
|
|
@@ -217,6 +333,12 @@ function resolveRate(model, tier, configRates) {
|
|
|
217
333
|
}
|
|
218
334
|
if (tier && configRates[tier]) return configRates[tier];
|
|
219
335
|
}
|
|
336
|
+
const builtin = builtinRate(model, tier);
|
|
337
|
+
const m = configRates ? configRates[BUILTIN_MULTIPLIER] : undefined;
|
|
338
|
+
return builtin && m ? scaleRate(builtin, m) : builtin;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function builtinRate(model, tier) {
|
|
220
342
|
if (model && DEFAULT_RATES[model]) return DEFAULT_RATES[model];
|
|
221
343
|
if (model) {
|
|
222
344
|
const fam = familyPrefixRate(DEFAULT_RATES, model);
|
|
@@ -252,8 +374,13 @@ function computeCost(rec, configRates) {
|
|
|
252
374
|
// outright. On a realistic row (30k input / 5k output / 200k cache read / 12k
|
|
253
375
|
// cache write on Opus-5 rates) it reported $0.30 against a true $0.45 — a 33%
|
|
254
376
|
// understatement, always in the direction of looking cheaper.
|
|
255
|
-
|
|
256
|
-
|
|
377
|
+
// The one-hour share of the writes, when the row recorded the lifetime split (M13),
|
|
378
|
+
// bills at the one-hour rate; the rest — and every write on a row or rate without
|
|
379
|
+
// the split — at `cache_write`.
|
|
380
|
+
const write1h = Math.min(cacheWrite, Math.max(0, Number(rec.cache_write_1h_tokens) || 0));
|
|
381
|
+
const rate1h = typeof rate.cache_write_1h === 'number' ? rate.cache_write_1h : rate.cache_write;
|
|
382
|
+
const usd = (input * rate.input + output * rate.output + cacheRead * rate.cache_read
|
|
383
|
+
+ (cacheWrite - write1h) * rate.cache_write + write1h * rate1h) / 1_000_000;
|
|
257
384
|
return Math.round(usd * 10000) / 10000;
|
|
258
385
|
}
|
|
259
386
|
|
|
@@ -587,12 +714,22 @@ function cmdCostClear(cwd, raw) {
|
|
|
587
714
|
|
|
588
715
|
// ─── Rate-table staleness ───────────────────────────────────────────────────
|
|
589
716
|
|
|
590
|
-
// Date DEFAULT_RATES was last verified against published provider pricing
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
717
|
+
// Date DEFAULT_RATES was last verified against published provider pricing — ALL
|
|
718
|
+
// providers' pricing pages, not only Anthropic's (the 2026-09-10 bump covered the
|
|
719
|
+
// Anthropic rows alone, and the Gemini and OpenAI rows it vouched for had never
|
|
720
|
+
// matched their pages: reality check 2026-09-22, RC29). Bump it only when every
|
|
721
|
+
// row has been read against its page that day.
|
|
722
|
+
//
|
|
723
|
+
// `models check` flags the table once it is older than RATES_STALE_AFTER_DAYS.
|
|
724
|
+
// That window was half a year until 2026-09-22 (R38), which is longer than the
|
|
725
|
+
// interval at which the lineup now moves: the default Claude model changed on
|
|
726
|
+
// every plan inside twelve days of the last verification. Sixty days is the
|
|
727
|
+
// review cadence the ecosystem reviews settled on. The calendar still cannot see
|
|
728
|
+
// a default-model change inside the window — that is the job of the
|
|
729
|
+
// documented-default-ids fixture test (tests/fixtures/documented-default-models.json,
|
|
730
|
+
// R31), which fails the suite the day a documented default id has no exact row.
|
|
731
|
+
const RATES_VERIFIED_AT = '2026-09-23';
|
|
732
|
+
const RATES_STALE_AFTER_DAYS = 60;
|
|
596
733
|
const RATE_TIERS = ['reasoning', 'mid', 'fast'];
|
|
597
734
|
|
|
598
735
|
function checkRatesStaleness(now = new Date()) {
|
|
@@ -611,7 +748,16 @@ function checkRatesStaleness(now = new Date()) {
|
|
|
611
748
|
function cmdModelsCheck(raw) {
|
|
612
749
|
// Surface the managed rates too: an organisation that pins `modelPricing`
|
|
613
750
|
// should be able to see that PAN found the block, not infer it from totals.
|
|
614
|
-
const
|
|
751
|
+
const managed = loadManagedModelPricing();
|
|
752
|
+
const result = {
|
|
753
|
+
...checkRatesStaleness(),
|
|
754
|
+
managed_model_pricing: Object.keys(ratesFromModelPricing(managed)),
|
|
755
|
+
managed_pricing_multiplier: modelPricingMultiplier(managed),
|
|
756
|
+
};
|
|
757
|
+
// A multiplier PAN cannot apply is reported, not silently dropped.
|
|
758
|
+
if (managed && 'multiplier' in managed && result.managed_pricing_multiplier === null) {
|
|
759
|
+
result.managed_pricing_multiplier_ignored = managed.multiplier;
|
|
760
|
+
}
|
|
615
761
|
const human = result.stale
|
|
616
762
|
? `Rate table verified ${result.rates_verified_at} (${result.age_days} days ago) — STALE: re-verify provider pricing and bump RATES_VERIFIED_AT in cost.cjs`
|
|
617
763
|
: `Rate table verified ${result.rates_verified_at} (${result.age_days} days ago) — OK`;
|
|
@@ -89,8 +89,11 @@ function extractFrontmatter(content) {
|
|
|
89
89
|
const frontmatter = {};
|
|
90
90
|
|
|
91
91
|
// --- Frontmatter delimiter detection ---
|
|
92
|
-
// Match the first YAML block bounded by opening "---\n" and closing "\n---"
|
|
93
|
-
|
|
92
|
+
// Match the first YAML block bounded by opening "---\n" and closing "\n---".
|
|
93
|
+
// CRLF and a leading BOM are normalised first: the LF-only match found no front
|
|
94
|
+
// matter at all in a CRLF file, so every reader saw an empty object.
|
|
95
|
+
const text = String(content == null ? '' : content).replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
96
|
+
const match = text.match(/^---\n([\s\S]+?)\n---/);
|
|
94
97
|
if (!match) return frontmatter;
|
|
95
98
|
|
|
96
99
|
const yaml = match[1];
|
|
@@ -214,17 +214,23 @@ function quarantinePath(cwd) {
|
|
|
214
214
|
* restore an entry by hand. The file leads with a warning so it is never loaded
|
|
215
215
|
* as trusted instruction memory.
|
|
216
216
|
*/
|
|
217
|
+
const QUARANTINE_HEADER = '# Quarantined memory (DO NOT auto-load as instructions)\n\nEntries below were pulled out of standing memory during reconcile because they read like\ndirectives aimed at the agent (possible cross-generation prompt injection). They are\nNOT trusted instructions. Review each; restore to state.md by hand only if legitimate.\n';
|
|
218
|
+
|
|
217
219
|
function appendQuarantine(cwd, entries, now) {
|
|
218
220
|
if (!entries.length) return;
|
|
219
221
|
const p = quarantinePath(cwd);
|
|
220
|
-
const fresh = !fs.existsSync(p);
|
|
221
222
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
223
|
+
// Create the file with its warning header atomically (`wx` fails if it exists)
|
|
224
|
+
// instead of checking existsSync first: between that check and the append a
|
|
225
|
+
// concurrent reconcile could write the header twice, or skip it (CodeQL #46,
|
|
226
|
+
// js/file-system-race).
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(p, QUARANTINE_HEADER, { encoding: 'utf-8', flag: 'wx' });
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e.code !== 'EEXIST') throw e;
|
|
231
|
+
}
|
|
222
232
|
const stamp = now || '(undated)';
|
|
223
|
-
|
|
224
|
-
? '# Quarantined memory (DO NOT auto-load as instructions)\n\nEntries below were pulled out of standing memory during reconcile because they read like\ndirectives aimed at the agent (possible cross-generation prompt injection). They are\nNOT trusted instructions. Review each; restore to state.md by hand only if legitimate.\n'
|
|
225
|
-
: '';
|
|
226
|
-
const block = `${header}\n## Quarantined ${stamp}\n\n${entries.join('\n')}\n`;
|
|
227
|
-
fs.appendFileSync(p, block, 'utf-8');
|
|
233
|
+
fs.appendFileSync(p, `\n## Quarantined ${stamp}\n\n${entries.join('\n')}\n`, 'utf-8');
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
/**
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Three derived targets:
|
|
12
12
|
* 1. AGENTS.md — the universal, cross-runtime tools memory. PAN owns exactly
|
|
13
|
-
* the marker-fenced `<!-- BEGIN/END PAN WIZARD -->` section (
|
|
14
|
-
*
|
|
13
|
+
* the marker-fenced `<!-- BEGIN/END PAN WIZARD -->` section (Codex, Copilot and
|
|
14
|
+
* OpenCode read AGENTS.md natively; Claude via the CLAUDE.md bridge; Gemini reads GEMINI.md by default). User content
|
|
15
15
|
* outside the markers is preserved byte-for-byte.
|
|
16
16
|
* 2. CLAUDE.md — the Claude bridge (`@AGENTS.md` import), regenerated only
|
|
17
17
|
* when the Claude runtime is installed here.
|
|
@@ -126,7 +126,7 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
|
|
|
126
126
|
const targets = [];
|
|
127
127
|
const warnings = [];
|
|
128
128
|
|
|
129
|
-
// 1. AGENTS.md — universal PAN section (
|
|
129
|
+
// 1. AGENTS.md — universal PAN section (Claude Code reads it through the bridge in step 2).
|
|
130
130
|
{
|
|
131
131
|
const p = path.join(cwd, 'AGENTS.md');
|
|
132
132
|
const existing = safeReadFile(p);
|
|
@@ -149,8 +149,15 @@ function cmdMemoryRebuild(cwd, opts = {}, raw) {
|
|
|
149
149
|
const p = path.join(planningPath(cwd), 'state.md');
|
|
150
150
|
const existing = safeReadFile(p);
|
|
151
151
|
if (existing != null) {
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
let desired = null;
|
|
153
|
+
try {
|
|
154
|
+
desired = syncStateFrontmatter(existing, cwd);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// A state.md PAN refuses to re-derive (stacked front matter) is reported,
|
|
157
|
+
// never rewritten on a guess.
|
|
158
|
+
targets.push({ file: '.planning/state.md', action: 'refused', reason: err.message });
|
|
159
|
+
}
|
|
160
|
+
if (desired !== null) targets.push({ file: '.planning/state.md', ...rebuildFile(p, existing, desired, apply) });
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -724,8 +724,11 @@ function cmdReport(cwd, opts = {}, raw) {
|
|
|
724
724
|
if (action === 'all') {
|
|
725
725
|
const res = renderAllToDisk(cwd, { now });
|
|
726
726
|
if (!res) return error('No phases found — nothing to report.');
|
|
727
|
+
// --open was parsed for `all` and never used; open the index it wrote, on the
|
|
728
|
+
// same only-when-written rule as the single-report actions.
|
|
729
|
+
const opened = opts.open && res.index.written ? openInBrowser(res.index.path) : false;
|
|
727
730
|
return output(
|
|
728
|
-
{ action, reports: res.reports, index: res.index },
|
|
731
|
+
{ action, reports: res.reports, index: res.index, opened },
|
|
729
732
|
raw,
|
|
730
733
|
`generated ${res.reports.length} phase report(s)${res.index.path ? ' + index' : ''}`,
|
|
731
734
|
);
|