@coreplane/switchboard 1.242.0 → 1.243.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 (59) hide show
  1. package/dist/assets/config/config.example.yaml +20 -1
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +86 -29
  3. package/dist/assets/deploy/cloudflare-resident/worker.ts +105 -13
  4. package/dist/assets/package-lock.json +3 -3
  5. package/dist/assets/package.json +1 -1
  6. package/dist/assets/project.json +3 -3
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/agents/registry.ts +30 -34
  9. package/dist/assets/src/config/profile.ts +68 -3
  10. package/dist/assets/src/core/authz/policy.ts +4 -0
  11. package/dist/assets/src/core/budgets.ts +313 -0
  12. package/dist/assets/src/core/coordinator/driver.ts +0 -10
  13. package/dist/assets/src/core/costs.ts +9 -76
  14. package/dist/assets/src/core/modelPricing.ts +212 -0
  15. package/dist/assets/src/core/prDescriptionTypes.ts +29 -24
  16. package/dist/assets/src/core/reviewVerdict.ts +7 -0
  17. package/dist/assets/src/core/runEvents.ts +28 -4
  18. package/dist/assets/src/core/runFriction.ts +2 -1
  19. package/dist/assets/src/core/runRecord.ts +47 -0
  20. package/dist/assets/src/core/runUsage.ts +158 -47
  21. package/dist/assets/src/core/schedules.ts +3 -0
  22. package/dist/assets/src/core/ship/coordinator.ts +101 -84
  23. package/dist/assets/src/core/ship/handoff.ts +9 -0
  24. package/dist/assets/src/execution/bashTimeout.ts +8 -5
  25. package/dist/assets/src/execution/residentRefresh.ts +28 -3
  26. package/dist/assets/src/execution/residentSteps.ts +1 -1
  27. package/dist/assets/web/dist/.vite/manifest.json +67 -64
  28. package/dist/assets/web/dist/assets/CostsPage-CwXOmkeQ.js +2 -0
  29. package/dist/assets/web/dist/assets/HomePage-PRxjQiGG.js +2 -0
  30. package/dist/assets/web/dist/assets/PendingTurnRow-BT9RhFZ7.js +1 -0
  31. package/dist/assets/web/dist/assets/{ResidentDetailPage-DTMBgnIW.js → ResidentDetailPage-DACalNVF.js} +1 -1
  32. package/dist/assets/web/dist/assets/ResidentsIndexPage-SHPdu6uZ.js +1 -0
  33. package/dist/assets/web/dist/assets/RunFoldRow-0SdOmOr5.js +1 -0
  34. package/dist/assets/web/dist/assets/RunRoutePage-BaFS2p8I.js +9 -0
  35. package/dist/assets/web/dist/assets/RunsIndexPage-DYI-iALj.js +1 -0
  36. package/dist/assets/web/dist/assets/ScheduledPage-DJ8HiCPt.js +1 -0
  37. package/dist/assets/web/dist/assets/{SettingsPage-BIGio8Y0.js → SettingsPage-DLiN5IgY.js} +1 -1
  38. package/dist/assets/web/dist/assets/{StatusDot-ELoXHlFt.js → StatusDot-Dw0T1M-P.js} +1 -1
  39. package/dist/assets/web/dist/assets/{Tooltip-BoeFwYP2.js → Tooltip-BbLuIAiS.js} +1 -1
  40. package/dist/assets/web/dist/assets/UnitRoutePage-DicUG96U.js +1 -0
  41. package/dist/assets/web/dist/assets/{dist-BU5UivXC.js → dist-twkFmSUY.js} +1 -1
  42. package/dist/assets/web/dist/assets/format-BldUwl_R.js +1 -0
  43. package/dist/assets/web/dist/assets/indexRow-B_s5tKyq.js +1 -0
  44. package/dist/assets/web/dist/assets/{main-B2fX10aW.css → main-B4kEF3Sg.css} +1 -1
  45. package/dist/assets/web/dist/assets/{main-D4EA1g6n.js → main-d-w-tIKt.js} +2 -2
  46. package/dist/assets/web/dist/assets/{sseReplay-g7ml86LM.js → sseReplay-C9m_EB8J.js} +4 -4
  47. package/dist/cli.js +2758 -1445
  48. package/package.json +1 -1
  49. package/dist/assets/web/dist/assets/CostsPage-CtmKhhOF.js +0 -2
  50. package/dist/assets/web/dist/assets/HomePage-QtH1EwYF.js +0 -2
  51. package/dist/assets/web/dist/assets/PendingTurnRow-BZA_vQt3.js +0 -1
  52. package/dist/assets/web/dist/assets/ResidentsIndexPage-CaDvXhzJ.js +0 -1
  53. package/dist/assets/web/dist/assets/RunFoldRow-mgyLW0oV.js +0 -1
  54. package/dist/assets/web/dist/assets/RunRoutePage-DypJYMQa.js +0 -6
  55. package/dist/assets/web/dist/assets/RunsIndexPage-DYraPoWD.js +0 -1
  56. package/dist/assets/web/dist/assets/ScheduledPage-DXD2gLJk.js +0 -1
  57. package/dist/assets/web/dist/assets/UnitRoutePage-jhCrWW3i.js +0 -1
  58. package/dist/assets/web/dist/assets/indexRow-BD1VT8o8.js +0 -1
  59. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
