@bongos/core 1.19.175 → 1.19.177

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/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.175",
6
- "core_contract": "1.19.175",
7
- "source_commit": "93e75e5bd9554037be1ec34ec0a428d9eec8db0c",
5
+ "core_version": "1.19.177",
6
+ "core_contract": "1.19.177",
7
+ "source_commit": "ec902233a41d6187d97c1b0761ca59b57ae3b180",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-16T20:19:55.300Z",
9
+ "built_at": "2026-08-16T20:47:16.225Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 305,
12
+ "docs_redacted": 306,
13
13
  "agent_docs_stubbed": 21,
14
14
  "functional_verbatim": 1273,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1599,
20
- "tree_sha256": "e99154c282f827663b8bf2628c82b2ea12e0285235bc7d46844f88d25a8f2fd7",
19
+ "file_count": 1600,
20
+ "tree_sha256": "07b51c6ba546cfe6dc3f252283ffdbf5fa06230b8a86baeb82b82311d5e11dee",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1339,6 +1339,11 @@
1339
1339
  "mode": "0000644",
1340
1340
  "sha256": "0bfe5ce46139bf9ee236868317cc80ee10eb8747d5606d0dd0fe9f2580ea5177"
1341
1341
  },
1342
+ {
1343
+ "path": "docs/adr/0172-per-craft-compensation-ideator-credit-lane.md",
1344
+ "mode": "0000644",
1345
+ "sha256": "d06219f51351c1dd73f7ba556c855e678a2e55e79dacd06419c212a6a24fcc64"
1346
+ },
1342
1347
  {
1343
1348
  "path": "docs/adr/README.md",
1344
1349
  "mode": "0000644",
@@ -2827,7 +2832,7 @@
2827
2832
  {
2828
2833
  "path": "modules/economy/credits.js",
2829
2834
  "mode": "0000644",
2830
- "sha256": "fd80563444ebb62c5d8a5e7d766a557f85bb90bb2d67f368f41e589f6b0d0ec3"
2835
+ "sha256": "38526b1d7a095fc48d062ddd645df074f381d2ef0e0aac6593ff306601659904"
2831
2836
  },
2832
2837
  {
2833
2838
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -4222,12 +4227,12 @@
4222
4227
  {
4223
4228
  "path": "package-lock.json",
4224
4229
  "mode": "0000644",
4225
- "sha256": "76f039bc2d79508787cf452ae0d84cafde39f3b43e492af622baf67f64b881a6"
4230
+ "sha256": "bd31fbe4175062649acbd10eba6caca643e13941a4d8a366bd5ba2625e7ac92d"
4226
4231
  },
4227
4232
  {
4228
4233
  "path": "package.json",
4229
4234
  "mode": "0000644",
4230
- "sha256": "9628ea87ea7a532bfb4776e9587bb916e4b9249b91cf72da1d28d0b7d3589ad2"
4235
+ "sha256": "f5fe7936f9d6739c7268697daab41d5520a0da60bc2b63099d8bd7145e977091"
4231
4236
  },
4232
4237
  {
4233
4238
  "path": "public-docs/index.html",
@@ -5637,7 +5642,7 @@
5637
5642
  {
5638
5643
  "path": "src/bongos/llm-pricing.js",
5639
5644
  "mode": "0000644",
5640
- "sha256": "c421166d7d5099f96b2b46cf9fb46b2c0b3594fb59d318c1a512751bd00a9969"
5645
+ "sha256": "f7688e75a873f9c287f6559311877081830798cb1b5f1e548ceff3fcbed9e31d"
5641
5646
  },
5642
5647
  {
5643
5648
  "path": "src/bongos/metrics.js",
@@ -5832,7 +5837,7 @@
5832
5837
  {
5833
5838
  "path": "src/module-api.js",
5834
5839
  "mode": "0000644",
5835
- "sha256": "1a5f88144d3ee5bcd4729388f02e9d5bc62541740df4ef256914c9aa8434d1e6"
5840
+ "sha256": "5904881edeeaeedb873a2968c36a01b3c2faf6e79112bd56653f28692aa5267b"
5836
5841
  },
5837
5842
  {
5838
5843
  "path": "src/module-loader/catalog.js",
@@ -6362,7 +6367,7 @@
6362
6367
  {
6363
6368
  "path": "tests/cost_reconcile.mjs",
6364
6369
  "mode": "0000644",
6365
- "sha256": "a94de0a9ebd15b8aeb80fa2fc976d847334e733bd0c2a65a869bac5c4b6b4dd8"
6370
+ "sha256": "8356250d8fc753afb2960d867f81586531053afd15f4e66da1a1b7b62b6b5db7"
6366
6371
  },
6367
6372
  {
6368
6373
  "path": "tests/cost_summary_shape.mjs",
@@ -6372,7 +6377,7 @@
6372
6377
  {
6373
6378
  "path": "tests/credit_grant.mjs",
6374
6379
  "mode": "0000644",
6375
- "sha256": "b483d3a13198f2acd5d721ff5164a6e315ea7294d4f1cfd7c4c2d9279f914f5e"
6380
+ "sha256": "2b871880fc9e6d636c5fb93d2641abad964f4d3e0ed1e78939b12dc849a7631d"
6376
6381
  },
6377
6382
  {
6378
6383
  "path": "tests/criterion_authoring_gate.mjs",
@@ -7097,7 +7102,7 @@
7097
7102
  {
7098
7103
  "path": "tests/llm-pricing.mjs",
7099
7104
  "mode": "0000644",
7100
- "sha256": "dec4e9ab447bfac75c6298c6f47d0f0f85e643c7519e192ca704d9a93a834499"
7105
+ "sha256": "de3fabaeafb3eb3ad500becf565f3b6d7118a763f1e80d085f94b6ba990d5c58"
7101
7106
  },
7102
7107
  {
7103
7108
  "path": "tests/local_preview.mjs",
@@ -0,0 +1,146 @@
1
+ # 0172 — Per-craft compensation: the ideator credit lane, separate from cost-plus
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-08-16
5
+ - **Deciders:** Lars (owner / Archon), Claude (Opus 5)
6
+ - **Track:** `internal` (Cloud Bongos — economy)
7
+ - **Task:** BV1.R171 ([#1002995](https://cloudbongos.com/builders#/task/1002995)) — the foundation of the **Ideator compensation** goal ([#1000067](https://cloudbongos.com/builders#/goal/1000067)).
8
+ - **Builds on:** [ADR 0054](<redacted>.md) (cost-plus session reward), [ADR 0146](<redacted><redacted>.md) (`rewardPolicy` + the `cost-plus-only` mode), [ADR 0023](<redacted>.md) (kind multipliers + the 25% idea-promotion slice), [ADR 0120](<redacted>.md) (pay-on-land).
9
+ - **Relates to:** goal 1000062 (three grades of idea — R141/R142/R143 shipped), idea 1000738 (the Board Room).
10
+ - **Gates:** IC2 ([#1002996](https://cloudbongos.com/builders#/task/1002996)) · IC3 ([#1002997](https://cloudbongos.com/builders#/task/1002997)) · IC4 ([#1002998](https://cloudbongos.com/builders#/task/1002998)) · IC5 ([#1002999](https://cloudbongos.com/builders#/task/1002999)) · IC6 ([#1003000](https://cloudbongos.com/builders#/task/1003000)).
11
+
12
+ ---
13
+
14
+ ## Context
15
+
16
+ Cloud Bongos pays **one kind of worker one way**. The equity stream is the
17
+ cost-plus session reward ([ADR 0054](<redacted>.md)):
18
+ `round(true_cost_usd × (1 + margin))`, booked when a shipped session lands. On
19
+ top of that, [ADR 0023](<redacted>.md) pays the
20
+ capturer of a promoted idea a **25% slice** of the resulting task's estimate
21
+ when it ships, and [ADR 0146](<redacted><redacted>.md)
22
+ lets an instance switch to `cost-plus-only`, which **withholds every per-task
23
+ estimate payout — the idea slice included** (it is a fraction of a payout that
24
+ is now zero, so it "cascades to 0", `credits.js:235`). cloudbongos.com runs
25
+ `cost-plus-only`.
26
+
27
+ Two things are now true at once, and they conflict:
28
+
29
+ 1. The **ABI model** (goal 1000062, the owner's Holy Workshop Trinity brief)
30
+ makes the **ideator** a first-class craft whose *output is a Full Idea* — a
31
+ deliverable, scored on completeness (R142, shipped). The owner wants that
32
+ craft **paid in credits**, on three streams (below).
33
+ 2. On the live instance those credits **cannot be paid**, because they ride the
34
+ very per-task estimate path `cost-plus-only` exists to withhold.
35
+
36
+ The owner resolved it with a principle, 2026-08-16:
37
+
38
+ > *"this should [be] separate from cost plus, an idea-tor should get paid
39
+ > differently than an artist and builder."*
40
+
41
+ Compensation is **per-craft**. A builder earning cost-plus for shipping and an
42
+ ideator earning credits for thinking are **not the same reward being tuned two
43
+ ways** — they are two different lanes. The `cost-plus-only` switch governs the
44
+ *builder estimate* lane; it must not reach across and silence the *ideator*
45
+ lane, because that lane is a different craft's pay.
46
+
47
+ ## Decision
48
+
49
+ ### 1. Compensation is per-craft; the ideator credit lane is its own reward, exempt from the `cost-plus-only` gate
50
+
51
+ The reward model gains a **named lane per craft**, not a single tunable payout:
52
+
53
+ - **Builder (engineer)** — the cost-plus session reward ([ADR 0054](<redacted>.md)) plus, where `mode !== 'cost-plus-only'`, the per-task estimate ([ADR 0146](<redacted><redacted>.md)). Unchanged.
54
+ - **Ideator** — the **idea credit lane** defined here (three streams, §3). Governed by its **own** policy flag, **never** by `perTaskCredit`.
55
+ - **Artist** — a lane of its own, **deliberately left unspecified here** (the artist craft — grading, copy, voice — is scoped in later ABI work). This ADR reserves the *shape* (a named lane) so the artist lane slots in the same way; it does not define the artist's pay.
56
+
57
+ The rule that makes this real and testable: **no ideator-lane credit is ever
58
+ gated on `perTaskCredit`.** `cost-plus-only` withholds the builder estimate and
59
+ nothing else.
60
+
61
+ ### 2. `rewardPolicy` grows an ideator dimension; the gate is separate from `perTaskCredit`
62
+
63
+ `rewardPolicy(rawReward)` (`modules/economy/credits.js`) today returns
64
+ `{ marginPct, usdPeg, mode, perTaskCredit }`, where
65
+ `perTaskCredit = mode !== 'cost-plus-only'`. It gains an **independent** flag:
66
+
67
+ ```
68
+ ideatorCredit: <resolved from reward config; default TRUE>
69
+ ```
70
+
71
+ `ideatorCredit` is **not derived from `mode`.** An instance in `cost-plus-only`
72
+ still has `ideatorCredit: true` unless it explicitly opts the ideator lane out.
73
+ The ideator-credit writers (IC2/IC4/IC5) gate on `config.ideatorCredit`, never
74
+ on `config.perTaskCredit` — the same shape as the existing per-task gate at
75
+ `credits.js:238`, pointed at a different flag. An instance that wants *no*
76
+ ideator pay sets it false; the default pays.
77
+
78
+ ### 3. The three ideator streams (the credit model)
79
+
80
+ All three book to `credit_log` under **ideator-lane reasons** (namespace
81
+ `idea.credit.*`), distinct from `task.shipped`, `task.confirmed`,
82
+ `idea_promotion_bonus`, and the session cost-plus. Idempotency keys are named
83
+ per stream so a re-grade / re-ship / re-vote never double-pays.
84
+
85
+ - **Stream 2 — a passing Full Idea pays 5 credits, once** (IC2, buildable now).
86
+ When a Full Idea (R143) clears the completeness bar, book **5** to its author,
87
+ once, `reason='idea.credit.full_pass'`, idempotent on `(idea_id, reason)`. The
88
+ bar is a completeness threshold set in the reward config (default proposed:
89
+ **60/100** — the R142 score at which four of five base fields are
90
+ substantively answered). Buildable today: R142 + R143 shipped.
91
+
92
+ - **Stream 1 — 5 credits per shipped task under the idea, scaled by idea grade**
93
+ (IC4). As each task descended from an idea (the IC3 lineage) first reaches
94
+ `shipped`, book `round(5 × gradeFactor)` to the idea's author, once per
95
+ `(idea_id, task_id)`, `reason='idea.credit.task_ship'`.
96
+ `gradeFactor = completeness%/100` for a Full Idea; for a **Quick Idea** (no
97
+ completeness score) a flat `QUICK_IDEA_FACTOR` (default proposed **0.6**).
98
+ **This REPLACES the ADR 0023 25% slice** (§4).
99
+
100
+ - **Stream 3 — a board-room-passed idea pays up to 100 × completeness%** (IC5,
101
+ future). When a Full Idea passes the project's governance vote (the Board
102
+ Room, idea 1000738 — *not yet built*), book
103
+ `max(floor, round(completeness% × 100))` to its author, once per idea,
104
+ `reason='idea.credit.governance_pass'`. Owner floor ~**30**. Consumes the
105
+ Board Room's pass event; invents no parallel vote.
106
+
107
+ ### 4. The ADR 0023 idea-promotion slice is retired in favour of stream 1
108
+
109
+ The 25% slice (`IDEA_BONUS_FRACTION = 0.25`, `reason='idea_promotion_bonus'`)
110
+ and stream 1 are **two rules for the same thing** — "an idea earns when its work
111
+ ships." Running both would pay an idea twice on every ship. Stream 1 (IC4) is
112
+ the go-forward rule; when IC4 lands it **removes** the 0.25 slice write, so
113
+ there is one idea-credit-on-ship path, not two. Until IC4 lands, the 0023 slice
114
+ stays (harmlessly zero on `cost-plus-only` today). The switch-over is a single
115
+ change inside IC4, guarded so no window pays both.
116
+
117
+ ### 5. Two structural facts the streams depend on (recorded, not built here)
118
+
119
+ - **Stream 1 needs one idea → many tasks.** `idea_inbox.promoted_to_task_id` is
120
+ a single FK. IC3 adds the many-tasks lineage — **preferred shape: an
121
+ ideas-owned `idea_tasks` join keyed by task id**, so it needs no `ALTER` on
122
+ the protected `tasks` table and the work stays inside the `ideas` module wall.
123
+ Set at promotion and at develop (R146).
124
+ - **Stream 3 needs the Board Room.** IC5 waits on idea 1000738.
125
+
126
+ ## Consequences
127
+
128
+ - **cloudbongos.com can pay ideators on `cost-plus-only`.** The whole reason
129
+ this goal exists. Builders keep earning cost-plus and nothing else; ideators
130
+ earn the idea lane; the two no longer collide. IC6 proves both hold at once.
131
+ - **`rewardPolicy` stays a pure function** — one added field, resolved from
132
+ reward config, unit-testable without the `branding()` cache (the ADR 0146
133
+ pattern). No new table: the thresholds/factors (pass bar, quick factor,
134
+ governance floor) are **policy constants in `credits.js`** for the same
135
+ reasons ADR 0023 kept the kind multipliers in code — they deserve a review +
136
+ ADR + commit trail, not a hot-path UPDATE. A future migration can promote them
137
+ to a table if per-instance tuning is wanted.
138
+ - **Artist pay is deliberately unresolved.** This ADR commits to the *shape*
139
+ (per-craft lanes) so the artist lane lands the same way; it does not price the
140
+ artist craft. That is later ABI work, and pretending to settle it here would
141
+ be a decision made without the design.
142
+ - **The numbers are proposed defaults, not owner-locked.** The pass bar (60),
143
+ quick factor (0.6), and governance floor (30) are this ADR's starting values;
144
+ each is a one-constant change and each downstream task (IC2/IC4/IC5) surfaces
145
+ its own for confirmation. The owner set the *structure* (5 / grade-scaled /
146
+ up-to-100-by-fullness); these fill the gaps the structure left open.
@@ -159,13 +159,23 @@ const REWARD_MODE_COST_PLUS_ONLY = 'cost-plus-only';
159
159
  // policy. Exported so a test can assert the mapping without touching the memoized
160
160
  // branding() cache (mirrors computeSessionTokenReward's pure/impure split). The
161
161
  // derived `perTaskCredit` boolean is what the per-task credit writer gates on.
162
+ //
163
+ // PER-CRAFT COMPENSATION (ADR 0172): `ideatorCredit` is a SEPARATE lane from the
164
+ // builder per-task estimate. It is NOT derived from `mode` — an instance in
165
+ // `cost-plus-only` still pays ideators (ideatorCredit stays true) because that
166
+ // switch governs the builder estimate, not a different craft's pay. The ideator
167
+ // credit writers (IC2/IC4/IC5) gate on `ideatorCredit`, never on `perTaskCredit`.
168
+ // An instance opts the ideator lane out with `reward.ideatorCredit: false`;
169
+ // absent/any-non-false → the default TRUE (a pre-ADR-0172 config keeps paying).
162
170
  function rewardPolicy(rawReward) {
163
171
  const r = rawReward || {};
164
172
  const marginPct = Number.isFinite(Number(r.marginPct)) ? Number(r.marginPct) : REWARD_MARGIN_PCT;
165
173
  const usdPeg = Number.isFinite(Number(r.usdPeg)) && Number(r.usdPeg) > 0 ? Number(r.usdPeg) : 1;
166
174
  const mode = r.mode === REWARD_MODE_COST_PLUS_ONLY ? REWARD_MODE_COST_PLUS_ONLY : REWARD_MODE_DEFAULT;
167
175
  const perTaskCredit = mode !== REWARD_MODE_COST_PLUS_ONLY;
168
- return { marginPct, usdPeg, mode, perTaskCredit };
176
+ // Default-on, opt-out-only: only an explicit `false` withholds the ideator lane.
177
+ const ideatorCredit = r.ideatorCredit !== false;
178
+ return { marginPct, usdPeg, mode, perTaskCredit, ideatorCredit };
169
179
  }
170
180
 
171
181
  // Per-instance reward policy, resolved over the code defaults. Best-effort: a
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.175",
3
+ "version": "1.19.177",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.175",
9
+ "version": "1.19.177",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.175",
3
+ "version": "1.19.177",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -34,20 +34,42 @@
34
34
  // Family-based pricing assumes the CURRENT model in each family (what this
35
35
  // instance runs). A historical id like claude-opus-4-1 ($15/$75) would be
36
36
  // mis-priced low here — acceptable: the platform only runs 4.x-current models.
37
+ //
38
+ // VERIFIED 2026-08-16 against the live Anthropic pricing page (task 1002883):
39
+ // added the `fable` family. familyFor() previously matched only opus/sonnet/
40
+ // haiku, so claude-fable-5 — the owner's top-model default and this instance's
41
+ // `assumed_builder_model` (modules/grading/grader-rubric.json) — fell through to
42
+ // null and was priced at the opus fallback with unknown_model=true. Fable is
43
+ // 2× opus, so every cost-plus reward computed from a Fable session was priced at
44
+ // HALF its true cost (not conservative: cost-plus UNDER-pays the builder). The
45
+ // grading side already carried the family token (grader.js MODEL_FAMILY_TOKENS);
46
+ // this table is the surface that never caught up.
47
+ // - fable = Claude Fable 5 / Mythos 5 ($10 in / $50 out) — identical rates,
48
+ // which is why one family token covers both.
49
+ // - opus = Claude Opus 5 / 4.8 / 4.7 ($5 in / $25 out) [re-verified]
50
+ // - sonnet = Claude Sonnet 4.6 ($3 in / $15 out) [re-verified]
51
+ // - haiku = Claude Haiku 4.5 ($1 in / $5 out) [re-verified]
52
+ // NOTE (deliberately NOT changed here — its own task, per the rule above):
53
+ // Claude Sonnet 5 is now $2/$10, not the $3/$15 this `sonnet` row carries. The
54
+ // grader's default worker model is claude-sonnet-5, so grade costs are currently
55
+ // OVER-priced ~1.5×. That is a separate price-table decision; see task 1003001.
37
56
  const PRICES = {
38
57
  // family : { input, output, cache_write_5m, cache_write_1h, cache_read } (USD / 1e6 tokens)
58
+ fable: { input: 10, output: 50, cache_write_5m: 12.5, cache_write_1h: 20, cache_read: 1 },
39
59
  opus: { input: 5, output: 25, cache_write_5m: 6.25, cache_write_1h: 10, cache_read: 0.5 },
40
60
  sonnet: { input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 },
41
61
  haiku: { input: 1, output: 5, cache_write_5m: 1.25, cache_write_1h: 2, cache_read: 0.1 },
42
62
  };
43
63
 
44
- // Resolve a model id (e.g. 'claude-opus-4-8', 'claude-sonnet-4-6',
45
- // 'claude-haiku-4-5') to a price family by substring — mirrors grader.js's
46
- // family-token convention (all live Claude ids contain one of these three).
47
- // Unknown → null (caller decides: we conservatively price at the opus rate AND
48
- // flag it, so an unpriced model is loud, never silently $0).
64
+ // Resolve a model id (e.g. 'claude-fable-5', 'claude-opus-4-8',
65
+ // 'claude-sonnet-4-6', 'claude-haiku-4-5') to a price family by substring —
66
+ // mirrors grader.js's MODEL_FAMILY_TOKENS convention, including the fable/mythos
67
+ // pair it checks first (all live Claude ids contain one of these tokens).
68
+ // Unknown → null (caller decides: we price at the opus rate AND flag it, so an
69
+ // unpriced model is loud, never silently $0 — see priceUsage on why opus).
49
70
  function familyFor(model) {
50
71
  const m = String(model || '').toLowerCase();
72
+ if (m.includes('fable') || m.includes('mythos')) return 'fable';
51
73
  if (m.includes('opus')) return 'opus';
52
74
  if (m.includes('sonnet')) return 'sonnet';
53
75
  if (m.includes('haiku')) return 'haiku';
@@ -70,7 +92,14 @@ function priceUsage(model, usage) {
70
92
  const u = usage || {};
71
93
  const fam = familyFor(model);
72
94
  const unknownModel = fam === null;
73
- const priceFamily = fam || 'opus'; // conservative default for an unrecognized model
95
+ // Unrecognized → price at the opus rate. Since `fable` landed (task 1002883)
96
+ // opus is no longer the ceiling, so this is a MID-range fallback, not a
97
+ // conservative one: an unknown Fable-class model prices at half its true rate.
98
+ // That is why unknown_model=true rides along — the loudness is the safeguard,
99
+ // not the rate. (Deliberately left at opus: every model this platform actually
100
+ // runs is now matched, so raising the fallback would only over-price genuinely
101
+ // foreign ids like gpt-4o.)
102
+ const priceFamily = fam || 'opus';
74
103
  const p = PRICES[priceFamily];
75
104
 
76
105
  const input = Number(u.input_tokens) || 0; // uncached input
@@ -179,7 +208,7 @@ function priceModelUsage(modelUsage) {
179
208
  * Like priceModelUsage, model_usage has NO 5m/1h cache-creation breakdown — the
180
209
  * `cache_creation` lump is priced at the 5m rate, exactly as session-cost.js
181
210
  * priceTranscript does in its `else` branch (no breakdown → 5m). Unknown model
182
- * ids are conservatively priced as opus AND collected, so an unpriced model is
211
+ * ids are priced as opus AND collected, so an unpriced model is
183
212
  * loud, never silently $0. Pure: no I/O, no Date.now().
184
213
  *
185
214
  * @param {Object<string, {input?:number,output?:number,cache_read?:number,cache_creation?:number}>} modelUsage
@@ -199,7 +228,7 @@ function componentsFromModelUsage(modelUsage) {
199
228
  const usage = u || {};
200
229
  const fam = familyFor(rawId);
201
230
  if (fam === null) unknown.push(canonicalizeModelId(rawId));
202
- const p = PRICES[fam || 'opus']; // conservative default for an unrecognized model
231
+ const p = PRICES[fam || 'opus']; // mid-range fallback; unknown ids ride the `unknown` list
203
232
  fresh_input_usd += ((Number(usage.input) || 0) * p.input) / 1e6;
204
233
  output_usd += ((Number(usage.output) || 0) * p.output) / 1e6;
205
234
  cache_read_usd += ((Number(usage.cache_read) || 0) * p.cache_read) / 1e6;
package/src/module-api.js CHANGED
@@ -777,8 +777,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
777
777
  // landed since 1.19.173 with no explicit bump. run 31969032984. (task 1002620)
778
778
  // 1.19.175 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
779
779
  // landed since 1.19.174 with no explicit bump. run 31970199477. (task 1002620)
780
+ // 1.19.176 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
781
+ // landed since 1.19.175 with no explicit bump. run 31971125373. (task 1002620)
782
+ // 1.19.177 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
783
+ // landed since 1.19.176 with no explicit bump. run 31971532260. (task 1002620)
780
784
  // ---------------------------------------------------------------------------
781
- const CORE_VERSION = '1.19.175'; // CI auto-patch carrier (ADR 0161); see changelog above
785
+ const CORE_VERSION = '1.19.177'; // CI auto-patch carrier (ADR 0161); see changelog above
782
786
 
783
787
  // A namespaced logger so a module's log lines are attributable + consistent.
784
788
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -100,3 +100,33 @@ test('1366 missing per-tier fields default to 0 (partial model_usage)', () => {
100
100
  assert.equal(out.cache_write_usd, 0);
101
101
  assert.ok(near(out.total_usd, 5));
102
102
  });
103
+
104
+ // ---------- fable / mythos family (task 1002883) ----------
105
+ // The reconciler is the SECOND pricing path the missing fable family broke: a
106
+ // cloud-box / overnight Fable session whose client cost hook never posted lands
107
+ // here, and before the fix it re-priced at the opus fallback — half the true
108
+ // cost, and flagged unknown on the platform's own default model.
109
+
110
+ test('1002883 fable 1M/1M/10M/1M → $10 / $50 / $10 / $12.50, total $82.50, not flagged', () => {
111
+ const out = componentsFromModelUsage({
112
+ 'claude-fable-5': { input: M, output: M, cache_read: 10 * M, cache_creation: M },
113
+ });
114
+ assert.ok(near(out.fresh_input_usd, 10), `fresh ${out.fresh_input_usd}`);
115
+ assert.ok(near(out.output_usd, 50), `output ${out.output_usd}`);
116
+ assert.ok(near(out.cache_read_usd, 10), `cache_read ${out.cache_read_usd}`); // 10M * 1/M
117
+ assert.ok(near(out.cache_write_usd, 12.5), `cache_write ${out.cache_write_usd}`);
118
+ assert.ok(near(out.total_usd, 82.5), `total ${out.total_usd}`);
119
+ const sum = out.fresh_input_usd + out.cache_read_usd + out.cache_write_usd + out.output_usd;
120
+ assert.ok(near(sum, out.total_usd), `tiers sum ${sum} must equal total ${out.total_usd}`);
121
+ assert.deepEqual(out.unknown_models, [], 'fable is the platform default — never "unknown"');
122
+ });
123
+
124
+ test('1002883 mythos prices as fable, and a fable session is 2× the same opus session', () => {
125
+ const usage = { input: M, output: M };
126
+ const fable = componentsFromModelUsage({ 'claude-fable-5': usage });
127
+ const mythos = componentsFromModelUsage({ 'claude-mythos-5': usage });
128
+ const opus = componentsFromModelUsage({ 'claude-opus-5': usage });
129
+ assert.ok(near(fable.total_usd, mythos.total_usd), 'mythos shares fable rates');
130
+ assert.deepEqual(mythos.unknown_models, []);
131
+ assert.ok(near(fable.total_usd, opus.total_usd * 2), `fable ${fable.total_usd} != 2× opus ${opus.total_usd}`);
132
+ });
@@ -516,6 +516,36 @@ await test('rewardPolicy: unknown/garbage mode → default (a typo never silentl
516
516
  assert.equal(rewardPolicy(undefined).perTaskCredit, true);
517
517
  });
518
518
 
519
+ // ── ADR 0172: the ideator credit lane is SEPARATE from the builder estimate ──
520
+
521
+ await test('rewardPolicy: ideatorCredit defaults ON, and a pre-0172 config keeps paying ideators', () => {
522
+ assert.equal(rewardPolicy({}).ideatorCredit, true, 'absent flag → the default TRUE');
523
+ assert.equal(rewardPolicy(null).ideatorCredit, true);
524
+ // a config written before this ADR (no ideatorCredit key) must not silently
525
+ // withhold the new lane
526
+ assert.equal(rewardPolicy({ mode: 'cost-plus-and-estimate', marginPct: 20 }).ideatorCredit, true);
527
+ });
528
+
529
+ await test('rewardPolicy: cost-plus-only WITHHOLDS the builder estimate but NOT the ideator lane', () => {
530
+ const p = rewardPolicy({ mode: 'cost-plus-only' });
531
+ assert.equal(p.perTaskCredit, false, 'the builder per-task estimate is off');
532
+ assert.equal(p.ideatorCredit, true, 'the ideator lane is a different craft\'s pay — unaffected');
533
+ // this is the whole point of ADR 0172: the two flags are independent
534
+ assert.notEqual(p.perTaskCredit, p.ideatorCredit);
535
+ });
536
+
537
+ await test('rewardPolicy: ideatorCredit is opt-out-only — only an explicit false withholds it', () => {
538
+ assert.equal(rewardPolicy({ ideatorCredit: false }).ideatorCredit, false, 'explicit opt-out');
539
+ // and it is orthogonal to mode: an instance can pay per-task estimate but NOT ideators
540
+ const p = rewardPolicy({ mode: 'cost-plus-and-estimate', ideatorCredit: false });
541
+ assert.equal(p.perTaskCredit, true);
542
+ assert.equal(p.ideatorCredit, false);
543
+ // any non-false value (a typo, a truthy) stays ON — never a silent zeroing
544
+ for (const v of [true, 1, 'yes', undefined, null]) {
545
+ assert.equal(rewardPolicy({ ideatorCredit: v }).ideatorCredit, true, `ideatorCredit=${JSON.stringify(v)} pays`);
546
+ }
547
+ });
548
+
519
549
  await test('rewardPolicy: cost-plus-only still honors a custom margin + peg', () => {
520
550
  const p = rewardPolicy({ mode: 'cost-plus-only', marginPct: 35, usdPeg: 2 });
521
551
  assert.equal(p.perTaskCredit, false);
@@ -2,6 +2,9 @@
2
2
  // Prices VERIFIED 2026-06-20 against the live Anthropic models/pricing page
3
3
  // (GDS-V4 task 1343): opus (4.8/4.7) $5/$25, sonnet (4.6) $3/$15, haiku (4.5)
4
4
  // $1/$5; flat across the 1M window (no long-context premium).
5
+ // Re-VERIFIED + extended 2026-08-16 (task 1002883): fable (Fable 5 / Mythos 5)
6
+ // $10/$50 — the family familyFor() used to miss, so Fable sessions priced at the
7
+ // opus fallback and cost-plus under-paid by half.
5
8
  // Run: node tests/llm-pricing.mjs
6
9
 
7
10
  import { strict as assert } from 'node:assert';
@@ -18,6 +21,9 @@ const M = 1_000_000;
18
21
  const near = (a, b) => Math.abs(a - b) < 1e-6;
19
22
 
20
23
  test('familyFor matches by substring; unknown → null', () => {
24
+ assert.equal(familyFor('claude-fable-5'), 'fable');
25
+ assert.equal(familyFor('claude-mythos-5'), 'fable'); // same rates → same family
26
+ assert.equal(familyFor('claude-mythos-preview'), 'fable');
21
27
  assert.equal(familyFor('claude-opus-4-8'), 'opus');
22
28
  assert.equal(familyFor('claude-sonnet-4-6'), 'sonnet');
23
29
  assert.equal(familyFor('claude-haiku-4-5'), 'haiku');
@@ -26,6 +32,68 @@ test('familyFor matches by substring; unknown → null', () => {
26
32
  assert.equal(familyFor(undefined), null);
27
33
  });
28
34
 
35
+ // ---------- fable / mythos family (task 1002883) ----------
36
+ // The regression this locks: claude-fable-5 is the owner's top-model default and
37
+ // the grader's assumed_builder_model, but familyFor() matched only opus/sonnet/
38
+ // haiku — so it fell to the opus fallback (unknown_model=true) at HALF its rate,
39
+ // and every cost-plus reward from a Fable session under-paid the builder.
40
+
41
+ test('fable 5: 1M input + 1M output = $10 + $50 = $60, and is NOT flagged unknown', () => {
42
+ const r = priceUsage('claude-fable-5', { input_tokens: M, output_tokens: M });
43
+ assert.ok(near(r.usd, 60), `got ${r.usd}`);
44
+ assert.equal(r.unknown_model, false, 'fable must resolve, not ride the opus fallback');
45
+ assert.equal(r.family, 'fable');
46
+ });
47
+
48
+ test('mythos 5 prices identically to fable 5 (one family, shared rates)', () => {
49
+ const f = priceUsage('claude-fable-5', { input_tokens: M, output_tokens: M });
50
+ const m = priceUsage('claude-mythos-5', { input_tokens: M, output_tokens: M });
51
+ assert.ok(near(f.usd, m.usd), `fable ${f.usd} != mythos ${m.usd}`);
52
+ assert.equal(m.family, 'fable');
53
+ });
54
+
55
+ test('fable cache tiers: read $1, 5m write $12.50, 1h write $20 per 1M', () => {
56
+ assert.ok(near(priceUsage('claude-fable-5', { cache_read_input_tokens: M }).usd, 1));
57
+ assert.ok(near(priceUsage('claude-fable-5', {
58
+ cache_creation_input_tokens: M,
59
+ cache_creation: { ephemeral_5m_input_tokens: M, ephemeral_1h_input_tokens: 0 },
60
+ }).usd, 12.5));
61
+ assert.ok(near(priceUsage('claude-fable-5', {
62
+ cache_creation_input_tokens: M,
63
+ cache_creation: { ephemeral_5m_input_tokens: 0, ephemeral_1h_input_tokens: M },
64
+ }).usd, 20));
65
+ });
66
+
67
+ test('fable rides the published cache multipliers off its own base input (1.25× / 2× / 0.1×)', () => {
68
+ const { fable } = PRICES;
69
+ assert.ok(near(fable.cache_write_5m, fable.input * 1.25));
70
+ assert.ok(near(fable.cache_write_1h, fable.input * 2));
71
+ assert.ok(near(fable.cache_read, fable.input * 0.1));
72
+ });
73
+
74
+ test('fable is exactly 2× opus — the size of the pre-fix cost-plus shortfall', () => {
75
+ const usage = { input_tokens: M, output_tokens: M };
76
+ const fable = priceUsage('claude-fable-5', usage).usd;
77
+ const opus = priceUsage('claude-opus-5', usage).usd;
78
+ assert.ok(near(fable, opus * 2), `fable ${fable} != 2× opus ${opus}`);
79
+ });
80
+
81
+ test('priceModelUsage: a fable session costs $60 / 1M+1M and reports no unknown models', () => {
82
+ const r = priceModelUsage({ 'claude-fable-5': { input: M, output: M } });
83
+ assert.ok(near(r.true_cost_usd, 60), `got ${r.true_cost_usd}`);
84
+ assert.deepEqual(r.unknown_models, [], 'fable must not be reported as unpriced');
85
+ });
86
+
87
+ test('canonicalizeModelId collapses fable variants without changing the price', () => {
88
+ assert.equal(canonicalizeModelId('claude-fable-5[1m]'), 'claude-fable-5');
89
+ const r = priceModelUsage({
90
+ 'claude-fable-5': { input: M },
91
+ 'claude-fable-5[1m]': { input: M },
92
+ });
93
+ assert.equal(r.per_model.length, 2, 'raw keys kept as separate breakdown rows');
94
+ assert.ok(near(r.true_cost_usd, 20), `got ${r.true_cost_usd}`); // 10 + 10
95
+ });
96
+
29
97
  test('opus 4.8: 1M input + 1M output = $5 + $25 = $30', () => {
30
98
  const r = priceUsage('claude-opus-4-8', { input_tokens: M, output_tokens: M });
31
99
  assert.ok(near(r.usd, 30), `got ${r.usd}`);