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.
Files changed (50) hide show
  1. package/README.md +40 -40
  2. package/bin/install-lib.cjs +186 -83
  3. package/bin/install.js +213 -195
  4. package/commands/pan/army.md +4 -4
  5. package/commands/pan/audit-deployment.md +2 -2
  6. package/commands/pan/cost.md +5 -5
  7. package/commands/pan/exec-phase.md +2 -0
  8. package/commands/pan/focus-auto.md +5 -5
  9. package/commands/pan/verify-phase.md +8 -9
  10. package/hooks/dist/pan-check-update.js +53 -2
  11. package/hooks/dist/pan-context-monitor.js +41 -0
  12. package/hooks/dist/pan-cost-logger.js +71 -2
  13. package/hooks/dist/pan-state-reinject.js +158 -0
  14. package/hooks/dist/pan-stop-guard.js +127 -2
  15. package/hooks/dist/pan-trace-logger.js +42 -1
  16. package/package.json +1 -1
  17. package/pan-wizard-core/bin/lib/agents-md.cjs +3 -2
  18. package/pan-wizard-core/bin/lib/commands.cjs +20 -4
  19. package/pan-wizard-core/bin/lib/config.cjs +2 -0
  20. package/pan-wizard-core/bin/lib/constants.cjs +3 -1
  21. package/pan-wizard-core/bin/lib/context-budget.cjs +27 -2
  22. package/pan-wizard-core/bin/lib/core.cjs +249 -27
  23. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +11 -0
  24. package/pan-wizard-core/bin/lib/cost.cjs +234 -88
  25. package/pan-wizard-core/bin/lib/frontmatter.cjs +5 -2
  26. package/pan-wizard-core/bin/lib/memory-optimize.cjs +12 -6
  27. package/pan-wizard-core/bin/lib/memory-rebuild.cjs +12 -5
  28. package/pan-wizard-core/bin/lib/phase-report.cjs +4 -1
  29. package/pan-wizard-core/bin/lib/roadmap.cjs +87 -18
  30. package/pan-wizard-core/bin/lib/state.cjs +86 -22
  31. package/pan-wizard-core/bin/lib/suggest.cjs +21 -9
  32. package/pan-wizard-core/bin/lib/template.cjs +6 -3
  33. package/pan-wizard-core/bin/lib/verify-deploy.cjs +20 -3
  34. package/pan-wizard-core/bin/pan-tools.cjs +10 -1
  35. package/pan-wizard-core/mcp/tool-registry.cjs +14 -4
  36. package/pan-wizard-core/references/model-profiles.md +4 -4
  37. package/pan-wizard-core/references/planning-config.md +19 -23
  38. package/pan-wizard-core/workflows/exec-phase.md +8 -0
  39. package/pan-wizard-core/workflows/help.md +2 -2
  40. package/pan-wizard-core/workflows/phase-tests.md +1 -1
  41. package/pan-wizard-core/workflows/progress.md +1 -1
  42. package/pan-wizard-core/workflows/settings.md +2 -4
  43. package/pan-wizard-core/workflows/todo-add.md +1 -1
  44. package/pan-wizard-core/workflows/todo-check.md +7 -6
  45. package/scripts/build-agent-plugin.js +4 -0
  46. package/scripts/build-hooks.js +2 -1
  47. package/scripts/build-plugin.js +10 -0
  48. package/scripts/npm-dist-tag.js +47 -0
  49. package/scripts/plugin-path.js +6 -1
  50. 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, indicative as of 2026-08. Users