@@ -1,3 +1,9 @@
1
+ import {
2
+ anthropicTokensCostUsd,
3
+ parseModelPrices,
4
+ type AnthropicTokens,
5
+ type ModelPriceTable,
6
+ } from "./modelPricing.js";
1
7
  import { systemClock } from "./trace/clock.js";
2
8
  // Spend report: what a group of deployed pieces ("switchboard" = the bot
3
9
  // Worker + its containers, the resident/sandbox/memory Workers) costs per day,
@@ -68,6 +74,8 @@ export interface CostsConfig {
68
74
  /** Env var holding an Anthropic Admin API key (sk-ant-admin…). Optional feature. */
69
75
  anthropicAdminKeyEnv: string;
70
76
  groups: Record<string, CostGroupConfig>;
77
+ /** `costs.prices`: the operator's per-million rates by `<provider>/<model>`, over the Anthropic list (item 4b). */
78
+ prices: ModelPriceTable;
71
79
  /** The snapshot the page and the twins serve (src/core/costsSnapshot.ts). */
72
80
  snapshot: {
73
81
  /** Hours between two reads of the billing sources. */
@@ -155,6 +163,7 @@ export function parseCostsConfig(raw: unknown): CostsConfig | undefined {
155
163
  anthropicAdminKeyEnv:
156
164
  typeof r.anthropicAdminKeyEnv === "string" ? r.anthropicAdminKeyEnv : DEFAULT_ANTHROPIC_ADMIN_ENV,
157
165
  groups,
166
+ prices: parseModelPrices(r.prices),
158
167
  snapshot: snapshotConfig(r.snapshot),
159
168
  };
160
169
  }
@@ -691,82 +700,6 @@ export function buildCostReport(
691
700
  };
692
701
  }
693
702
 
