toolroll 0.9.81 → 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 (137) hide show
  1. package/CHANGELOG.md +21 -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/invoke.d.ts +1 -1
  26. package/dist/invoke.js +14 -0
  27. package/dist/lead.js +4 -1
  28. package/dist/memory-pass.js +4 -0
  29. package/dist/nightly-check.d.ts +4 -1
  30. package/dist/nightly-check.js +5 -2
  31. package/dist/nightly-github.d.ts +316 -0
  32. package/dist/nightly-github.js +1022 -0
  33. package/dist/operate.d.ts +91 -1
  34. package/dist/operate.js +158 -1
  35. package/dist/project-knowledge.d.ts +4 -4
  36. package/dist/provider.d.ts +3 -0
  37. package/dist/provider.js +9 -2
  38. package/dist/release-adapters.d.ts +21 -21
  39. package/dist/release-adapters.js +52 -19
  40. package/dist/release-auto.js +26 -0
  41. package/dist/release-card.js +3 -0
  42. package/dist/release-cli.js +59 -3
  43. package/dist/release-view.d.ts +3 -0
  44. package/dist/release-view.js +2 -0
  45. package/dist/release.d.ts +16 -0
  46. package/dist/release.js +16 -0
  47. package/dist/reported-usage.d.ts +69 -0
  48. package/dist/reported-usage.js +97 -0
  49. package/dist/server/chat.js +4 -0
  50. package/dist/server/render-tasks.js +1 -0
  51. package/dist/server/tasks.js +8 -1
  52. package/dist/store.d.ts +109 -1
  53. package/dist/store.js +394 -4
  54. package/dist/subagents.js +4 -0
  55. package/dist/subscription-chat.d.ts +2 -0
  56. package/dist/subscription-chat.js +52 -13
  57. package/dist/task-sizing.js +4 -0
  58. package/dist/teardown-git.d.ts +9 -2
  59. package/dist/teardown-git.js +56 -33
  60. package/evals/cases/catalog/case.json +25 -0
  61. package/evals/cases/catalog/fixture/README.md +3 -0
  62. package/evals/cases/catalog/fixture/package.json +8 -0
  63. package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
  64. package/evals/cases/catalog/oracle.mjs +23 -0
  65. package/evals/cases/catalog/probe.mjs +10 -0
  66. package/evals/cases/catalog/source.md +9 -0
  67. package/evals/cases/csv-quoting/case.json +23 -0
  68. package/evals/cases/csv-quoting/fixture/README.md +3 -0
  69. package/evals/cases/csv-quoting/fixture/csv.js +1 -0
  70. package/evals/cases/csv-quoting/fixture/package.json +8 -0
  71. package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
  72. package/evals/cases/csv-quoting/oracle.mjs +14 -0
  73. package/evals/cases/csv-quoting/probe.mjs +9 -0
  74. package/evals/cases/csv-quoting/source.md +9 -0
  75. package/evals/cases/dedupe/case.json +23 -0
  76. package/evals/cases/dedupe/fixture/README.md +3 -0
  77. package/evals/cases/dedupe/fixture/dedupe.js +1 -0
  78. package/evals/cases/dedupe/fixture/package.json +8 -0
  79. package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
  80. package/evals/cases/dedupe/oracle.mjs +16 -0
  81. package/evals/cases/dedupe/probe.mjs +13 -0
  82. package/evals/cases/dedupe/source.md +9 -0
  83. package/evals/cases/endpoint/case.json +25 -0
  84. package/evals/cases/endpoint/fixture/README.md +3 -0
  85. package/evals/cases/endpoint/fixture/package.json +8 -0
  86. package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
  87. package/evals/cases/endpoint/oracle.mjs +27 -0
  88. package/evals/cases/endpoint/probe.mjs +10 -0
  89. package/evals/cases/endpoint/source.md +11 -0
  90. package/evals/cases/math-divide/case.json +23 -0
  91. package/evals/cases/math-divide/fixture/README.md +3 -0
  92. package/evals/cases/math-divide/fixture/package.json +8 -0
  93. package/evals/cases/math-divide/fixture/src/math.js +1 -0
  94. package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
  95. package/evals/cases/math-divide/oracle.mjs +13 -0
  96. package/evals/cases/math-divide/probe.mjs +9 -0
  97. package/evals/cases/math-divide/source.md +9 -0
  98. package/evals/cases/math-multiply/case.json +23 -0
  99. package/evals/cases/math-multiply/fixture/README.md +3 -0
  100. package/evals/cases/math-multiply/fixture/package.json +8 -0
  101. package/evals/cases/math-multiply/fixture/src/math.js +2 -0
  102. package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
  103. package/evals/cases/math-multiply/oracle.mjs +12 -0
  104. package/evals/cases/math-multiply/probe.mjs +9 -0
  105. package/evals/cases/math-multiply/source.md +9 -0
  106. package/evals/cases/math-subtract/case.json +23 -0
  107. package/evals/cases/math-subtract/fixture/README.md +3 -0
  108. package/evals/cases/math-subtract/fixture/package.json +8 -0
  109. package/evals/cases/math-subtract/fixture/src/math.js +1 -0
  110. package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
  111. package/evals/cases/math-subtract/oracle.mjs +12 -0
  112. package/evals/cases/math-subtract/probe.mjs +9 -0
  113. package/evals/cases/math-subtract/source.md +9 -0
  114. package/evals/cases/merge-config/case.json +23 -0
  115. package/evals/cases/merge-config/fixture/README.md +3 -0
  116. package/evals/cases/merge-config/fixture/config.js +1 -0
  117. package/evals/cases/merge-config/fixture/package.json +8 -0
  118. package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
  119. package/evals/cases/merge-config/oracle.mjs +28 -0
  120. package/evals/cases/merge-config/probe.mjs +15 -0
  121. package/evals/cases/merge-config/source.md +9 -0
  122. package/evals/cases/paginate/case.json +23 -0
  123. package/evals/cases/paginate/fixture/README.md +3 -0
  124. package/evals/cases/paginate/fixture/package.json +8 -0
  125. package/evals/cases/paginate/fixture/paginate.js +1 -0
  126. package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
  127. package/evals/cases/paginate/oracle.mjs +18 -0
  128. package/evals/cases/paginate/probe.mjs +13 -0
  129. package/evals/cases/paginate/source.md +9 -0
  130. package/evals/cases/round2-asks/case.json +24 -0
  131. package/evals/cases/round2-asks/fixture/README.md +3 -0
  132. package/evals/cases/round2-asks/fixture/package.json +8 -0
  133. package/evals/cases/round2-asks/fixture/src/math.js +1 -0
  134. package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
  135. package/evals/cases/round2-asks/source.md +11 -0
  136. package/evals/manifest.json +15 -0
  137. 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
