toolroll 0.9.79 → 0.9.82

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 (139) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/cli.js +1 -0
  3. package/dist/commit-extract.d.ts +18 -0
  4. package/dist/commit-extract.js +39 -1
  5. package/dist/converse.d.ts +12 -0
  6. package/dist/converse.js +18 -2
  7. package/dist/deploy-gate.js +8 -0
  8. package/dist/eval-gate.d.ts +51 -0
  9. package/dist/eval-gate.js +199 -0
  10. package/dist/eval-hooks.d.ts +39 -0
  11. package/dist/eval-hooks.js +477 -0
  12. package/dist/eval-oracle-main.d.ts +1 -0
  13. package/dist/eval-oracle-main.js +31 -0
  14. package/dist/eval-oracle.d.ts +60 -0
  15. package/dist/eval-oracle.js +217 -0
  16. package/dist/eval-world.d.ts +17 -0
  17. package/dist/eval-world.js +97 -0
  18. package/dist/evals.d.ts +299 -0
  19. package/dist/evals.js +964 -0
  20. package/dist/exec.js +15 -1
  21. package/dist/flow-draft.js +4 -0
  22. package/dist/flow-sort.js +4 -0
  23. package/dist/github-jobs.d.ts +25 -0
  24. package/dist/github-jobs.js +22 -0
  25. package/dist/guides.js +12 -9
  26. package/dist/invoke.d.ts +1 -1
  27. package/dist/invoke.js +14 -0
  28. package/dist/lead.js +4 -1
  29. package/dist/memory-pass.js +4 -0
  30. package/dist/nightly-check.d.ts +4 -1
  31. package/dist/nightly-check.js +5 -2
  32. package/dist/nightly-github.d.ts +316 -0
  33. package/dist/nightly-github.js +1022 -0
  34. package/dist/operate.d.ts +91 -1
  35. package/dist/operate.js +158 -1
  36. package/dist/project-knowledge.d.ts +4 -4
  37. package/dist/provider.d.ts +3 -0
  38. package/dist/provider.js +9 -2
  39. package/dist/release-adapters.d.ts +21 -21
  40. package/dist/release-adapters.js +53 -20
  41. package/dist/release-auto.d.ts +1 -1
  42. package/dist/release-auto.js +26 -0
  43. package/dist/release-card.js +3 -0
  44. package/dist/release-cli.js +59 -3
  45. package/dist/release-view.d.ts +3 -0
  46. package/dist/release-view.js +2 -0
  47. package/dist/release.d.ts +36 -17
  48. package/dist/release.js +74 -24
  49. package/dist/reported-usage.d.ts +69 -0
  50. package/dist/reported-usage.js +97 -0
  51. package/dist/server/chat.js +4 -0
  52. package/dist/server/render-tasks.js +1 -0
  53. package/dist/server/tasks.js +8 -1
  54. package/dist/store.d.ts +109 -1
  55. package/dist/store.js +394 -4
  56. package/dist/subagents.js +4 -0
  57. package/dist/subscription-chat.d.ts +2 -0
  58. package/dist/subscription-chat.js +52 -13
  59. package/dist/task-sizing.js +4 -0
  60. package/dist/teardown-git.d.ts +9 -2
  61. package/dist/teardown-git.js +56 -33
  62. package/evals/cases/catalog/case.json +25 -0
  63. package/evals/cases/catalog/fixture/README.md +3 -0
  64. package/evals/cases/catalog/fixture/package.json +8 -0
  65. package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
  66. package/evals/cases/catalog/oracle.mjs +23 -0
  67. package/evals/cases/catalog/probe.mjs +10 -0
  68. package/evals/cases/catalog/source.md +9 -0
  69. package/evals/cases/csv-quoting/case.json +23 -0
  70. package/evals/cases/csv-quoting/fixture/README.md +3 -0
  71. package/evals/cases/csv-quoting/fixture/csv.js +1 -0
  72. package/evals/cases/csv-quoting/fixture/package.json +8 -0
  73. package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
  74. package/evals/cases/csv-quoting/oracle.mjs +14 -0
  75. package/evals/cases/csv-quoting/probe.mjs +9 -0
  76. package/evals/cases/csv-quoting/source.md +9 -0
  77. package/evals/cases/dedupe/case.json +23 -0
  78. package/evals/cases/dedupe/fixture/README.md +3 -0
  79. package/evals/cases/dedupe/fixture/dedupe.js +1 -0
  80. package/evals/cases/dedupe/fixture/package.json +8 -0
  81. package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
  82. package/evals/cases/dedupe/oracle.mjs +16 -0
  83. package/evals/cases/dedupe/probe.mjs +13 -0
  84. package/evals/cases/dedupe/source.md +9 -0
  85. package/evals/cases/endpoint/case.json +25 -0
  86. package/evals/cases/endpoint/fixture/README.md +3 -0
  87. package/evals/cases/endpoint/fixture/package.json +8 -0
  88. package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
  89. package/evals/cases/endpoint/oracle.mjs +27 -0
  90. package/evals/cases/endpoint/probe.mjs +10 -0
  91. package/evals/cases/endpoint/source.md +11 -0
  92. package/evals/cases/math-divide/case.json +23 -0
  93. package/evals/cases/math-divide/fixture/README.md +3 -0
  94. package/evals/cases/math-divide/fixture/package.json +8 -0
  95. package/evals/cases/math-divide/fixture/src/math.js +1 -0
  96. package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
  97. package/evals/cases/math-divide/oracle.mjs +13 -0
  98. package/evals/cases/math-divide/probe.mjs +9 -0
  99. package/evals/cases/math-divide/source.md +9 -0
  100. package/evals/cases/math-multiply/case.json +23 -0
  101. package/evals/cases/math-multiply/fixture/README.md +3 -0
  102. package/evals/cases/math-multiply/fixture/package.json +8 -0
  103. package/evals/cases/math-multiply/fixture/src/math.js +2 -0
  104. package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
  105. package/evals/cases/math-multiply/oracle.mjs +12 -0
  106. package/evals/cases/math-multiply/probe.mjs +9 -0
  107. package/evals/cases/math-multiply/source.md +9 -0
  108. package/evals/cases/math-subtract/case.json +23 -0
  109. package/evals/cases/math-subtract/fixture/README.md +3 -0
  110. package/evals/cases/math-subtract/fixture/package.json +8 -0
  111. package/evals/cases/math-subtract/fixture/src/math.js +1 -0
  112. package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
  113. package/evals/cases/math-subtract/oracle.mjs +12 -0
  114. package/evals/cases/math-subtract/probe.mjs +9 -0
  115. package/evals/cases/math-subtract/source.md +9 -0
  116. package/evals/cases/merge-config/case.json +23 -0
  117. package/evals/cases/merge-config/fixture/README.md +3 -0
  118. package/evals/cases/merge-config/fixture/config.js +1 -0
  119. package/evals/cases/merge-config/fixture/package.json +8 -0
  120. package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
  121. package/evals/cases/merge-config/oracle.mjs +28 -0
  122. package/evals/cases/merge-config/probe.mjs +15 -0
  123. package/evals/cases/merge-config/source.md +9 -0
  124. package/evals/cases/paginate/case.json +23 -0
  125. package/evals/cases/paginate/fixture/README.md +3 -0
  126. package/evals/cases/paginate/fixture/package.json +8 -0
  127. package/evals/cases/paginate/fixture/paginate.js +1 -0
  128. package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
  129. package/evals/cases/paginate/oracle.mjs +18 -0
  130. package/evals/cases/paginate/probe.mjs +13 -0
  131. package/evals/cases/paginate/source.md +9 -0
  132. package/evals/cases/round2-asks/case.json +24 -0
  133. package/evals/cases/round2-asks/fixture/README.md +3 -0
  134. package/evals/cases/round2-asks/fixture/package.json +8 -0
  135. package/evals/cases/round2-asks/fixture/src/math.js +1 -0
  136. package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
  137. package/evals/cases/round2-asks/source.md +11 -0
  138. package/evals/manifest.json +15 -0
  139. package/package.json +2 -1