694
- // ---- Anthropic list prices ----------------------------------------------------------------
695
-
696
- /** USD per million tokens of one kind, one model family. */
697
- export interface AnthropicModelPrice {
698
- input: number;
699
- output: number;
700
- cacheWrite5m: number;
701
- cacheWrite1h: number;
702
- cacheRead: number;
703
- }
704
-
705
- /** Anthropic list prices per model family (platform.claude.com/docs/en/about-claude/pricing;
706
- * re-check when it moves). Keyed by the family id the usage report spells — a dated
707
- * release (`claude-haiku-4-5-20251001`) resolves to its family through
708
- * `anthropicPriceOf`. Only what the estimate needs: the open day priced at
709
- * the rate it will bill at; the cost report remains the invoice. */
710
- export const ANTHROPIC_PRICES: Record<string, AnthropicModelPrice> = {
711
- "claude-fable-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
712
- "claude-mythos-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
713
- "claude-fable-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
714
- "claude-mythos-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
715
- "claude-opus-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
716
- "claude-opus-4-8": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
717
- "claude-opus-4-7": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
718
- "claude-opus-4-6": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
719
- "claude-opus-4-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
720
- "claude-opus-4-1": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
721
- "claude-opus-4": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
722
- "claude-sonnet-5": { input: 2, output: 10, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.2 },
723
- "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
724
- "claude-sonnet-4-5": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
725
- "claude-sonnet-4": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
726
- "claude-haiku-4-5": { input: 1, output: 5, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1 },
727
- "claude-haiku-3-5": { input: 0.8, output: 4, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08 },
728
- };
729
-
730
- const DATED_RELEASE_SUFFIX = /^-\d{8}$/;
731
-
732
- /** The family prices of a model id: the id itself, or the id less a dated
733
- * release suffix (`-YYYYMMDD`). Nothing else counts as "the same family" —
734
- * `claude-fable-5-1` is not `claude-fable-5` with a suffix, and its cache
735
- * reads bill differently. Unknown → undefined, never a guess. */
736
- export function anthropicPriceOf(modelId: string): AnthropicModelPrice | undefined {
737
- const exact = ANTHROPIC_PRICES[modelId];
738
- if (exact) return exact;
739
- for (const family of Object.keys(ANTHROPIC_PRICES)) {
740
- if (modelId.startsWith(family) && DATED_RELEASE_SUFFIX.test(modelId.slice(family.length)))
741
- return ANTHROPIC_PRICES[family];
742
- }
743
- return undefined;
744
- }
745
-
746
- /** Token counts of one usage-report row, in the report's own kinds. */
747
- export interface AnthropicTokens {
748
- uncachedInput: number;
749
- output: number;
750
- cacheRead: number;
751
- cacheWrite5m: number;
752
- cacheWrite1h: number;
753
- }
754
-
755
- /** What those tokens cost at the family's list prices; undefined for a model
756
- * the table does not know (the caller reports the tokens, never $0). */
757
- export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): number | undefined {
758
- const p = anthropicPriceOf(modelId);
759
- if (!p) return undefined;
760
- return (
761
- (t.uncachedInput * p.input +
762
- t.output * p.output +
763
- t.cacheRead * p.cacheRead +
764
- t.cacheWrite5m * p.cacheWrite5m +
765
- t.cacheWrite1h * p.cacheWrite1h) /
766
- 1_000_000
767
- );
768
- }
769
-
770
703
  // ---- range ----------------------------------------------------------------------------
771
704
 
772
705
  const DEFAULT_DAYS = 30;