@@ -307,6 +307,22 @@ export type ReleaseAuthorityAdapter = {
307
307
  /** Releases that approve themselves when green (docs/plans/auto-approve-green.md): the plane's own seals, each proven
308
308
  * again inside the plane. Absent: a plane that can't, and every release asks a person as before. */
309
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
+ }>;
310
326
  };
311
327
  /** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
312
328
  * hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
package/dist/release.js CHANGED
@@ -570,6 +570,16 @@ export async function runRelease(options, adapters) {
570
570
  if (held !== null)
571
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.`);
572
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
+ };
573
583
  /** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
574
584
  const watch = async (action) => { if (rel !== undefined)
575
585
  await identity(action, true); };
@@ -1000,6 +1010,7 @@ export async function runRelease(options, adapters) {
1000
1010
  await watch("deploy");
1001
1011
  // Every deploy reservation, whatever the journal saved: CI must still be green on the exact commit.
1002
1012
  await autoCiHolds("the deploy");
1013
+ await nightlyHolds("deploy");
1003
1014
  const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
1004
1015
  if (!reserved.ok)
1005
1016
  throw new Stop("release", `The deploy couldn't be reserved: ${reserved.why}`);
@@ -1017,6 +1028,7 @@ export async function runRelease(options, adapters) {
1017
1028
  await pinned();
1018
1029
  // Every deploy issued, whatever the journal saved (a retry after a rollback included): CI still green.
1019
1030
  await autoCiHolds("the deploy");
1031
+ await nightlyHolds("deploy");
1020
1032
  let operation, secret = "";
1021
1033
  if (rel !== undefined && j.authority && epoch !== null) {
1022
1034
  const reserved = await rel.reserve({ authority: j.authority.id, kind: "deploy", taskId: j.task.id, candidate: j.candidate.sha, epoch });
@@ -1110,6 +1122,9 @@ export async function runRelease(options, adapters) {
1110
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 });
1111
1123
  if (verdict.state !== "passed")
1112
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");
1113
1128
  // Main once more, from GitHub, the last thing before the merge is reserved: moved since the comparison, it
1114
1129
  // stops here (finding 4). GitHub's own up-to-date rule refuses the rest of the window.
1115
1130
  const now = await adapters.github.mainHead(j.github);
@@ -1166,6 +1181,7 @@ export async function runRelease(options, adapters) {
1166
1181
  throw new Stop("conflict", `${j.tag} already points at ${at.slice(0, 7)}, not the merge ${merge.sha.slice(0, 7)}.`);
1167
1182
  if (at === null) {
1168
1183
  await autoCiHolds("the tag");
1184
+ await nightlyHolds("tag");
1169
1185
  intend("tag");
1170
1186
  await adapters.github.createTag(j.github, j.tag, merge.sha);
1171
1187
  }
@@ -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;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * What a provider itself reported about one call (docs/plans/evals-nightly.md, part B1): the exact model, its turn
3
+ * count, its reported cost and its cache usage. Read only from the provider's own events and response bodies (Claude's
4
+ * session `system`/`init` and primary `result`, Codex's `turn.completed`, an API's response body), never from text an
5
+ * agent or a candidate wrote. Anything unreported or malformed is unknown (null), never 0.
6
+ *
7
+ * Three amounts are kept apart and never summed into one number: API dollars (work billed by use, with a reported or
8
+ * priced cost), subscription usage (the provider's own reported cost of work a plan covers: not billed), and unknown.
9
+ */
10
+ import { usageOf } from "./run-efficiency.js";
11
+ /** A model id as a provider reports it: printable ASCII with no spaces, at most 200 characters. Anything else is
12
+ * unknown, never trimmed into something that could read as a different model. */
13
+ const MODEL_ID = /^[\x21-\x7e]{1,200}$/;
14
+ /** Several reported models are kept as their sorted ids joined by this (it has spaces, so no one id can equal it):
15
+ * a run whose calls ran different models never reads as any one of them. */
16
+ export const MODELS_JOIN = " + ";
17
+ export function reportedModelOf(value) {
18
+ return typeof value === "string" && MODEL_ID.test(value) ? value : null;
19
+ }
20
+ /** The one model a Claude result's `modelUsage` names; null when it names none, several, or anything malformed. */
21
+ export function soleReportedModel(modelUsage) {
22
+ if (typeof modelUsage !== "object" || modelUsage === null || Array.isArray(modelUsage))
23
+ return null;
24
+ const keys = Object.keys(modelUsage);
25
+ return keys.length === 1 ? reportedModelOf(keys[0]) : null;
26
+ }
27
+ /** Two reports of the same piece of work (a resumed turn, a second init): unknown if either is, the model when they
28
+ * agree, otherwise every model named, sorted (never one chosen). */
29
+ export function combineReportedModels(a, b) {
30
+ if (a === null || b === null)
31
+ return null;
32
+ if (a === b)
33
+ return a;
34
+ return [...new Set([...a.split(MODELS_JOIN), ...b.split(MODELS_JOIN)])].sort().join(MODELS_JOIN);
35
+ }
36
+ export function providerTurnsOf(value) {
37
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
38
+ }
39
+ export function reportedCostUsdOf(value) {
40
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
41
+ }
42
+ /** A Claude primary `result` (streamed or buffered `--output-format json`), with what the session's inits named. The
43
+ * init's model is the session's own; inits that disagree leave it unknown; with no init naming one, the result's
44
+ * `modelUsage` counts only when it names exactly one model. */
45
+ export function claudeResultReport(result, init = { model: null, conflict: false }) {
46
+ const usage = usageOf({ provider: "claude", model: null, tokensIn: null, tokensOut: null, usageJson: usageJsonOf(result["usage"]) });
47
+ return {
48
+ modelReported: init.conflict ? null : init.model ?? soleReportedModel(result["modelUsage"]),
49
+ providerTurns: providerTurnsOf(result["num_turns"]),
50
+ reportedCostUsd: reportedCostUsdOf(result["total_cost_usd"]),
51
+ tokensIn: usage.in,
52
+ cacheReadTokens: usage.cacheRead,
53
+ cacheWriteTokens: usage.cacheWrite,
54
+ };
55
+ }
56
+ /** Codex's `turn.completed` usage: its input already counts what came from cache; it reports no cache writes, no
57
+ * model and no cost in `exec --json`, so those stay unknown. */
58
+ export function codexUsageReport(usage) {
59
+ const read = usageOf({ provider: "codex", model: null, tokensIn: null, tokensOut: null, usageJson: usageJsonOf(usage) });
60
+ return { modelReported: null, providerTurns: null, reportedCostUsd: null, tokensIn: read.in, cacheReadTokens: read.cacheRead, cacheWriteTokens: null };
61
+ }
62
+ function usageJsonOf(usage) {
63
+ return typeof usage === "object" && usage !== null && !Array.isArray(usage) ? JSON.stringify(usage) : null;
64
+ }
65
+ /** A run's amount, from its fixed billing and its priced spend (run_spend). Subscription work reads its provider's
66
+ * reported cost as usage, never as dollars; billed work without a reported or priced cost is unknown. v141 (Codex
67
+ * round 1 #4, #10): billing this computer's default guessed (`basis` not evidence) is unknown, and so is a run whose
68
+ * cost the provider never reported (its `cost_usd` then holds a conservative charge, never a measurement). */
69
+ export function runAmountOf(run) {
70
+ if (run.basis !== undefined && run.basis !== "evidence")
71
+ return { kind: "unknown" };
72
+ if (run.costUnknown === true)
73
+ return { kind: "unknown" };
74
+ if (run.billing === "subscription")
75
+ return { kind: "subscription", reportedUsd: reportedCostUsdOf(run.costUsd) };
76
+ if (run.billing === "api-key" && run.spendMicrousd !== null && Number.isSafeInteger(run.spendMicrousd) && run.spendMicrousd >= 0) {
77
+ return { kind: "api", microusd: run.spendMicrousd };
78
+ }
79
+ return { kind: "unknown" };
80
+ }
81
+ const SUBSCRIPTION_CHAT = new Set(["claude-subscription", "codex-subscription"]);
82
+ /** A chat request's amount: a subscription chat's reported cost is usage; an API chat's settled charge is dollars,
83
+ * unless its spend is unknown (then the amount is, whatever was settled against the reservation). v141 (Codex round 1
84
+ * #4): a subscription harness's request is subscription usage only when its own events said it was billed to the plan
85
+ * (`billingReported`); one they said used a key is API dollars at its reported cost; one they said nothing of is
86
+ * unknown. Absent `billingReported` keeps the older reading for callers that predate it. */
87
+ export function chatTurnAmountOf(turn) {
88
+ if (SUBSCRIPTION_CHAT.has(turn.provider)) {
89
+ if (turn.billingReported === undefined || turn.billingReported === "subscription")
90
+ return { kind: "subscription", reportedUsd: reportedCostUsdOf(turn.reportedCostUsd) };
91
+ const cost = reportedCostUsdOf(turn.reportedCostUsd);
92
+ return turn.billingReported === "api-key" && cost !== null ? { kind: "api", microusd: Math.round(cost * 1_000_000) } : { kind: "unknown" };
93
+ }
94
+ if (turn.unknownSpend || turn.settledMicrousd === null || !Number.isSafeInteger(turn.settledMicrousd) || turn.settledMicrousd < 0)
95
+ return { kind: "unknown" };
96
+ return { kind: "api", microusd: turn.settledMicrousd };
97
+ }
@@ -1396,6 +1396,8 @@ export function createChatHandlers(runtime) {
1396
1396
  const pinnedSettle = settleForPrice(enabled.price, result.answer.tokensIn, result.answer.tokensOut);
1397
1397
  const settled = Math.max(pinnedSettle, result.answer.reportedCostMicrousd ?? 0);
1398
1398
  const envelope = parseAssistantEnvelope(result.answer.text);
1399
+ // v140: what the provider's response body itself reported (absent: unreported, kept NULL).
1400
+ const reported = { modelReported: result.answer.modelReported ?? null, reportedCostUsd: result.answer.reportedCostUsd ?? null };
1399
1401
  if (!envelope.ok) {
1400
1402
  finish({
1401
1403
  state: "failed",
@@ -1403,6 +1405,7 @@ export function createChatHandlers(runtime) {
1403
1405
  tokensIn: result.answer.tokensIn,
1404
1406
  tokensOut: result.answer.tokensOut,
1405
1407
  settledMicrousd: settled,
1408
+ ...reported,
1406
1409
  });
1407
1410
  tell(null, "the model's answer was malformed and was discarded");
1408
1411
  return;
@@ -1437,6 +1440,7 @@ export function createChatHandlers(runtime) {
1437
1440
  settledMicrousd: settled,
1438
1441
  replyBytes: Buffer.byteLength(envelope.envelope.reply, "utf8"),
1439
1442
  candidateCount: kept,
1443
+ ...reported,
1440
1444
  });
1441
1445
  tell(envelope.envelope.reply, null, envelope.proposalsDiscarded);
1442
1446
  }
@@ -2519,6 +2519,7 @@ export function releaseCardHtml(taskId, release, canStop) {
2519
2519
  html `<details><summary>Later commits (${release.landed.length})</summary>${landed}</details>`,
2520
2520
  release.obligations.length === 0 ? "" : html `<details><summary>Still to pass (${release.obligations.length})</summary><ul>${joinHtml(release.obligations.map(one => html `<li>${one}</li>`), "")}</ul></details>`,
2521
2521
  release.nightly === null ? "" : html `<p class="meta">Nightly check of main: ${release.nightly.outcome ?? "running"} at ${release.nightly.sha.slice(0, 7)}${release.openMainFailures > 0 ? ` · ${release.openMainFailures} open failure${release.openMainFailures === 1 ? "" : "s"}` : ""}</p>`,
2522
+ ...release.githubNightly.map(line => html `<p class="meta">${line}</p>`),
2522
2523
  release.carrying && canStop ? postForm(`${taskHref(taskId)}/stop-carrying`, html `<button type="submit" class="secondary">Stop carrying later commits</button>`, { attrs: { class: "inline" } }) : "",
2523
2524
  html `</div>`,
2524
2525
  ], "\n");