@@ -17,6 +17,7 @@ import { fullCheckProblem, projectGitDir } from "./check-report.js";
17
17
  import { sealedRun } from "./release-recheck.js";
18
18
  import { runCheckLevel } from "./check-levels.js";
19
19
  import { readVerifiedCommit, verifiedBlobs, verifiedTreeDiff, verifiedTreeEntries } from "./commit-extract.js";
20
+ import { EVAL_HOOK_FILES, evalHookProblem, storeRewriteProblem } from "./eval-hooks.js";
20
21
  import { mightName, testInfrastructure } from "./test-infra.js";
21
22
  import { chatControlHref } from "./chat-controls.js";
22
23
  import { INFRA_RERUNS_PER_CANDIDATE, REQUIRED_PR_CHECKS, REQUIRED_PR_CHECKS_WORKFLOW, prChecksVerdict } from "./release.js";
@@ -27,6 +28,7 @@ import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
27
28
  import { releaseOperations, revokeRelease } from "./store.js";
28
29
  import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
29
30
  import { verificationEvidence } from "./verification-evidence.js";
31
+ import { nightlyDebtHold } from "./nightly-github.js";
30
32
  export { RELEASE_APPROVAL_ACTION, releaseApprovalSetting, releaseAutoApproves };
31
33
  /** The setting's words, one place. */
32
34
  export const RELEASE_APPROVAL_WORDS = { ask: "Ask me", auto: "Approve automatically when checks pass" };