34
- * can override with `.planning/config.json` → `cost.rates`.
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 — verified against platform pricing 2026-09-10. Opus 4.6+/Opus 5 are
51
- // $5/$25 (the old $15/$75 Opus pricing ended with the 4.5 generation). Cache
52
- // rates follow Anthropic's convention: read ≈ 0.1× input, write ≈ 1.25× input —
53
- // EXCEPT Fable 5.1, whose cache reads bill at 0.025× input ($0.25). Fable 5.1
54
- // needs its own row: without it the family-prefix fallback priced its reads at
55
- // the Fable 5 rate, 4× too high on the model the `fable`/`best` aliases resolve to
56
- // (model-config, read 2026-09-10: neither Fable model is any plan's default), and
57
- // cached re-reads are the bulk of PAN's traffic (ADR-0044).
58
- 'claude-fable-5-1': { input: 10.0, output: 50.0, cache_read: 0.25, cache_write: 12.5 },
59
- 'claude-fable-5': { input: 10.0, output: 50.0, cache_read: 1.0, cache_write: 12.5 },
60
- // Mythos 5.1 / Mythos 5 (limited availability) — platform.claude.com/docs/en/about-claude/pricing,
61
- // read 2026-09-10: $10/$50; the page's cache footnote names Fable 5.1 AND Mythos 5.1 as
62
- // the two models whose cache reads bill at 0.025× input; Mythos 5 follows the 0.1× rule.
63
- // Added for reality check R7: resolveRate returned null for both ids.
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
- 'claude-opus-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
67
- 'claude-opus-4-8': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
68
- 'claude-opus-4-7': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
69
- 'claude-opus-4-6': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
70
- // Opus 4.5 (dated id claude-opus-4-5-20251101) — same pricing page, read 2026-09-10:
71
- // $5/$25/$0.50/$6.25. Without this row the dated id had no family prefix to land on
72
- // and priced as null (R7).
73
- 'claude-opus-4-5': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
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
- // (pricing page, read 2026-09-10). Lesson: never write down a pre-announced
77
- // price — this row carried the future rate for a month and over-billed by half.
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) — pricing page, read 2026-09-10:
81
- // $3/$15/$0.30/$3.75 (the pre-Sonnet-5 rate; Sonnet 5 is $2/$10). R7.
82
- 'claude-sonnet-4-5': { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75 },
83
- 'claude-haiku-4-5': { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 },
84
-
85
- // OpenAI — verified against published pricing 2026-08. Prompt caching is a 90%
86
- // input discount with no separate write charge, so cache_write bills at the
87
- // plain input rate. GPT-5.6 ships in three tiers; the bare `gpt-5.6` id prices
88
- // as the Sol flagship, with tier-specific keys for Terra/Luna (longest-prefix
89
- // match wins in resolveRate). Luna reflects the 2026-07-30 price cut.
90
- 'gpt-5.6': { input: 5.0, output: 30.0, cache_read: 0.5, cache_write: 5.0 },
91
- 'gpt-5.6-terra': { input: 2.0, output: 12.0, cache_read: 0.2, cache_write: 2.0 },
92
- 'gpt-5.6-luna': { input: 0.20, output: 1.20, cache_read: 0.02, cache_write: 0.20 },
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 — published rates (per million tokens, approximate; users can override via config.json → cost.rates).
96
- // Pro tiers use the <=200K-context tier; long-context calls may be billed at ~2x. Cache rates are Google's context-cache pricing (~25% of input rate).
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
- 'gemini-3.1-pro': { input: 2.00, output: 12.0, cache_read: 0.50, cache_write: 2.00 },
99
- 'gemini-3.1-pro-preview': { input: 2.00, output: 12.0, cache_read: 0.50, cache_write: 2.00 },
100
- 'gemini-2.5-pro': { input: 1.25, output: 10.0, cache_read: 0.3125, cache_write: 1.25 },
101
- 'gemini-2.5-flash': { input: 0.30, output: 2.50, cache_read: 0.075, cache_write: 0.30 },
102
- 'gemini-2.5-flash-lite': { input: 0.10, output: 0.40, cache_read: 0.025, cache_write: 0.10 },
103
-
104
- // Tier fallbacks when model id is unknown (reasoning tracks current Opus pricing)
105
- 'reasoning': { input: 5.0, output: 25.0, cache_read: 0.5, cache_write: 6.25 },
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.243 lets an organisation pin contracted per-model rates in
132
- // MANAGED settings — `modelPricing: { "<modelId>": { inputCostPer1MTokens,
133
- // outputCostPer1MTokens } }` (settings-reference, read 2026-09-10) — and prices
134
- // its own /usage with them. Honouring the same block keeps PAN's ledger on the
135
- // numbers the organisation actually pays. The shape carries no cache fields, so
136
- // cache rates are DERIVED: the family's own multipliers when DEFAULT_RATES knows
137
- // the family (Fable 5.1 reads bill at 0.025× input, not 0.1×), otherwise the
138
- // Anthropic convention (read 0.1×, write 1.25×).
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
- for (const [id, p] of Object.entries(modelPricing)) {
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
- const input = Number(p.inputCostPer1MTokens);
148
- const output = Number(p.outputCostPer1MTokens);
149
- if (!Number.isFinite(input) || !Number.isFinite(output) || input < 0 || output < 0) continue;
150
- const fam = DEFAULT_RATES[id] || familyPrefixRate(DEFAULT_RATES, id);
151
- const readMult = fam && fam.input > 0 ? fam.cache_read / fam.input : 0.1;
152
- const writeMult = fam && fam.input > 0 ? fam.cache_write / fam.input : 1.25;
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 && typeof mp === 'object' && !Array.isArray(mp)) merged = { ...(merged || {}), ...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
- // 2. managed `modelPricing` — the organisation's contracted rates
198
- // 3. DEFAULT_RATES — resolveRate's own fallback when neither names the model
199
- // Returns undefined (not {}) when nothing overrides, so callers keep the exact
200
- // pre-2026-09 behaviour of passing no config rates.
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
- return Object.keys(rates).length > 0 ? rates : undefined;
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
- const usd = (input * rate.input + output * rate.output
256
- + cacheRead * rate.cache_read + cacheWrite * rate.cache_write) / 1_000_000;
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
- // Bump this whenever the table is re-verified; `models check` flags the table
592
- // once it is older than RATES_STALE_AFTER_DAYS (provider prices move faster
593
- // than PAN releases do).
594
- const RATES_VERIFIED_AT = '2026-09-10';
595
- const RATES_STALE_AFTER_DAYS = 180;
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 result = { ...checkRatesStaleness(), managed_model_pricing: Object.keys(loadManagedModelPricing() || {}) };
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
- const match = content.match(/^---\n([\s\S]+?)\n---/);
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
- const header = fresh
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 (every runtime,
14
- * including Copilot/.github, reads AGENTS.md natively). User content
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 (all runtimes read it natively).
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
- const desired = syncStateFrontmatter(existing, cwd);
153
- targets.push({ file: '.planning/state.md', ...rebuildFile(p, existing, desired, apply) });
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
  );