@@ -0,0 +1,212 @@
1
+ import type { ModelUsage, RunUsage } from "./runUsage.js";
2
+
3
+ // The price of a model's tokens (docs/reference/specs/costs.md): one table
4
+ // serves every dollar of token arithmetic — the open day's estimate from
5
+ // Anthropic's hourly usage report and the by-user report's run tokens. The
6
+ // list prices are Anthropic's, keyed by family and copied from the pricing
7
+ // page; re-check them when the page moves.
8
+
9
+ /** USD per million tokens of one kind, one model family. */
10
+ export interface AnthropicModelPrice {
11
+ input: number;
12
+ output: number;
13
+ cacheWrite5m: number;
14
+ cacheWrite1h: number;
15
+ cacheRead: number;
16
+ }
17
+
18
+ /** Anthropic list prices per model family (platform.claude.com/docs/en/about-claude/pricing;
19
+ * re-check when it moves). Keyed by the family id the usage report spells — a dated
20
+ * release (`claude-haiku-4-5-20251001`) resolves to its family through
21
+ * `anthropicPriceOf`. Only what the estimate needs: the open day priced at
22
+ * the rate it will bill at; the cost report remains the invoice. */
23
+ export const ANTHROPIC_PRICES: Record<string, AnthropicModelPrice> = {
24
+ "claude-fable-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
25
+ "claude-mythos-5-1": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 0.25 },
26
+ "claude-fable-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
27
+ "claude-mythos-5": { input: 10, output: 50, cacheWrite5m: 12.5, cacheWrite1h: 20, cacheRead: 1 },
28
+ "claude-opus-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
29
+ "claude-opus-4-8": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
30
+ "claude-opus-4-7": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
31
+ "claude-opus-4-6": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
32
+ "claude-opus-4-5": { input: 5, output: 25, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5 },
33
+ "claude-opus-4-1": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
34
+ "claude-opus-4": { input: 15, output: 75, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
35
+ "claude-sonnet-5": { input: 2, output: 10, cacheWrite5m: 2.5, cacheWrite1h: 4, cacheRead: 0.2 },
36
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
37
+ "claude-sonnet-4-5": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
38
+ "claude-sonnet-4": { input: 3, output: 15, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3 },
39
+ "claude-haiku-4-5": { input: 1, output: 5, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1 },
40
+ "claude-haiku-3-5": { input: 0.8, output: 4, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08 },
41
+ };
42
+
43
+ const DATED_RELEASE_SUFFIX = /^-\d{8}$/;
44
+
45
+ /** The family prices of a model id: the id itself, or the id less a dated
46
+ * release suffix (`-YYYYMMDD`). Nothing else counts as "the same family" —
47
+ * `claude-fable-5-1` is not `claude-fable-5` with a suffix, and its cache
48
+ * reads bill differently. Unknown → undefined, never a guess. */
49
+ export function anthropicPriceOf(modelId: string): AnthropicModelPrice | undefined {
50
+ const exact = ANTHROPIC_PRICES[modelId];
51
+ if (exact) return exact;
52
+ for (const family of Object.keys(ANTHROPIC_PRICES)) {
53
+ if (modelId.startsWith(family) && DATED_RELEASE_SUFFIX.test(modelId.slice(family.length)))
54
+ return ANTHROPIC_PRICES[family];
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ /** Token counts of one usage-report row, in the report's own kinds. */
60
+ export interface AnthropicTokens {
61
+ uncachedInput: number;
62
+ output: number;
63
+ cacheRead: number;
64
+ cacheWrite5m: number;
65
+ cacheWrite1h: number;
66
+ }
67
+
68
+ /** What those tokens cost at the family's list prices; undefined for a model
69
+ * the table does not know (the caller reports the tokens, never $0). */
70
+ export function anthropicTokensCostUsd(modelId: string, t: AnthropicTokens): number | undefined {
71
+ const p = anthropicPriceOf(modelId);
72
+ if (!p) return undefined;
73
+ return (
74
+ (t.uncachedInput * p.input +
75
+ t.output * p.output +
76
+ t.cacheRead * p.cacheRead +
77
+ t.cacheWrite5m * p.cacheWrite5m +
78
+ t.cacheWrite1h * p.cacheWrite1h) /
79
+ 1_000_000
80
+ );
81
+ }
82
+
83
+ // ---- the operator's table: `costs.prices` over the list (costs.md item 4b) --------------
84
+
85
+ /** USD per million tokens of each kind a run's spans count, for one `<provider>/<model>` ref. */
86
+ export interface ModelPrice {
87
+ input: number;
88
+ output: number;
89
+ cacheRead: number;
90
+ cacheWrite: number;
91
+ }
92
+
93
+ /** `costs.prices`: the exact ref as the spans name it → its rates. */
94
+ export type ModelPriceTable = Readonly<Record<string, ModelPrice>>;
95
+
96
+ /** No table configured: the list alone prices. */
97
+ export const NO_PRICES: ModelPriceTable = Object.freeze({});
98
+
99
+ const PRICE_KINDS = ["input", "output", "cacheRead", "cacheWrite"] as const;
100
+
101
+ const isRate = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v) && v >= 0;
102
+
103
+ /** `costs.prices` as config spells it. Absent → the empty table. A key is a
104
+ * `<provider>/<model>` ref; a value names all four kinds as finite dollars per
105
+ * million ≥ 0 (a kind left out would price at $0 in silence); anything else
106
+ * is refused by name. */
107
+ export function parseModelPrices(raw: unknown): ModelPriceTable {
108
+ if (raw === undefined || raw === null) return NO_PRICES;
109
+ if (typeof raw !== "object" || Array.isArray(raw))
110
+ throw new Error("costs.prices must be a mapping of <provider>/<model> → rates");
111
+ const out: Record<string, ModelPrice> = {};
112
+ for (const [ref, value] of Object.entries(raw as Record<string, unknown>)) {
113
+ const slash = ref.indexOf("/");
114
+ if (slash <= 0 || slash === ref.length - 1)
115
+ throw new Error(`costs.prices.${ref} must be keyed <provider>/<model>, the ref a run's spans name`);
116
+ if (typeof value !== "object" || value === null || Array.isArray(value))
117
+ throw new Error(
118
+ `costs.prices.${ref} must be a mapping of { input, output, cacheRead, cacheWrite } in USD per million tokens`,
119
+ );
120
+ const v = value as Record<string, unknown>;
121
+ const price: Partial<ModelPrice> = {};
122
+ for (const kind of PRICE_KINDS) {
123
+ if (!isRate(v[kind]))
124
+ throw new Error(`costs.prices.${ref}.${kind} must be a finite number of USD per million tokens, 0 or more`);
125
+ price[kind] = v[kind];
126
+ }
127
+ out[ref] = price as ModelPrice;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /** The price of a ref: the configured table first (the exact ref), else the
133
+ * Anthropic list by family after the provider prefix is dropped — cache
134
+ * writes at the 5-minute rate, the one cache-write count a span carries —
135
+ * and undefined for a model neither knows (the caller reports the tokens,
136
+ * never $0). */
137
+ export function modelPriceOf(ref: string, prices: ModelPriceTable = NO_PRICES): ModelPrice | undefined {
138
+ const configured = prices[ref];
139
+ if (configured) return configured;
140
+ const list = anthropicPriceOf(modelIdOf(ref));
141
+ if (!list) return undefined;
142
+ return { input: list.input, output: list.output, cacheRead: list.cacheRead, cacheWrite: list.cacheWrite5m };
143
+ }
144
+
145
+ /** What a model's counted tokens cost at a price, USD. */
146
+ export function modelUsageUsd(m: ModelUsage, p: ModelPrice): number {
147
+ return (
148
+ (m.inputTokens * p.input +
149
+ m.outputTokens * p.output +
150
+ m.cacheReadTokens * p.cacheRead +
151
+ m.cacheWriteTokens * p.cacheWrite) /
152
+ 1_000_000
153
+ );
154
+ }
155
+
156
+ // ---- a run's tokens, priced -----------------------------------------------------------
157
+
158
+ /** One model's tokens, priced; `usd` is null for a model the price table does not know. */
159
+ export interface PricedModelUsage extends ModelUsage {
160
+ usd: number | null;
161
+ }
162
+
163
+ /** `anthropic/claude-fable-5` → `claude-fable-5`: the spans name the provider, the price table the model. */
164
+ export const modelIdOf = (ref: string): string => (ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : ref);
165
+
166
+ /** A run's dollars as every surface prints them (costs.md item 4c): cents from a
167
+ * dollar up (`$1.24`), a tenth of a cent below that (`$0.038`), and `<$0.001`
168
+ * under that — a run that spent anything never reads as `$0.000`; `$0.00` is a
169
+ * run with no turns and nothing else. */
170
+ export function formatUsd(usd: number): string {
171
+ if (usd === 0) return "$0.00";
172
+ if (usd < 0.001) return "<$0.001";
173
+ return `$${usd.toFixed(usd >= 1 ? 2 : 3)}`;
174
+ }
175
+
176
+ /** What one run cost (costs.md item 4c): the dollars when every model it ran
177
+ * on has a price, null when one has none — a total that left a model's tokens
178
+ * out would understate the run — and $0 for a run with no turns at all.
179
+ * `byModel` says which model was unpriced. */
180
+ export interface RunCost {
181
+ usd: number | null;
182
+ byModel: Record<string, PricedModelUsage>;
183
+ }
184
+
185
+ export function runCostOf(usage: RunUsage, prices: ModelPriceTable = NO_PRICES): RunCost {
186
+ const priced = llmUsdOfUsage(usage, prices);
187
+ const unpriced = Object.values(priced.byModel).some((m) => m.usd === null);
188
+ return { usd: unpriced ? null : priced.usd, byModel: priced.byModel };
189
+ }
190
+
191
+ /** A usage priced through the table (`modelPriceOf`): dollars for the models a
192
+ * price is known for, and the tokens of the ones it is not (never $0 in silence). */
193
+ export function llmUsdOfUsage(
194
+ usage: RunUsage,
195
+ prices: ModelPriceTable = NO_PRICES,
196
+ ): {
197
+ usd: number;
198
+ unpricedTokens: number;
199
+ byModel: Record<string, PricedModelUsage>;
200
+ } {
201
+ let usd = 0;
202
+ let unpricedTokens = 0;
203
+ const byModel: Record<string, PricedModelUsage> = {};
204
+ for (const [ref, m] of Object.entries(usage.byModel)) {
205
+ const price = modelPriceOf(ref, prices);
206
+ const priced = price ? modelUsageUsd(m, price) : undefined;
207
+ if (priced === undefined) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
208
+ else usd += priced;
209
+ byModel[ref] = { ...m, usd: priced ?? null };
210
+ }
211
+ return { usd, unpricedTokens, byModel };
212
+ }
@@ -6,49 +6,54 @@
6
6
  // types and enforces (via its annotated parse return) that the zod output
7
7
  // stays assignable to them. Add a field here first, then to the schema.
8
8
 
9
- /** A hunk the reader is pointed at: a path + inclusive 1-based line range in
10
- * the PR head. The sha is NOT stored here — it is supplied at render time. */
11
- export interface TourAnchor {
9
+ /** The lines a pointer sends the reader to: a path + inclusive 1-based line
10
+ * range in the PR head. The sha is NOT stored here — it is supplied at render
11
+ * time. */
12
+ export interface PrAnchor {
12
13
  path: string;
13
14
  from: number;
14
15
  to: number;
15
16
  }
16
17
 
17
- /** One Tour step, reader-first: heading (what the change is), the explanation,
18
- * an optional "look for" pointer, then the code. */
19
- export interface TourStep {
20
- title: string;
21
- description: string;
22
- lookFor?: string;
23
- anchor: TourAnchor;
18
+ /** One row of the map's "Where to look": a linked label, one sentence, an
19
+ * optional risk (rendered as ⚠), and the lines the label links to. */
20
+ export interface Pointer {
21
+ label: string;
22
+ text: string;
23
+ risk?: string;
24
+ anchor: PrAnchor;
24
25
  }
25
26
 
26
- /** A Tour anchor with the sha its permalink was rendered at — what a reader
27
- * gets back from a rendered body (or from a submitted object at the head it
28
- * was rendered for), so a surface can tell whether the anchors are at the
29
- * head it is looking at. */
30
- export interface RenderedTourAnchor extends TourAnchor {
27
+ /** An anchor with the sha its permalink was rendered at — what a reader gets
28
+ * back from a rendered body (or from a submitted object at the head it was
29
+ * rendered for), so a surface can tell whether the pointers are at the head
30
+ * it is looking at. */
31
+ export interface RenderedPrAnchor extends PrAnchor {
31
32
  sha: string;
32
33
  }
33
34
 
34
- /** A Tour step whose anchor carries its render sha. Assignable to `TourStep`. */
35
- export interface RenderedTourStep extends TourStep {
36
- anchor: RenderedTourAnchor;
35
+ /** A pointer whose anchor carries its render sha. Assignable to `Pointer`. */
36
+ export interface RenderedPointer extends Pointer {
37
+ anchor: RenderedPrAnchor;
37
38
  }
38
39
 
40
+ /** The PR description as data (docs/decisions/0050): the map above the fold
41
+ * (tldr, why, pointers, feedbackWanted, risk, verified), every field capped
42
+ * so the map's size does not grow with the diff, and the collapsed half
43
+ * (decisions, validation, agentNotes) below it. */
39
44
  export interface PrDescription {
40
45
  /** The PR title's single source. Metadata for the PR's own title field —
41
46
  * never rendered into the body (GitHub shows the title itself). */
42
47
  title: string;
43
48
  tldr: string;
44
- whatWhy: string;
45
- tour: TourStep[];
46
- /** Every touched file the Tour steps did not cover, one line each. */
47
- remaining: { path: string; note: string }[];
49
+ why: string;
50
+ pointers: Pointer[];
51
+ feedbackWanted: string;
52
+ risk: string;
53
+ verified: string;
48
54
  decisions: { title: string; rationale: string }[];
49
- risks: string;
50
55
  validation: {
51
- summary?: string;
52
56
  criteria: { criterion: string; proof: string }[];
53
57
  };
58
+ agentNotes?: string;
54
59
  }
@@ -163,6 +163,13 @@ export function formatFinding(f: Finding): string {
163
163
  return `[${f.severity}] ${f.id} ${location} — ${f.title}`;
164
164
  }
165
165
 
166
+ /** One compact disposition line — `id: fixed|declined[ — note]` — the coding
167
+ * side's answer to a finding, as ship's re-review turn and the thread's
168
+ * artifacts block both render it. */
169
+ export function formatDisposition(d: FindingDisposition): string {
170
+ return `${d.findingId}: ${d.disposition}${d.note ? ` — ${d.note}` : ""}`;
171
+ }
172
+
166
173
  /**
167
174
  * Build the body posted to GitHub: the deterministic verdict line, the
168
175
  * compact findings list (when present), a blank line, then the model's
@@ -1,7 +1,7 @@
1
1
  // Types only, and from the zod-free module deliberately: this file is part of
2
2
  // the node-free contract the memory Worker and web app compile with their own
3
3
  // tsconfigs — importing prDescription.ts would drag zod into those graphs.
4
- import type { PrDescription, RenderedTourStep } from "./prDescriptionTypes.js";
4
+ import type { PrDescription, RenderedPointer } from "./prDescriptionTypes.js";
5
5
  import type { HarnessScope } from "./harness/scope.js";
6
6
 
7
7
  /** The `pr_description` review artifact minus the event envelope
@@ -26,8 +26,11 @@ export interface PrDescriptionArtifact {
26
26
  /** The body as rendered (submitted) or as GitHub holds it (parsed), capped. */
27
27
  body: string;
28
28
  tldr?: string;
29
- tour: RenderedTourStep[];
30
- remaining: { path: string; note: string }[];
29
+ why?: string;
30
+ /** The map's "Where to look", each anchor stamped with the sha its permalink
31
+ * was rendered at. (A record written under the previous contract carries
32
+ * this array as `tour`; the line parser accepts either name.) */
33
+ pointers: RenderedPointer[];
31
34
  decisions: { title: string; rationale: string }[];
32
35
  /** Nothing missing or malformed — always true for `submitted`. */
33
36
  complete: boolean;
@@ -388,6 +391,13 @@ export type RouteInputObject2 = { readonly [key: string]: RouteInputLeafOrList |
388
391
  export type RouteInputObject3 = { readonly [key: string]: RouteInputLeafOrList | RouteInputObject2 };
389
392
  export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
390
393
 
394
+ /** How a door decision about a state change ended (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
395
+ * `hand_back` — the router bound a state-changing command and the door answered
396
+ * the line to paste, nothing invoked; `pasted` — the typed line that followed
397
+ * a hand-back in its thread, with the same receipt, ran. A routed read
398
+ * carries no outcome: it is not a decision about a state change. */
399
+ export type RouteOutcome = "hand_back" | "pasted";
400
+
391
401
  export type RunEvent =
392
402
  /** `callId` is the provider's tool_use id — the explicit pair key between a
393
403
  * call and its result (live-view item 13); the runner stamps it on both. */
@@ -673,6 +683,14 @@ export type RunEvent =
673
683
  seq?: number;
674
684
  at?: number;
675
685
  }
686
+ /** The run's lease as the harness started it (docs/reference/specs/run-history.md
687
+ * item 2; decision 0046): when the wall clock began, when the lease ends,
688
+ * and when the loop ends — the lease's end less the write-up and the
689
+ * post-step the lease holds back — so a record says how long the run had
690
+ * and where its loop was cut. Published by the harness once, at the first
691
+ * start of the loop; a resumed run carries the original. Head material,
692
+ * like `run_meta`. Additive: unknown → ignored. */
693
+ | { type: "lease"; startedAt: number; endsAt: number; loopEndsAt: number; seq?: number; at?: number }
676
694
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
677
695
  * item 48a): the instance the run is a child of, the unit its idempotency
678
696
  * key named, and the base branch its pull request targets — published by
@@ -734,7 +752,11 @@ export type RunEvent =
734
752
  * (`command`), `command` the id the model called, `input` the bound input
735
753
  * (redacted, each value capped) and `receipt` the chat form the reply led
736
754
  * with (`routed: <chat form>`, redacted and capped at `ROUTE_RECEIPT_CAP`);
737
- * how the invoke ended is the run's own status and `answer`. */
755
+ * how the invoke ended is the run's own status and `answer`. A door
756
+ * decision about a state change (record 0044) is a command run too, with
757
+ * `outcome` saying which: a hand-back invoked nothing and its `answer` is
758
+ * the line to paste; a paste is the typed line that followed, whatever its
759
+ * command, `handBackRunId` naming the hand-back's record. */
738
760
  | {
739
761
  type: "route";
740
762
  preset: string;
@@ -745,6 +767,8 @@ export type RunEvent =
745
767
  command?: string;
746
768
  input?: { readonly [key: string]: RouteInputValue };
747
769
  receipt?: string;
770
+ outcome?: RouteOutcome;
771
+ handBackRunId?: string;
748
772
  seq?: number;
749
773
  at?: number;
750
774
  }
@@ -425,7 +425,8 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
425
425
  ev.type === "review_posted" ||
426
426
  ev.type === "ship_round" ||
427
427
  ev.type === "route" ||
428
- ev.type === "reference"
428
+ ev.type === "reference" ||
429
+ ev.type === "lease"
429
430
  ) {
430
431
  sideFactEvents++;
431
432
  return;
@@ -209,6 +209,10 @@ export interface RunRecord {
209
209
  * (docs/reference/specs/resident-repos.md item 29) — read off the record,
210
210
  * never off the reply's text. */
211
211
  pr?: RunPullRequest;
212
+ /** The run's lease as the harness started it (item 2; decision 0046): the
213
+ * `lease` event, folded at the assembly. Present only on a run whose
214
+ * harness started a loop; a record written before the event lacks it. */
215
+ lease?: RunLease;
212
216
  /** What the run cost in tokens, per model, summed from its `model.turn`
213
217
  * spans at finish (`usageOfEvents`; docs/reference/specs/costs.md, cost by user).
214
218
  * Every record written since carries it (zero turns included); one written
@@ -225,6 +229,24 @@ export interface RunPullRequest {
225
229
  head?: string;
226
230
  }
227
231
 
232
+ /** A run's lease as the record names it (item 2): when the wall clock
233
+ * began, when the lease ends, and where the loop was cut so the write-up
234
+ * and the post-step run inside it. */
235
+ export interface RunLease {
236
+ startedAt: number;
237
+ endsAt: number;
238
+ loopEndsAt: number;
239
+ }
240
+
241
+ /** The lease a run's events say its harness started — the first `lease`
242
+ * event; a resumed run's later generations publish none — or nothing. */
243
+ export function leaseOfEvents(events: readonly RunEvent[]): RunLease | undefined {
244
+ for (const e of events) {
245
+ if (e.type === "lease") return { startedAt: e.startedAt, endsAt: e.endsAt, loopEndsAt: e.loopEndsAt };
246
+ }
247
+ return undefined;
248
+ }
249
+
228
250
  /** The pull request a run's events say it opened or edited — the last
229
251
  * `pr_opened` wins, as an edit after an open names the same PR — or nothing. */
230
252
  export function prOfEvents(events: readonly RunEvent[]): RunPullRequest | undefined {
@@ -465,6 +487,10 @@ export interface RunListOptions {
465
487
  /** The runs one run spawned or that continue a thread it opened
466
488
  * (`RunRecord.parentRunId`, item 46) — a conductor's children as one listing. */
467
489
  parentRunId?: string;
490
+ /** The runs that name one pull request (`namesPullRequest`, item 58): the
491
+ * coding runs whose post-step opened or edited it and the review runs that
492
+ * posted to it — the findings ledger's runs as one listing. */
493
+ pr?: { repo: string; number: number };
468
494
  /** What the ACTOR may see (authorization): the store predicate compiled
469
495
  * from the policy, pushed down so no surface loads rows and filters after.
470
496
  * Absent = no visibility constraint — only a caller that has already decided
@@ -577,6 +603,27 @@ export function matchesVisibility(
577
603
  }
578
604
  }
579
605
 
606
+ /** The pull request number a stored row names (item 58): the one its coding
607
+ * post-step opened or edited (`pr`), else the one its review posted to
608
+ * (`reviewPost.target`). Undefined for a run that named none — a review whose
609
+ * post was skipped included. The Worker's `pr_number` column is this value,
610
+ * written at `put` and backfilled from `summary_json` with the same rule. */
611
+ export function pullRequestNumberOf(row: Pick<RunListItem, "pr" | "reviewPost">): number | undefined {
612
+ if (row.pr !== undefined) return row.pr.number;
613
+ if (row.reviewPost?.posted) return row.reviewPost.target.number;
614
+ return undefined;
615
+ }
616
+
617
+ /** The one truth table for `RunListOptions.pr`: the row's repository is the
618
+ * pull request's and the number it names is the pull request's. The Worker
619
+ * applies the same test in SQL (`repo = ? AND pr_number = ?`). */
620
+ export function namesPullRequest(
621
+ row: Pick<RunListItem, "repo" | "pr" | "reviewPost">,
622
+ pr: { repo: string; number: number },
623
+ ): boolean {
624
+ return row.repo === pr.repo && pullRequestNumberOf(row) === pr.number;
625
+ }
626
+
580
627
  /** The list order (`finishedAt` desc, `id` desc) as a cursor predicate: true
581
628
  * when `row` comes strictly after the cursor. Shared by `selectListItems`;
582
629
  * the Worker applies the same predicate in SQL. */