@@ -177,6 +179,11 @@ export function authorizeAutoCheck(store, input) {
177
179
  const SRC_TEST_INFRA = /^src\/(fixtures\/|(.*\/)?(test-[^/]*|[^/]*-test-support[^/]*|[^/]*fixtures?[^/]*)\.[cm]?[jt]sx?$)/;
178
180
  /** A test file (normalized path): `*.test.ts`, never inside a fixtures folder. */
179
181
  const TEST_FILE = /^(?!.*(^|\/)fixtures?\/)[^]*\.test\.ts$/;
182
+ /** The eval set's and GitHub nightly's policy code (docs/plans/evals-nightly.md, part C, finding 10): the cases and
183
+ * oracles, the evaluator, its gate, hooks and world, reviewer scoring, nightly ingestion, their contracts, and the
184
+ * accounting readings evals trust (reported-usage.ts, run-efficiency.ts). Where they are called from, and the store's
185
+ * accounting reads, are held by eval-hooks.ts instead (evalHookProblem). */
186
+ const EVAL_POLICY = /^(evals\/|src\/evals[^/]*\.ts$|src\/eval-[^/]*\.ts$|src\/reviewer-evals\.ts$|src\/nightly-github\.ts$|src\/contracts\/evals[^/]*\.ts$|src\/reported-usage\.ts$|src\/run-efficiency\.ts$)/;
180
187
  /** Build or type configuration anywhere: a person approves it. */
181
188
  const BUILD_CONFIG = /(^|\/)(tsconfig[^/]*\.json|[^/]*\.config\.[cm]?[jt]s|\.npmrc|\.nvmrc|\.node-version)$/;
182
189
  /**
@@ -201,6 +208,8 @@ export function autoPathProblem(path) {
201
208
  }
202
209
  // Test files under test/ carry the same trust as src's (Alex: test authors are trusted) — only the `.test.ts` files
203
210
  // themselves, never a helper, fixture, setup or config file beside them, nor one inside a fixtures folder.
211
+ if (EVAL_POLICY.test(normal))
212
+ return "it's eval or nightly policy code";
204
213
  if (testUnderTest)
205
214
  return null;
206
215
  if (BUILD_CONFIG.test(normal))
@@ -312,6 +321,19 @@ function changeProblem(repo, base, candidate) {
312
321
  scans.push(found);
313
322
  }
314
323
  const infra = new Set(scans.flatMap(one => [...one.infra]));
324
+ // The eval and nightly enforcement calls the base holds must all still be in place at the candidate (eval-hooks.ts).
325
+ // One verified read of every listed file per tree (an unreadable tree reads as every file missing: a person decides).
326
+ const blobAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, EVAL_HOOK_FILES); return (file) => blobs.ok ? blobs.value.get(file)?.bytes ?? null : null; };
327
+ const hooks = evalHookProblem(blobAt(after.value.tree), blobAt(before.value.tree));
328
+ if (hooks !== null)
329
+ return `${hooks}: a person decides`;
330
+ // No changed file may write over a pinned Store method from outside store.ts (eval-hooks.ts storeRewriteProblem).
331
+ const changed = diff.value.filter(change => change.after !== null).map(change => change.path);
332
+ // A failed read is no reader at all (a person decides), never files that read as absent.
333
+ const changedAt = (tree) => { const blobs = verifiedBlobs(gitDir, tree, changed); return blobs.ok ? (file) => blobs.value.get(file)?.bytes ?? null : null; };
334
+ const rewrites = storeRewriteProblem(changed, changedAt(after.value.tree), changedAt(before.value.tree));
335
+ if (rewrites !== null)
336
+ return `${rewrites}: a person decides`;
315
337
  for (const change of diff.value) {
316
338
  const modes = [change.before?.mode, change.after?.mode].filter((one) => one !== undefined);
317
339
  if (modes.includes("120000") || modes.includes("160000"))
@@ -461,6 +483,10 @@ export function sealAutoGreen(store, input) {
461
483
  if (changed !== null)
462
484
  return refuse(changed);
463
485
  // GitHub CI, as the plane's own gh saw it moments ago, on exactly this commit.
486
+ // GitHub's nightly lane debt (evals-nightly.md, part A2): dormant while automatic approval is refused, held here too.
487
+ const nightly = nightlyDebtHold(store, { repo: facts.repo, taskId: input.taskId, candidate: input.candidate, phase: "auto", release: true, now: input.now });
488
+ if (nightly !== null)
489
+ return refuse(nightly.why);
464
490
  const ci = ciVerdict(input.ci, input.candidate, input.now);
465
491
  if (!ci.ok)
466
492
  return refuse(`CI: ${ci.problem}`);
@@ -27,6 +27,7 @@ import { routeFromJson } from "./phase-routing.js";
27
27
  import { projectName } from "./project.js";
28
28
  import { RELEASE_CI_RUNNING_WORDS, releaseCiRunningOf } from "./release-ci-hold.js";
29
29
  import { RELEASE_PATH, releaseConfigProblem, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
30
+ import { nightlyReleaseLines } from "./nightly-github.js";
30
31
  const NONE = "none";
31
32
  /** A budget in dollars from integer micro-dollars, in integer arithmetic: cents when it is whole cents, else every digit. */
32
33
  const money = (micro) => {
@@ -258,6 +259,8 @@ export function releaseCardOf(store, scope, formDigest, words) {
258
259
  ...(terms !== null && terms.policy.carry && terms.access !== "sandboxed" ? ["This project's checks don't run in the sandbox: later test-only commits will run outside the sandbox without being shown to you first."] : []),
259
260
  // Filed while CI runs (Alex, 2026-10-29): a person may approve now, and is told plainly what still waits.
260
261
  ...(releaseCiRunningOf(store, scope.taskId) === null ? [] : [RELEASE_CI_RUNNING_WORDS]),
262
+ // GitHub's nightly (nightly-github.ts): what holds this release, or why it can't be read. Not a signed term.
263
+ ...(terms === null ? [] : nightlyReleaseLines(store, { repo: terms.repo, taskId: scope.taskId, candidate: scope.candidate ?? null, now: new Date() })),
261
264
  "",
262
265
  ...changeWords(changes, terms, 5),
263
266
  "",
@@ -21,6 +21,7 @@ import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxed
21
21
  import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
22
22
  import { allowAutoPublish, setReleaseApproval } from "./release-auto.js";
23
23
  import { createNightlyCheckFlow } from "./nightly-check.js";
24
+ import { aliasNightlyLane, optInGithubNightly, retireNightlyLane } from "./nightly-github.js";
24
25
  import { parseSchedule } from "./flow-schedule.js";
25
26
  export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itself, from a pushed branch with an open pull request
26
27
 
@@ -58,6 +59,12 @@ toolroll release nightly --repo <path> --as <you> --token <password> [--schedule
58
59
  — the full release check of main's head every night, approved once with your password; it never deploys,
59
60
  merges or completes, and a failure tells you and holds composed releases until a full check passes
60
61
 
62
+ toolroll release nightly --github --repo <path> --as <you> --token <password> — read GitHub's Nightly runs on main
63
+ for this project (its recorded GitHub remote): a lane that fails its tests holds every deploy of the project, a
64
+ release's merge and its tag until a later nightly passes it, or the release's own CI re-ran it after the failure and
65
+ passed. --lane-alias <lane-id> <new name> says a renamed job is the same lane; --lane-retire <lane-id> retires a
66
+ lane no required check names any more (both with your password)
67
+
61
68
  toolroll release ask-every-commit on|off --repo <path> [--as <you> --token <password>] — this project's releases ask
62
69
  for every later commit (on), or carry test-only commits again (off: your password, never the lead's token)
63
70
 
@@ -91,8 +98,8 @@ A runner's lease is returned with \`toolroll worker release <lease>\`.`;
91
98
  /** A runner lease id (claim.ts mints randomUUID()). */
92
99
  export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
93
100
  export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
94
- const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage"]);
95
- const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy"]);
101
+ const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage", "--lane-retire"]);
102
+ const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy", "--github"]);
96
103
  export async function runReleaseCommand(args, write, deps = {}) {
97
104
  // The flag, not a secret's value that happens to read "--json".
98
105
  // A secret is never exactly one of these names (the loop below refuses that as a missing value).
@@ -104,12 +111,21 @@ export async function runReleaseCommand(args, write, deps = {}) {
104
111
  };
105
112
  const positional = [];
106
113
  const limits = {};
107
- let stage;
114
+ let stage, laneRetire, laneAlias;
108
115
  let repo, tap, gate, to, request, as, token, scheduleFlag;
109
116
  for (let i = 0; i < args.length; i += 1) {
110
117
  const raw = args[i];
111
118
  if (BARE_FLAGS.has(raw))
112
119
  continue;
120
+ // `--lane-alias <lane-id> <new name>`: two values, neither a flag of this command.
121
+ if (raw === "--lane-alias") {
122
+ const [id, name] = [args[i + 1], args[i + 2]];
123
+ if (id === undefined || name === undefined || id.startsWith("--") || name.startsWith("--"))
124
+ return refuse("usage", "--lane-alias takes <lane-id> <new name>.", 2);
125
+ laneAlias = [id, name];
126
+ i += 2;
127
+ continue;
128
+ }
113
129
  // `--flag=value` for every value flag; a secret (--token) is also the next argument even when it starts with "--".
114
130
  const equals = raw.startsWith("--") ? raw.indexOf("=") : -1;
115
131
  const arg = equals === -1 ? raw : raw.slice(0, equals);
@@ -139,6 +155,8 @@ export async function runReleaseCommand(args, write, deps = {}) {
139
155
  gate = value;
140
156
  else if (arg === "--stage")
141
157
  stage = value;
158
+ else if (arg === "--lane-retire")
159
+ laneRetire = value;
142
160
  else {
143
161
  const match = /^([a-z]+)=([0-9]+)$/.exec(value);
144
162
  if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
@@ -169,6 +187,13 @@ export async function runReleaseCommand(args, write, deps = {}) {
169
187
  return refuse("usage", "--stage goes with --recover-deploy.", 2);
170
188
  if (bare.has("--yes"))
171
189
  return refuse("usage", "--yes goes with --release-gate <id> or --recover-deploy; a release needs no confirmation beyond the owner's approval.", 2);
190
+ if (bare.has("--github") || laneAlias !== undefined || laneRetire !== undefined) {
191
+ if (positional[0] !== "nightly" || positional.length !== 1)
192
+ return refuse("usage", "--github, --lane-alias and --lane-retire go with `toolroll release nightly`.", 2);
193
+ if ([bare.has("--github"), laneAlias !== undefined, laneRetire !== undefined].filter(Boolean).length !== 1 || scheduleFlag !== undefined || bare.has("--start"))
194
+ return refuse("usage", "Use one of --github, --lane-alias or --lane-retire, without --schedule or --start.", 2);
195
+ return runGithubNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, alias: laneAlias, retire: laneRetire }, write, json, deps);
196
+ }
172
197
  if (positional[0] === "nightly" && positional.length === 1)
173
198
  return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
174
199
  if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
@@ -463,3 +488,34 @@ async function runNightlyCommand(flags, write, json, deps) {
463
488
  store.close();
464
489
  }
465
490
  }
491
+ /** `toolroll release nightly --github | --lane-alias | --lane-retire`: GitHub's nightly as release debt
492
+ * (nightly-github.ts), each a person's step proved with their password now. */
493
+ async function runGithubNightlyCommand(flags, write, json, deps) {
494
+ const answer = (ok, fields, words, code) => { write(json ? checkedEnvelopeJson({ ok, command: "release nightly", ...fields }) : words); return code; };
495
+ if (flags.repo === undefined)
496
+ return answer(false, { reason: "usage", message: "Name the project with --repo <path>." }, "Name the project with --repo <path>.", 2);
497
+ if (flags.as === undefined || flags.token === undefined)
498
+ return answer(false, { reason: "usage", message: "This needs your password: add --as <you> --token <password>." }, "This needs your password: add --as <you> --token <password>.", 2);
499
+ if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined || process.env["TOOLROLL_REMOTE_TOKEN"] !== undefined)
500
+ return answer(false, { reason: "person-only", message: "Only a person changes how GitHub's nightly holds releases." }, "Only a person changes how GitHub's nightly holds releases.", 1);
501
+ const store = openStore(deps.database ?? databasePath(process.env, homedir()));
502
+ const now = (deps.now ?? (() => new Date()))();
503
+ try {
504
+ if (flags.alias !== undefined) {
505
+ const [laneId, name] = flags.alias;
506
+ const done = aliasNightlyLane(store, { repo: flags.repo, laneId, name, as: flags.as, token: flags.token }, now);
507
+ return done.ok ? answer(true, { lane: laneId, name }, `Jobs named "${name}" now count for lane ${laneId}.`, 0) : answer(false, { reason: "refused", message: done.message }, done.message, 1);
508
+ }
509
+ if (flags.retire !== undefined) {
510
+ const done = retireNightlyLane(store, { repo: flags.repo, laneId: flags.retire, as: flags.as, token: flags.token }, now);
511
+ return done.ok ? answer(true, { lane: flags.retire, closed: done.closed }, `Retired lane ${flags.retire}${done.closed > 0 ? `; ${done.closed} open failure${done.closed === 1 ? "" : "s"} closed` : ""}.`, 0)
512
+ : answer(false, { reason: "refused", message: done.message }, done.message, 1);
513
+ }
514
+ const made = optInGithubNightly(store, { repo: flags.repo, as: flags.as, token: flags.token }, now);
515
+ return made.ok ? answer(true, { github: made.slug, lanes: made.lanes }, `GitHub's nightly for ${made.slug} now holds this project's deploys, release merges and tags while a lane fails (${made.lanes} lanes).`, 0)
516
+ : answer(false, { reason: "refused", message: made.message }, made.message, 1);
517
+ }
518
+ finally {
519
+ store.close();
520
+ }
521
+ }
@@ -30,5 +30,8 @@ export type ReleaseView = {
30
30
  outcome: string | null;
31
31
  } | null;
32
32
  openMainFailures: number;
33
+ /** GitHub's nightly (nightly-github.ts): each open lane failure, held or exempted for this release's tip, and a
34
+ * warning when it couldn't be read or hasn't run lately. */
35
+ githubNightly: string[];
33
36
  };
34
37
  export declare function releaseViewOf(store: Store, taskId: string, now: Date, obligations?: (authority: number) => string[]): ReleaseView | null;
@@ -4,6 +4,7 @@
4
4
  * main; for a carried task, whose release approval it was carried from. Read-only, from the store.
5
5
  */
6
6
  import { liveReleaseOf } from "./scope.js";
7
+ import { nightlyReleaseLines } from "./nightly-github.js";
7
8
  import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
8
9
  export function releaseViewOf(store, taskId, now, obligations = () => []) {
9
10
  const found = releaseAuthorityOfTask(store, taskId);
@@ -23,5 +24,6 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
23
24
  obligations: obligations(a.id),
24
25
  nightly: nightly === undefined ? null : { at: String(nightly["at"]), sha: String(nightly["sha"]), outcome: nightly["outcome"] == null ? null : String(nightly["outcome"]) },
25
26
  openMainFailures: open,
27
+ githubNightly: nightlyReleaseLines(store, { repo: a.repo, taskId: a.tipTask, candidate: a.tipSha, now }),
26
28
  };
27
29
  }
package/dist/release.d.ts CHANGED
@@ -32,16 +32,18 @@
32
32
  * Everything outside this module is an adapter (release-adapters.ts), so the state machine is tested without git,
33
33
  * GitHub, npm or a running Toolroll.
34
34
  */
35
- /** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
36
- * concluded `success` on the gated commit; src/ci-workflow.test.ts holds the workflow to this list. */
37
- export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment", "git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 22 · macos-latest", "node 24 · macos-latest"];
38
- /** The workflow file every REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
35
+ /** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
36
+ * concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
37
+ * this list, and publish.yml's release proof to the same list. */
38
+ export declare const REQUIRED_PR_CHECKS: readonly ["node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment", "git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 24 · macos-latest", "node 22 · ubuntu-latest", "node 22 · macos-latest"];
39
+ /** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
39
40
  * added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
40
- export declare const REQUIRED_PR_CHECKS_WORKFLOW = ".github/workflows/ci.yml";
41
+ export declare const REQUIRED_PR_CHECKS_WORKFLOW: Readonly<Record<typeof REQUIRED_PR_CHECKS[number], string>>;
41
42
  /**
42
- * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1): the
43
- * git lanes join main's protection once they have run there, so a protection without them is reported, never refused.
44
- * A later release empties this list; from then on main's protection must name every REQUIRED_PR_CHECKS check.
43
+ * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
44
+ * Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
45
+ * required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
46
+ * (and so to publish) until it does.
45
47
  */
46
48
  export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
47
49
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
@@ -57,10 +59,11 @@ export type MainProtection = {
57
59
  missing: string[];
58
60
  notYetRequired: string[];
59
61
  };
60
- /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, pipeline-speed.md item 3):
61
- * the publish job and the macOS legs it needs, each named. Nothing else reported on the merge commit (main's own CI push
62
- * run) holds publication. */
63
- export declare const RELEASE_TAG_CHECKS: readonly ["publish", "tag macOS · node 22", "tag macOS · node 24"];
62
+ /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
63
+ * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
64
+ * the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
65
+ * holds publication. */
66
+ export declare const RELEASE_TAG_CHECKS: readonly ["release proof", "publish"];
64
67
  export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
65
68
  export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
66
69
  export type ReleaseStep = typeof RELEASE_STEPS[number];
@@ -304,6 +307,22 @@ export type ReleaseAuthorityAdapter = {
304
307
  /** Releases that approve themselves when green (docs/plans/auto-approve-green.md): the plane's own seals, each proven
305
308
  * again inside the plane. Absent: a plane that can't, and every release asks a person as before. */
306
309
  auto?: AutoReleaseAdapter;
310
+ /** GitHub's nightly lane debt (nightly-github.ts, docs/plans/evals-nightly.md part A) right before a deploy, the merge
311
+ * and the tag: one bounded read of GitHub (20 s), this candidate's exemptions read again, then the plane's stored
312
+ * debt. A warning never holds; a hold stops the release. Absent: a plane that can't read it. */
313
+ nightly?(input: {
314
+ taskId: string;
315
+ candidate: string;
316
+ pr: number;
317
+ phase: "deploy" | "merge" | "tag";
318
+ }): Promise<{
319
+ ok: true;
320
+ warning: string | null;
321
+ } | {
322
+ ok: false;
323
+ why: string;
324
+ warning: string | null;
325
+ }>;
307
326
  };
308
327
  /** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
309
328
  * hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
@@ -662,10 +681,10 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
662
681
  failed: string[];
663
682
  };
664
683
  /**
665
- * Publication's verdict on the tag commit (pipeline-speed.md, item 3): only RELEASE_TAG_CHECKS, from GitHub Actions
666
- * runs of publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's
667
- * own CI run — is not waited for: the candidate's checks already passed on its exact head before the merge, and the
668
- * merged tree was proved equal to it.
684
+ * Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
685
+ * publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
686
+ * push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
687
+ * tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
669
688
  */
670
689
  export declare function tagChecksVerdict(runs: readonly CheckRun[]): ReturnType<typeof checksVerdict>;
671
690
  /** Run the release from wherever its journal stopped. */
@@ -686,7 +705,7 @@ export declare function transientNetworkError(error: unknown): boolean;
686
705
  */
687
706
  export declare function prChecksVerdict(runs: readonly CheckRun[], head: string, required: readonly string[], options?: {
688
707
  auto?: true;
689
- workflow?: string;
708
+ workflow?: string | Readonly<Record<string, string>>;
690
709
  }): {
691
710
  state: "passed";
692
711
  failures: CheckFailure[];
package/dist/release.js CHANGED
@@ -38,37 +38,54 @@ import { dirname, join } from "node:path";
38
38
  import { personProof } from "./person-proof.js";
39
39
  import { RELEASE_CI_RUNNING_WORDS as CI_RUNNING_WORDS } from "./release-ci-hold.js";
40
40
  import { transientFailure } from "./release-net.js";
41
- /** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
42
- * concluded `success` on the gated commit; src/ci-workflow.test.ts holds the workflow to this list. */
41
+ /** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
42
+ * concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
43
+ * this list, and publish.yml's release proof to the same list. */
43
44
  export const REQUIRED_PR_CHECKS = [
44
- "node 22 · ubuntu-latest",
45
+ // The unit suite on Node 24 (ci.yml "test-shard" and "test"), on every pull request.
45
46
  "node 24 · ubuntu-latest",
47
+ // The area lanes (ci.yml), on every pull request except a documentation-only one (scripts/ci-scope.mjs), where they
48
+ // are skipped; a release's pull request runs them.
46
49
  "linux native containment · delegated cgroup v2",
47
50
  "linux fence · sandbox-runtime",
48
51
  "Windows Job Object containment",
49
52
  // The git lanes (ci.yml job "git"): the declared floor (git-floor.ts) and Debian trixie's 2.47.
50
53
  "git 2.41 · debian trixie",
51
54
  "git 2.47 · debian trixie",
52
- // The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a): tested on the candidate
53
- // before it merges, no longer only on the tag after.
54
- "node 22 · macos-latest",
55
+ // The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a), Node 24.
55
56
  "node 24 · macos-latest",
57
+ // Node 22, the engines floor, on Linux and macOS (release-candidate.yml): on a release-* branch's pull request,
58
+ // skipped on every other pull request.
59
+ "node 22 · ubuntu-latest",
60
+ "node 22 · macos-latest",
56
61
  ];
57
- /** The workflow file every REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
62
+ /** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
58
63
  * added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
59
- export const REQUIRED_PR_CHECKS_WORKFLOW = ".github/workflows/ci.yml";
64
+ export const REQUIRED_PR_CHECKS_WORKFLOW = {
65
+ "node 24 · ubuntu-latest": ".github/workflows/ci.yml",
66
+ "linux native containment · delegated cgroup v2": ".github/workflows/ci.yml",
67
+ "linux fence · sandbox-runtime": ".github/workflows/ci.yml",
68
+ "Windows Job Object containment": ".github/workflows/ci.yml",
69
+ "git 2.41 · debian trixie": ".github/workflows/ci.yml",
70
+ "git 2.47 · debian trixie": ".github/workflows/ci.yml",
71
+ "node 24 · macos-latest": ".github/workflows/ci.yml",
72
+ "node 22 · ubuntu-latest": ".github/workflows/release-candidate.yml",
73
+ "node 22 · macos-latest": ".github/workflows/release-candidate.yml",
74
+ };
60
75
  /**
61
- * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1): the
62
- * git lanes join main's protection once they have run there, so a protection without them is reported, never refused.
63
- * A later release empties this list; from then on main's protection must name every REQUIRED_PR_CHECKS check.
76
+ * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
77
+ * Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
78
+ * required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
79
+ * (and so to publish) until it does.
64
80
  */
65
- export const PROTECTION_REPORT_ONLY_CHECKS = ["git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 22 · macos-latest", "node 24 · macos-latest"];
81
+ export const PROTECTION_REPORT_ONLY_CHECKS = [];
66
82
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
67
83
  export const PROTECTION_MINIMUM_CHECKS = REQUIRED_PR_CHECKS.filter(name => !PROTECTION_REPORT_ONLY_CHECKS.includes(name));
68
- /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, pipeline-speed.md item 3):
69
- * the publish job and the macOS legs it needs, each named. Nothing else reported on the merge commit (main's own CI push
70
- * run) holds publication. */
71
- export const RELEASE_TAG_CHECKS = ["publish", "tag macOS · node 22", "tag macOS · node 24"];
84
+ /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
85
+ * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
86
+ * the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
87
+ * holds publication. */
88
+ export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
72
89
  export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
73
90
  export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
74
91
  /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
@@ -192,10 +209,10 @@ export function checksVerdict(runs, required) {
192
209
  return waiting.length > 0 ? { state: "pending", waiting } : { state: "passed" };
193
210
  }
194
211
  /**
195
- * Publication's verdict on the tag commit (pipeline-speed.md, item 3): only RELEASE_TAG_CHECKS, from GitHub Actions
196
- * runs of publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's
197
- * own CI run — is not waited for: the candidate's checks already passed on its exact head before the merge, and the
198
- * merged tree was proved equal to it.
212
+ * Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
213
+ * publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
214
+ * push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
215
+ * tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
199
216
  */
200
217
  export function tagChecksVerdict(runs) {
201
218
  const names = RELEASE_TAG_CHECKS;
@@ -553,6 +570,16 @@ export async function runRelease(options, adapters) {
553
570
  if (held !== null)
554
571
  throw new Stop("approval", `${held}. It was merged; nothing more is tagged, published or sent to Homebrew until a person lets it go on: \`toolroll release publish-ok ${j.task.id} --as <you> --token <password>\`, then rerun.`);
555
572
  };
573
+ /** GitHub's nightly lane debt before a deploy, the merge or the tag (evals-nightly.md, part A2). */
574
+ const nightlyHolds = async (phase) => {
575
+ if (rel?.nightly === undefined)
576
+ return;
577
+ const answer = await rel.nightly({ taskId: j.task.id, candidate: j.candidate.sha, pr: j.pr, phase });
578
+ if (answer.warning !== null)
579
+ say(` ${answer.warning}`);
580
+ if (!answer.ok)
581
+ throw new Stop("nightly-debt", `${answer.why} Nothing was ${phase === "deploy" ? "deployed" : phase === "merge" ? "merged" : "tagged or published"}; rerun the release once it passes.`);
582
+ };
556
583
  /** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
557
584
  const watch = async (action) => { if (rel !== undefined)
558
585
  await identity(action, true); };
@@ -983,6 +1010,7 @@ export async function runRelease(options, adapters) {
983
1010
  await watch("deploy");
984
1011
  // Every deploy reservation, whatever the journal saved: CI must still be green on the exact commit.
985
1012
  await autoCiHolds("the deploy");
1013
+ await nightlyHolds("deploy");
986
1014
  const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
987
1015
  if (!reserved.ok)
988
1016
  throw new Stop("release", `The deploy couldn't be reserved: ${reserved.why}`);
@@ -1000,6 +1028,7 @@ export async function runRelease(options, adapters) {
1000
1028
  await pinned();
1001
1029
  // Every deploy issued, whatever the journal saved (a retry after a rollback included): CI still green.
1002
1030
  await autoCiHolds("the deploy");
1031
+ await nightlyHolds("deploy");
1003
1032
  let operation, secret = "";
1004
1033
  if (rel !== undefined && j.authority && epoch !== null) {
1005
1034
  const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
@@ -1093,6 +1122,9 @@ export async function runRelease(options, adapters) {
1093
1122
  const verdict = prChecksVerdict(await adapters.github.checks(j.github, j.candidate.sha), j.candidate.sha, REQUIRED_PR_CHECKS, { ...autoCap(), workflow: REQUIRED_PR_CHECKS_WORKFLOW });
1094
1123
  if (verdict.state !== "passed")
1095
1124
  await ciFailedAfterDeploy(`Pull request checks are no longer all passing on ${j.candidate.sha.slice(0, 7)}`);
1125
+ // GitHub's nightly lane debt (a bounded read), before main's last read so it doesn't widen that window; the
1126
+ // reservation below checks the stored debt again in its own transaction.
1127
+ await nightlyHolds("merge");
1096
1128
  // Main once more, from GitHub, the last thing before the merge is reserved: moved since the comparison, it
1097
1129
  // stops here (finding 4). GitHub's own up-to-date rule refuses the rest of the window.
1098
1130
  const now = await adapters.github.mainHead(j.github);
@@ -1149,6 +1181,7 @@ export async function runRelease(options, adapters) {
1149
1181
  throw new Stop("conflict", `${j.tag} already points at ${at.slice(0, 7)}, not the merge ${merge.sha.slice(0, 7)}.`);
1150
1182
  if (at === null) {
1151
1183
  await autoCiHolds("the tag");
1184
+ await nightlyHolds("tag");
1152
1185
  intend("tag");
1153
1186
  await adapters.github.createTag(j.github, j.tag, merge.sha);
1154
1187
  }
@@ -1422,9 +1455,12 @@ export function prChecksVerdict(runs, head, required, options = {}) {
1422
1455
  }
1423
1456
  // A required name counts only from the workflow that defines it: a job of the same name in another workflow (or
1424
1457
  // one whose workflow GitHub doesn't name) fails the verdict rather than stand in for it.
1425
- if (options.workflow !== undefined && required.includes(run.name) && !workflowIs(run.workflowPath, options.workflow)) {
1426
- failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${options.workflow})`);
1427
- continue;
1458
+ if (options.workflow !== undefined && required.includes(run.name)) {
1459
+ const want = typeof options.workflow === "string" ? options.workflow : options.workflow[run.name];
1460
+ if (want === undefined || !workflowIs(run.workflowPath, want)) {
1461
+ failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${want ?? "a workflow this release knows for it"})`);
1462
+ continue;
1463
+ }
1428
1464
  }
1429
1465
  if (run.app === "github-actions" && (typeof run.suite !== "number" || !Number.isSafeInteger(run.suite))) {
1430
1466
  failed.push(`${run.name} (no workflow run)`);
@@ -1468,6 +1504,13 @@ export function prChecksVerdict(runs, head, required, options = {}) {
1468
1504
  waiting.push(`${name} (${latest.status})`);
1469
1505
  continue;
1470
1506
  }
1507
+ // Skipped is never success (test-matrix.md): GitHub's protection reads a skipped job as passing a merge, a release
1508
+ // never does. A documentation-only pull request skips the area lanes, and any pull request but a release-* branch's
1509
+ // skips Node 22, so a release needs every lane executed on its exact head.
1510
+ if (latest.conclusion === "skipped" && required.includes(name)) {
1511
+ failed.push(`${name} (skipped: a release needs it run on this exact commit; release from a branch named release-*, whose pull request runs every lane)`);
1512
+ continue;
1513
+ }
1471
1514
  if (latest.conclusion !== "success") {
1472
1515
  failed.push(`${name} (${latest.conclusion ?? "no conclusion"})`);
1473
1516
  continue;
@@ -1525,5 +1568,12 @@ export function checkFailedWords(input) {
1525
1568
  }
1526
1569
  /** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
1527
1570
  export function workflowIs(path, file) {
1528
- return typeof path === "string" && (path === file || path.startsWith(`${file}@`));
1571
+ if (typeof path !== "string")
1572
+ return false;
1573
+ if (path === file)
1574
+ return true;
1575
+ // GitHub may append @ref, but ci.yml@evil.yml is also a valid *different* workflow filename. Multiple @ signs
1576
+ // likewise cannot distinguish that filename from a ref. Refuse ambiguous suffixes instead of granting its checks.
1577
+ const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
1578
+ return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
1529
1579
  }
@@ -0,0 +1,69 @@
1
+ import type { Billing } from "./spend.js";
2
+ /** Several reported models are kept as their sorted ids joined by this (it has spaces, so no one id can equal it):
3
+ * a run whose calls ran different models never reads as any one of them. */
4
+ export declare const MODELS_JOIN = " + ";
5
+ export declare function reportedModelOf(value: unknown): string | null;
6
+ /** The one model a Claude result's `modelUsage` names; null when it names none, several, or anything malformed. */
7
+ export declare function soleReportedModel(modelUsage: unknown): string | null;
8
+ /** Two reports of the same piece of work (a resumed turn, a second init): unknown if either is, the model when they
9
+ * agree, otherwise every model named, sorted (never one chosen). */
10
+ export declare function combineReportedModels(a: string | null, b: string | null): string | null;
11
+ export declare function providerTurnsOf(value: unknown): number | null;
12
+ export declare function reportedCostUsdOf(value: unknown): number | null;
13
+ /** One provider call's own report. `tokensIn` is everything the model read (cached included) when the provider
14
+ * reported every part of it (run-efficiency.ts usageOf), else null. */
15
+ export type ProviderReport = {
16
+ modelReported: string | null;
17
+ providerTurns: number | null;
18
+ reportedCostUsd: number | null;
19
+ tokensIn: number | null;
20
+ cacheReadTokens: number | null;
21
+ cacheWriteTokens: number | null;
22
+ };
23
+ /** What the session's own `system`/`init` events named: the one model they agree on, or that they disagree. */
24
+ export type ClaudeInitModel = {
25
+ model: string | null;
26
+ conflict: boolean;
27
+ };
28
+ /** A Claude primary `result` (streamed or buffered `--output-format json`), with what the session's inits named. The
29
+ * init's model is the session's own; inits that disagree leave it unknown; with no init naming one, the result's
30
+ * `modelUsage` counts only when it names exactly one model. */
31
+ export declare function claudeResultReport(result: Record<string, unknown>, init?: ClaudeInitModel): ProviderReport;
32
+ /** Codex's `turn.completed` usage: its input already counts what came from cache; it reports no cache writes, no
33
+ * model and no cost in `exec --json`, so those stay unknown. */
34
+ export declare function codexUsageReport(usage: unknown): ProviderReport;
35
+ /** One of the three amounts, never added to another. */
36
+ export type ReportedAmount = {
37
+ kind: "api";
38
+ microusd: number;
39
+ }
40
+ /** The provider's own reported cost of work a subscription covers: usage, not billed (null: it reported none). */
41
+ | {
42
+ kind: "subscription";
43
+ reportedUsd: number | null;
44
+ } | {
45
+ kind: "unknown";
46
+ };
47
+ /** A run's amount, from its fixed billing and its priced spend (run_spend). Subscription work reads its provider's
48
+ * reported cost as usage, never as dollars; billed work without a reported or priced cost is unknown. v141 (Codex
49
+ * round 1 #4, #10): billing this computer's default guessed (`basis` not evidence) is unknown, and so is a run whose
50
+ * cost the provider never reported (its `cost_usd` then holds a conservative charge, never a measurement). */
51
+ export declare function runAmountOf(run: {
52
+ billing: Billing | null;
53
+ spendMicrousd: number | null;
54
+ costUsd: number | null;
55
+ basis?: "evidence" | "default" | null;
56
+ costUnknown?: boolean;
57
+ }): ReportedAmount;
58
+ /** A chat request's amount: a subscription chat's reported cost is usage; an API chat's settled charge is dollars,
59
+ * unless its spend is unknown (then the amount is, whatever was settled against the reservation). v141 (Codex round 1
60
+ * #4): a subscription harness's request is subscription usage only when its own events said it was billed to the plan
61
+ * (`billingReported`); one they said used a key is API dollars at its reported cost; one they said nothing of is
62
+ * unknown. Absent `billingReported` keeps the older reading for callers that predate it. */
63
+ export declare function chatTurnAmountOf(turn: {
64
+ provider: string;
65
+ settledMicrousd: number | null;
66
+ unknownSpend: boolean;
67
+ reportedCostUsd: number | null;
68
+ billingReported?: "subscription" | "api-key" | null;
69
+ }): ReportedAmount;