@@ -20,6 +20,7 @@ import { withActor } from "../actor.js";
20
20
  import { gateWords } from "../approval-policy.js";
21
21
  import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator } from '../assignment.js';
22
22
  import { attentionCardForUnverifiedDone, classify } from "../board.js";
23
+ import { nightlyNeedsYou } from "../nightly-github.js";
23
24
  import { codingHandoffPreview, createCodingHandoff } from '../coding-handoff.js';
24
25
  import { codingShippingHtml } from '../coding-shipping-ui.js';
25
26
  import { codingWorkspaceHtml, codingWorkspaceScript } from '../coding-ui.js';
@@ -414,7 +415,13 @@ export function createTasksHandlers(runtime) {
414
415
  repairChain,
415
416
  });
416
417
  });
417
- const laneCards = [...cards, ...unverifiedCards];
418
+ // Project-level Needs you (evals-nightly.md, part A3): GitHub's nightly lane failures and the plane's own nightly
419
+ // check of main, for the projects this board shows.
420
+ const nightlyCards = nightlyNeedsYou(store, repo => visible(repo) && (all || project === null || repo === project)).map(need => ({
421
+ lane: "attention", taskId: need.key, title: need.title, repo: need.repo, reason: need.reason, href: need.href, claim: null,
422
+ stalledSince: need.since, attempt: null, overdue: false, priority: 0, assignedRunner: null,
423
+ }));
424
+ const laneCards = [...cards, ...unverifiedCards, ...nightlyCards];
418
425
  // The worker strip: the workers this board's work can go to — bound to
419
426
  // this project, or (rolled up) to any project the ceiling admits.
420
427
  const workers = boardWorkers(store.listRunners(), { all, project, visible });