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.
- package/CHANGELOG.md +47 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/guides.js +12 -9
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- package/dist/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +91 -1
- package/dist/operate.js +158 -1
- package/dist/project-knowledge.d.ts +4 -4
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +53 -20
- package/dist/release-auto.d.ts +1 -1
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +36 -17
- package/dist/release.js +74 -24
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/server/chat.js +4 -0
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/tasks.js +8 -1
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
package/dist/operate.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type IntegrationIo } from "./integrations.js";
|
|
|
2
2
|
import { type OnboardIo } from "./agent-onboard.js";
|
|
3
3
|
import { type Sizer } from "./task-sizing.js";
|
|
4
4
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
5
|
+
import { type GhRead } from "./nightly-github.js";
|
|
5
6
|
import type { StepIo } from "./flow-steps.js";
|
|
6
7
|
import type { FetchLike } from "./flow-share.js";
|
|
7
8
|
import { type Store } from "./store.js";
|
|
@@ -9,7 +10,7 @@ import { type DispatchAdapter } from "./sync.js";
|
|
|
9
10
|
import { type RolePort, type UpRoleStart } from "./up-processes.js";
|
|
10
11
|
import { type TelegramTransport } from "./telegram.js";
|
|
11
12
|
import { type LeadCliSeams } from "./lead-cli.js";
|
|
12
|
-
import { type Principal, type RemoteSource } from "./operate-remote.js";
|
|
13
|
+
import { type Principal, type RemoteRun, type RemoteSource } from "./operate-remote.js";
|
|
13
14
|
import { type PublishExec } from "./publish.js";
|
|
14
15
|
import { type Runner as CommandRunner } from "./builder.js";
|
|
15
16
|
import { type ReleaseIo } from "./releases.js";
|
|
@@ -150,6 +151,79 @@ export declare function runOperateAs(argv: string[], opts: {
|
|
|
150
151
|
tool?: string;
|
|
151
152
|
options?: OperateOptions;
|
|
152
153
|
}): Promise<number>;
|
|
154
|
+
type Context = {
|
|
155
|
+
/** A remote run's person (runOperateAs): every credential check resolves to them. Absent for every local command. */
|
|
156
|
+
principal?: RemoteRun;
|
|
157
|
+
/** The lead token this command runs under: it signs in as its owner. */
|
|
158
|
+
leadToken?: string;
|
|
159
|
+
desktopIdentity?: string;
|
|
160
|
+
/** A desktop service directory is state, never an implicitly enrolled project. */
|
|
161
|
+
inferProjectFromCwd?: boolean;
|
|
162
|
+
/** Exact additions approved by the native project picker, independent of the enrollment registry. */
|
|
163
|
+
additionalProjectRepos?: () => readonly string[];
|
|
164
|
+
store: Store;
|
|
165
|
+
write: Write;
|
|
166
|
+
json: boolean;
|
|
167
|
+
/** Every --repo occurrence in order (arc 2) — only `up` reads it. */
|
|
168
|
+
repoList?: string[];
|
|
169
|
+
now: Date;
|
|
170
|
+
clock: () => Date;
|
|
171
|
+
evidenceRoot: string;
|
|
172
|
+
/** The directory holding the database — where credential files live. */
|
|
173
|
+
databaseFile: string;
|
|
174
|
+
/** Where `storage` looks for leftover test temp folders; default the temp folder and /tmp. */
|
|
175
|
+
tempRoots?: readonly string[];
|
|
176
|
+
/** The long-lived service's token: only a pass carrying it may start the automatic storage sweep. */
|
|
177
|
+
sweepOwner?: object;
|
|
178
|
+
telegramTokenFile: string;
|
|
179
|
+
agentRunner?: CommandRunner;
|
|
180
|
+
gitRunner?: CommandRunner;
|
|
181
|
+
telegramTransport?: TelegramTransport;
|
|
182
|
+
telegramEnv?: Record<string, string | undefined>;
|
|
183
|
+
/** Injected by tests: what `publish` runs for git and gh. */
|
|
184
|
+
publishExec?: PublishExec;
|
|
185
|
+
leadSeams?: LeadCliSeams;
|
|
186
|
+
/** Injected by tests: the external-dispatch gh surface. */
|
|
187
|
+
dispatchAdapter?: DispatchAdapter;
|
|
188
|
+
/** Injected by tests: how flow triggers reach GitHub (gh) and Linear (fetch), and where their secrets live. */
|
|
189
|
+
flowTriggerIo?: Partial<TriggerIo>;
|
|
190
|
+
/** Injected by tests: how flow check and update steps run commands and reach GitHub and Linear. */
|
|
191
|
+
flowStepIo?: Partial<StepIo>;
|
|
192
|
+
/** Injected by tests: how often a worker's flow passes run beside its builds (default FLOW_EVERY_MS). */
|
|
193
|
+
flowEveryMs?: number;
|
|
194
|
+
/** Injected by tests: the operating system this runs on (default process.platform). */
|
|
195
|
+
platform?: NodeJS.Platform;
|
|
196
|
+
/** Injected by tests: whether this machine has an OS fence for checks (default: probed). */
|
|
197
|
+
fenceAvailable?: () => boolean;
|
|
198
|
+
/** Injected by tests: how `flows import` fetches a flow file's address. */
|
|
199
|
+
flowFetch?: FetchLike;
|
|
200
|
+
integrationIo?: Partial<IntegrationIo>;
|
|
201
|
+
/**
|
|
202
|
+
* The stop fence (Codex M5-M8 audit, IV-1): set by the watch when a
|
|
203
|
+
* signal lands. A pass that sees true admits NOTHING more — no schedule
|
|
204
|
+
* fires, no claim, no run, no spawn. The in-flight build finishes under
|
|
205
|
+
* its own bounds (or the grace kill); admission is what stops.
|
|
206
|
+
*/
|
|
207
|
+
shouldStop?: () => boolean;
|
|
208
|
+
/** A failed recovery pass pauses new admissions without stopping an owned build. */
|
|
209
|
+
shouldPauseAdmission?: () => boolean;
|
|
210
|
+
/** Test seam for task wait; production uses a timer. */
|
|
211
|
+
waitSleep?: (milliseconds: number) => Promise<void>;
|
|
212
|
+
releaseIo?: ReleaseIo;
|
|
213
|
+
installBin?: string;
|
|
214
|
+
onboardSeams?: OnboardSeams;
|
|
215
|
+
upSeams?: {
|
|
216
|
+
terminal?: boolean;
|
|
217
|
+
env?: Record<string, string | undefined>;
|
|
218
|
+
openBrowser?: (url: string) => void;
|
|
219
|
+
};
|
|
220
|
+
/** This process is one role of a `toolroll up`, started by it (never from the command line). */
|
|
221
|
+
upRole?: UpRoleHost;
|
|
222
|
+
/** In-memory seams a child process can't receive: `up` runs its console and worker in this one process. */
|
|
223
|
+
upOneProcess?: boolean;
|
|
224
|
+
/** Test seam: this computer's tailnet names (production asks `tailscale status`). */
|
|
225
|
+
tailnetNames?: () => Promise<readonly string[]>;
|
|
226
|
+
};
|
|
153
227
|
/** Durably create the login file BEFORE the bootstrap commits (arc 2
|
|
154
228
|
* finding 27): exclusive, 0600, fsynced — file and directory both — so the
|
|
155
229
|
* row only ever follows a durable secret. Throws on any failure. */
|
|
@@ -181,3 +255,19 @@ export type UpRoleHost = {
|
|
|
181
255
|
/** A role child's whole run (up-role-child.ts): the ordinary command prelude (containment, secrets, limits), then the
|
|
182
256
|
* role itself. Only a process started by `up` gets here; no command line selects a role. */
|
|
183
257
|
export declare function runUpRole(start: UpRoleStart, port: RolePort): Promise<number>;
|
|
258
|
+
/** GitHub's nightly as release debt (docs/plans/evals-nightly.md, part A): the watch loop's background read of each
|
|
259
|
+
* opted-in project's nightly runs, at most every 30 minutes, within a 60 s overall deadline, through the plane's own gh
|
|
260
|
+
* (release-adapters.ts githubReads). Nothing is read for a project that never opted in. */
|
|
261
|
+
export declare function refreshGithubNightlies(store: Store, now: () => Date, reads?: GhRead): Promise<number>;
|
|
262
|
+
/**
|
|
263
|
+
* `toolroll evals run | attempt | cases | recover` (docs/plans/evals-nightly.md, part C).
|
|
264
|
+
*
|
|
265
|
+
* - `run [--diagnostic] [--case a,b] [--k n] --as <you> --token <password>`: admit a campaign (a person's proof, the
|
|
266
|
+
* no release busy) and run it to its end from this installed plane. With no flags it is canonical,
|
|
267
|
+
* refused while any case is a draft. Each attempt is a child process: `evals attempt`.
|
|
268
|
+
* - `attempt <campaign> <case> <n> --world <dir> --db <world db>`: one attempt's world (eval-world.ts). Only under its
|
|
269
|
+
* runner's dispatch gate, and only on the world's own database.
|
|
270
|
+
* - `cases`: each installed case, its version, its pin, and whether a person has validated it yet.
|
|
271
|
+
* - `recover [--abandon <campaign>]`: settle campaigns whose runner stopped; with --abandon, a person ends a stuck one.
|
|
272
|
+
*/
|
|
273
|
+
export declare function evalsCommand(positional: readonly string[], flags: Map<string, string | true>, context: Context): Promise<number>;
|
package/dist/operate.js
CHANGED
|
@@ -60,6 +60,9 @@ import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
|
|
|
60
60
|
import { refreshConnections } from "./mcp-connect.js";
|
|
61
61
|
import { releaseCheckPurposeFor, releaseTermsFor } from "./release-carry.js";
|
|
62
62
|
import { refreshTrustedMains, settleReleaseChecks } from "./nightly-check.js";
|
|
63
|
+
import { githubNightlyRepos, readGithubNightlies } from "./nightly-github.js";
|
|
64
|
+
import { exec as releaseExec, githubReads } from "./release-adapters.js";
|
|
65
|
+
import { releaseTools } from "./release-git.js";
|
|
63
66
|
import { SECRETS_FIRST, startToolSecrets, toolSecretStorage } from "./project-tools.js";
|
|
64
67
|
import { followDiscord } from "./discord.js";
|
|
65
68
|
import { followTeams } from "./teams.js";
|
|
@@ -99,7 +102,7 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
99
102
|
* **Nothing ever prompts.** There is no terminal on the other end at 3am.
|
|
100
103
|
*/
|
|
101
104
|
import { homedir, hostname } from "node:os";
|
|
102
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
105
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
103
106
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
104
107
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
105
108
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
@@ -247,6 +250,7 @@ import { installationStatus, renderInstallationStatus, renderTaskWait, repairSta
|
|
|
247
250
|
import { latestRelease, newerRelease, notifySecurityRelease, notifyVersionLive, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
248
251
|
import { installMethod } from "./install-method.js";
|
|
249
252
|
import { PACKAGE_VERSION } from "./version.js";
|
|
253
|
+
import { machineActing } from "./scope.js";
|
|
250
254
|
/**
|
|
251
255
|
* 0 done · 1 broke · 2 bad usage · 3 ran fine, the answer is no.
|
|
252
256
|
*
|
|
@@ -676,6 +680,9 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
676
680
|
"choose", "note", "max-open-decisions", "name", "days", "publication", "auto-approve", "review-auto", "port", "host", "allow-host",
|
|
677
681
|
"for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
|
|
678
682
|
"say", "ceiling-usd", "cap-usd",
|
|
683
|
+
// evals: the cases a diagnostic campaign runs, its k, an attempt's world and what its builds may not read, and the
|
|
684
|
+
// stuck campaign a person ends (recover --abandon).
|
|
685
|
+
"case", "k", "world", "deny", "abandon",
|
|
679
686
|
"token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
|
|
680
687
|
// v102: project rules.
|
|
681
688
|
"not-requester", "protect", "protect-paths",
|
|
@@ -714,6 +721,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
714
721
|
"release",
|
|
715
722
|
]);
|
|
716
723
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
724
|
+
// evals run: a report-only campaign; keep each attempt's world for a look afterwards.
|
|
725
|
+
"diagnostic", "keep",
|
|
717
726
|
// task scope --release ... --no-carry: every later commit asks again.
|
|
718
727
|
"no-carry",
|
|
719
728
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
|
|
@@ -1147,6 +1156,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1147
1156
|
switch (command) {
|
|
1148
1157
|
case "status":
|
|
1149
1158
|
return statusCommand(positional, flags, context);
|
|
1159
|
+
case "evals":
|
|
1160
|
+
return evalsCommand(positional, flags, context);
|
|
1150
1161
|
case "ready":
|
|
1151
1162
|
return readyCommand(flags, context);
|
|
1152
1163
|
case "task":
|
|
@@ -6279,6 +6290,10 @@ async function runWatchLoop(args) {
|
|
|
6279
6290
|
const settledChecks = settleReleaseChecks(store, context.evidenceRoot, context.clock());
|
|
6280
6291
|
if (settledChecks > 0)
|
|
6281
6292
|
progress(`watch: settled ${settledChecks} release check(s)`);
|
|
6293
|
+
// GitHub's nightly as release debt (nightly-github.ts): each opted-in project at most every 30 minutes, 60 s in
|
|
6294
|
+
// all. A test's injected agent runner means a test: no real gh.
|
|
6295
|
+
if (context.agentRunner === undefined)
|
|
6296
|
+
await refreshGithubNightlies(store, context.clock);
|
|
6282
6297
|
// A finished check may send a build's card down its failure path: now, not on the next beat.
|
|
6283
6298
|
if (ran + batched > 0)
|
|
6284
6299
|
moveCards(store, repo, context.clock(), context.evidenceRoot);
|
|
@@ -12373,3 +12388,145 @@ async function runUpWorkerRole(context, config, port, build) {
|
|
|
12373
12388
|
}
|
|
12374
12389
|
return EXIT.ok;
|
|
12375
12390
|
}
|
|
12391
|
+
/** GitHub's nightly as release debt (docs/plans/evals-nightly.md, part A): the watch loop's background read of each
|
|
12392
|
+
* opted-in project's nightly runs, at most every 30 minutes, within a 60 s overall deadline, through the plane's own gh
|
|
12393
|
+
* (release-adapters.ts githubReads). Nothing is read for a project that never opted in. */
|
|
12394
|
+
export async function refreshGithubNightlies(store, now, reads) {
|
|
12395
|
+
if (githubNightlyRepos(store).length === 0)
|
|
12396
|
+
return 0;
|
|
12397
|
+
return readGithubNightlies(store, { gh: reads ?? githubReads(releaseExec, releaseTools().gh, { retry: true }), now });
|
|
12398
|
+
}
|
|
12399
|
+
/**
|
|
12400
|
+
* `toolroll evals run | attempt | cases | recover` (docs/plans/evals-nightly.md, part C).
|
|
12401
|
+
*
|
|
12402
|
+
* - `run [--diagnostic] [--case a,b] [--k n] --as <you> --token <password>`: admit a campaign (a person's proof, the
|
|
12403
|
+
* no release busy) and run it to its end from this installed plane. With no flags it is canonical,
|
|
12404
|
+
* refused while any case is a draft. Each attempt is a child process: `evals attempt`.
|
|
12405
|
+
* - `attempt <campaign> <case> <n> --world <dir> --db <world db>`: one attempt's world (eval-world.ts). Only under its
|
|
12406
|
+
* runner's dispatch gate, and only on the world's own database.
|
|
12407
|
+
* - `cases`: each installed case, its version, its pin, and whether a person has validated it yet.
|
|
12408
|
+
* - `recover [--abandon <campaign>]`: settle campaigns whose runner stopped; with --abandon, a person ends a stuck one.
|
|
12409
|
+
*/
|
|
12410
|
+
export async function evalsCommand(positional, flags, context) {
|
|
12411
|
+
const { write, json } = context;
|
|
12412
|
+
// Loaded when an eval command runs, so no other command (nor the console's bundle) pays for the eval runner.
|
|
12413
|
+
const { admitEvalCampaign, campaignSpending, EVALS_ROOT, installedBuildDigest, loadEvalSet, planCampaign, recoverEvalCampaigns, runEvalCampaign } = await import("./evals.js");
|
|
12414
|
+
const { inEvalWorld } = await import("./eval-gate.js");
|
|
12415
|
+
const { evalCliEntry, runEvalWorld } = await import("./eval-world.js");
|
|
12416
|
+
const action = positional[0];
|
|
12417
|
+
const command = `evals${action === undefined ? "" : ` ${action}`}`;
|
|
12418
|
+
const usage = "Use: toolroll evals run [--diagnostic] [--case <ids>] [--k <n>] --as <you> --token <password> | toolroll evals cases | toolroll evals recover [--abandon <campaign> --as <you> --token <password>]";
|
|
12419
|
+
if (action === "recover" && flags.has("abandon"))
|
|
12420
|
+
return evalsAbandonCommand(flags, context);
|
|
12421
|
+
if (action === "recover") {
|
|
12422
|
+
// A campaign whose runner stopped: its unfinished attempts recorded as interrupted, then it ends incomplete.
|
|
12423
|
+
const recovered = recoverEvalCampaigns(context.store, context.clock());
|
|
12424
|
+
return succeed(write, json, command, { campaigns: recovered }, () => recovered.length === 0 ? ["No campaign is running."] : recovered.map(one => `${one.id}: ${one.recovered ? "recovered" : "left running"} (${one.why})`));
|
|
12425
|
+
}
|
|
12426
|
+
if (action === "cases") {
|
|
12427
|
+
const set = loadEvalSet();
|
|
12428
|
+
if (!set.ok)
|
|
12429
|
+
return fail(write, json, command, "unreadable", set.problem, EXIT.failed);
|
|
12430
|
+
const cases = set.cases.map(one => ({ id: one.id, kind: one.kind, version: one.version, pin: one.pin, validatedBy: one.validatedBy, draft: one.validatedBy === null }));
|
|
12431
|
+
return succeed(write, json, command, { manifestDigest: set.manifestDigest, cases }, () => cases.map(one => `${one.id} (${one.kind}) ${one.version.slice(0, 12)} on ${one.pin.model}: ${one.draft ? "draft, awaiting a person's validation" : `validated by ${one.validatedBy}`}`));
|
|
12432
|
+
}
|
|
12433
|
+
if (action === "attempt") {
|
|
12434
|
+
// Only its campaign's runner starts an attempt: under the runner's dispatch gate, on the world's own database.
|
|
12435
|
+
if (!inEvalWorld())
|
|
12436
|
+
return fail(write, json, command, "refused", "An eval attempt runs only from its campaign's runner.", EXIT.refused);
|
|
12437
|
+
const [, campaign, caseId, n] = positional;
|
|
12438
|
+
const world = text(flags, "world");
|
|
12439
|
+
if (campaign === undefined || caseId === undefined || n === undefined || !/^[1-9][0-9]?$/.test(n) || world === undefined)
|
|
12440
|
+
return fail(write, json, command, "usage", "Use: toolroll evals attempt <campaign> <case> <n> --world <dir> --db <world>/toolroll/orders.db", EXIT.usage);
|
|
12441
|
+
let worldDir;
|
|
12442
|
+
try {
|
|
12443
|
+
worldDir = realpathSync(world);
|
|
12444
|
+
}
|
|
12445
|
+
catch {
|
|
12446
|
+
return fail(write, json, command, "refused", "The world's folder can't be read.", EXIT.refused);
|
|
12447
|
+
}
|
|
12448
|
+
if (resolve(context.databaseFile) !== join(worldDir, "toolroll", "orders.db"))
|
|
12449
|
+
return fail(write, json, command, "refused", "An eval attempt runs only on its world's own database.", EXIT.refused);
|
|
12450
|
+
const set = loadEvalSet();
|
|
12451
|
+
const one = set.ok ? set.cases.find(item => item.id === caseId) : undefined;
|
|
12452
|
+
if (one === undefined)
|
|
12453
|
+
return fail(write, json, command, "refused", set.ok ? `No case is named ${caseId}.` : set.problem, EXIT.refused);
|
|
12454
|
+
const deny = (text(flags, "deny") ?? "").split(",").filter(path => path !== "" && isAbsolute(path));
|
|
12455
|
+
const report = await runEvalWorld(one, { worldDir, databaseFile: context.databaseFile, deny, timeoutMs: one.timeoutMinutes * 60_000 });
|
|
12456
|
+
return succeed(write, json, command, { ...report }, () => [JSON.stringify(report)]);
|
|
12457
|
+
}
|
|
12458
|
+
if (action !== "run" || positional.length !== 1)
|
|
12459
|
+
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
12460
|
+
if (inEvalWorld())
|
|
12461
|
+
return fail(write, json, command, "refused", "An eval world doesn't start campaigns of its own.", EXIT.refused);
|
|
12462
|
+
if (machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined)
|
|
12463
|
+
return fail(write, json, command, "person-only", "Only a person starts an eval campaign: it spends money.", EXIT.refused);
|
|
12464
|
+
const k = text(flags, "k");
|
|
12465
|
+
const cases = text(flags, "case");
|
|
12466
|
+
const set = loadEvalSet();
|
|
12467
|
+
if (!set.ok)
|
|
12468
|
+
return fail(write, json, command, "unreadable", set.problem, EXIT.failed);
|
|
12469
|
+
const planned = planCampaign(set, { ...(cases === undefined ? {} : { cases: cases.split(",").filter(Boolean) }), ...(k === undefined ? {} : { k: Number(k) }), diagnostic: flag(flags, "diagnostic") });
|
|
12470
|
+
if (!planned.ok)
|
|
12471
|
+
return fail(write, json, command, "refused", planned.problem, EXIT.refused);
|
|
12472
|
+
const acting = await askCredentials(flags, context);
|
|
12473
|
+
if (acting === null)
|
|
12474
|
+
return fail(write, json, command, "usage", "An eval campaign is started with your password: add --as <you> --token <password>.", EXIT.usage);
|
|
12475
|
+
const { plan } = planned;
|
|
12476
|
+
const buildDigest = () => installedBuildDigest(PACKAGE_VERSION);
|
|
12477
|
+
const sealed = { cases: plan.cases.map(one => ({ id: one.id, version: one.version, pin: one.pin, maxDispatches: one.maxDispatches })), k: plan.k, baseline: null, evals: EVALS_ROOT };
|
|
12478
|
+
// A campaign whose runner stopped no longer holds its place.
|
|
12479
|
+
recoverEvalCampaigns(context.store, context.clock());
|
|
12480
|
+
const admitted = admitEvalCampaign(context.store, { kind: "eval", canonical: plan.canonical, manifestDigest: set.manifestDigest, buildDigest: buildDigest(), version: PACKAGE_VERSION, k: plan.k,
|
|
12481
|
+
sealed, as: acting.name, token: acting.token, now: context.clock() });
|
|
12482
|
+
if (!admitted.ok)
|
|
12483
|
+
return fail(write, json, command, admitted.reason, admitted.message, EXIT.refused);
|
|
12484
|
+
const stateDir = dirname(context.databaseFile);
|
|
12485
|
+
const entry = evalCliEntry();
|
|
12486
|
+
const keep = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "SHELL", "TMPDIR"];
|
|
12487
|
+
const ended = await runEvalCampaign(context.store, admitted.id, plan.cases, {
|
|
12488
|
+
// Beside the plane's state folder, never inside it: a world's builds read-deny the plane's state.
|
|
12489
|
+
workRoot: join(dirname(stateDir), "toolroll-eval-worlds", admitted.id),
|
|
12490
|
+
buildDigest,
|
|
12491
|
+
keepWorlds: flag(flags, "keep"),
|
|
12492
|
+
now: context.clock,
|
|
12493
|
+
command: input => ({ file: entry.file, args: [...entry.args, "evals", "attempt", input.campaign, input.caseId, String(input.n), "--world", input.worldDir, "--deny", stateDir, "--db", input.databaseFile, "--json"],
|
|
12494
|
+
env: { ...Object.fromEntries(keep.flatMap(name => process.env[name] === undefined ? [] : [[name, process.env[name]]])), XDG_CONFIG_HOME: join(input.worldDir, "config") } }),
|
|
12495
|
+
});
|
|
12496
|
+
const rows = context.store.handle.prepare("SELECT case_id, n, replaces, outcome, error_why, api_microusd, subscription_microusd, usage_unknown FROM eval_attempt WHERE campaign = ? ORDER BY case_id, n, replaces").all(admitted.id);
|
|
12497
|
+
const nullable = (value) => value === null || value === undefined ? null : Number(value);
|
|
12498
|
+
const attempts = rows.map(row => ({ case: String(row["case_id"]), n: Number(row["n"]), replaces: Number(row["replaces"]), outcome: row["outcome"] === null ? null : String(row["outcome"]),
|
|
12499
|
+
why: row["error_why"] === null ? null : String(row["error_why"]), apiMicrousd: nullable(row["api_microusd"]), subscriptionMicrousd: nullable(row["subscription_microusd"]), usageUnknown: Number(row["usage_unknown"]) === 1 }));
|
|
12500
|
+
// What was actually spent, as recorded: API dollars and subscription usage apart, unknown never shown as measured.
|
|
12501
|
+
const spent = campaignSpending(context.store, admitted.id);
|
|
12502
|
+
const dollars = (micro, unknown) => micro === null ? `unknown (${unknown} attempt${unknown === 1 ? "" : "s"} with unknown spending)` : `$${(micro / 1_000_000).toFixed(2)}`;
|
|
12503
|
+
return succeed(write, json, command, { campaign: admitted.id, canonical: plan.canonical, state: ended.state, why: ended.why, attempts, spending: spent }, () => [
|
|
12504
|
+
`Campaign ${admitted.id} (${plan.canonical ? "canonical" : "diagnostic"}) is ${ended.state}${ended.why === null ? "" : `: ${ended.why}`}.`,
|
|
12505
|
+
...attempts.map(one => `${one.case} #${one.n}${one.replaces === 0 ? "" : " (replacement)"}: ${one.outcome ?? "not finished"}${one.why === null ? "" : ` — ${one.why}`}`),
|
|
12506
|
+
`API dollars: ${dollars(spent.apiMicrousd, spent.unknownAttempts)}. Subscription usage, not billed: ${dollars(spent.subscriptionMicrousd, spent.unknownAttempts)}.`,
|
|
12507
|
+
]);
|
|
12508
|
+
}
|
|
12509
|
+
/**
|
|
12510
|
+
* `toolroll evals recover --abandon <campaign> --as <you> --token <password>` (PR #263 review): a person ends a campaign
|
|
12511
|
+
* recovery can't settle (its runner or a world's processes can't be proven gone). Its unfinished attempts are recorded
|
|
12512
|
+
* as errors and its spending as unknown, and it ends incomplete; nothing is signalled and no world is removed. A lead
|
|
12513
|
+
* token, a machine or an eval world is refused; the password is checked inside the write (evals.ts abandonEvalCampaign).
|
|
12514
|
+
*/
|
|
12515
|
+
async function evalsAbandonCommand(flags, context) {
|
|
12516
|
+
const { write, json } = context;
|
|
12517
|
+
const command = "evals recover";
|
|
12518
|
+
const { abandonEvalCampaign } = await import("./evals.js");
|
|
12519
|
+
const { inEvalWorld } = await import("./eval-gate.js");
|
|
12520
|
+
const id = text(flags, "abandon");
|
|
12521
|
+
if (id === undefined || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(id))
|
|
12522
|
+
return fail(write, json, command, "usage", "Use: toolroll evals recover --abandon <campaign> --as <you> --token <password>", EXIT.usage);
|
|
12523
|
+
if (inEvalWorld() || machineActing() || process.env["TOOLROLL_LEAD_TOKEN"] !== undefined)
|
|
12524
|
+
return fail(write, json, command, "person-only", "Only a person abandons an eval campaign.", EXIT.refused);
|
|
12525
|
+
const acting = await askCredentials(flags, context);
|
|
12526
|
+
if (acting === null)
|
|
12527
|
+
return fail(write, json, command, "usage", "A campaign is abandoned with your password: add --as <you> --token <password>.", EXIT.usage);
|
|
12528
|
+
const ended = abandonEvalCampaign(context.store, { id, as: acting.name, token: acting.token, now: context.clock() });
|
|
12529
|
+
if (!ended.ok)
|
|
12530
|
+
return fail(write, json, command, ended.reason, ended.message, EXIT.refused);
|
|
12531
|
+
return succeed(write, json, command, { campaign: id, state: "incomplete", attempts: ended.attempts, spending: "unknown" }, () => [`Campaign ${id} ended incomplete. ${ended.attempts} unfinished attempt${ended.attempts === 1 ? "" : "s"} recorded as errors; what it spent is unknown. Nothing was stopped: check for its processes yourself.`]);
|
|
12532
|
+
}
|
|
@@ -23,18 +23,18 @@ export declare function knowledgeVersion(store: Store, repo: string, actor: stri
|
|
|
23
23
|
/** Chat reads only an admitted project. Keep the full source in the project UI. */
|
|
24
24
|
export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
|
|
25
25
|
title?: never;
|
|
26
|
+
content?: never;
|
|
26
27
|
history?: never;
|
|
27
28
|
revision: number;
|
|
28
29
|
stale: boolean;
|
|
29
30
|
notice: string;
|
|
30
31
|
decision?: never;
|
|
31
|
-
content?: never;
|
|
32
32
|
instructions?: never;
|
|
33
33
|
references?: never;
|
|
34
34
|
decisions?: never;
|
|
35
35
|
} | {
|
|
36
36
|
title?: never;
|
|
37
|
-
|
|
37
|
+
content?: never;
|
|
38
38
|
history?: never;
|
|
39
39
|
stale?: never;
|
|
40
40
|
decision: {
|
|
@@ -52,7 +52,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
52
52
|
id: number;
|
|
53
53
|
};
|
|
54
54
|
notice: string;
|
|
55
|
-
|
|
55
|
+
revision?: never;
|
|
56
56
|
instructions?: never;
|
|
57
57
|
references?: never;
|
|
58
58
|
decisions?: never;
|
|
@@ -69,9 +69,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
|
|
|
69
69
|
decisions?: never;
|
|
70
70
|
} | {
|
|
71
71
|
title?: never;
|
|
72
|
+
content?: never;
|
|
72
73
|
stale?: never;
|
|
73
74
|
decision?: never;
|
|
74
|
-
content?: never;
|
|
75
75
|
revision: number;
|
|
76
76
|
history: {
|
|
77
77
|
revision: number;
|
package/dist/provider.d.ts
CHANGED
|
@@ -177,6 +177,9 @@ export type ParsedEnvelope = {
|
|
|
177
177
|
structuralTerminal: import("./exhaustion.js").StructuralTerminal | null;
|
|
178
178
|
/** What an ACP agent itself reported it used — kept on the run, never priced. Absent for every other dialect. */
|
|
179
179
|
agentReportedUsage?: string | null;
|
|
180
|
+
/** v140: the exact model the provider itself reported for this turn (Claude: its session's init, or a buffered
|
|
181
|
+
* result's one `modelUsage` model). Absent or null: not reported — every other dialect today. */
|
|
182
|
+
modelReported?: string | null;
|
|
180
183
|
/** An ACP conversation's facts the conformance check reads (acp-client.ts AcpEnvelope): nothing else does. */
|
|
181
184
|
acp?: {
|
|
182
185
|
offered: string[];
|
package/dist/provider.js
CHANGED
|
@@ -30,6 +30,7 @@ import { ACP_AGENTS, isAcpProvider, withAcp } from "./acp-agents.js";
|
|
|
30
30
|
import { acpParse } from "./acp-client.js";
|
|
31
31
|
import { agentSpecSchema, MODEL_ID, modelIdSchema, PROVIDER_ID_VALUES, providerIdSchema } from "./contracts/provider.js";
|
|
32
32
|
import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
|
|
33
|
+
import { claudeResultReport, reportedModelOf } from "./reported-usage.js";
|
|
33
34
|
export const PROVIDER_IDS = PROVIDER_ID_VALUES;
|
|
34
35
|
export function isProviderId(value) {
|
|
35
36
|
return providerIdSchema.safeParse(value).success;
|
|
@@ -293,7 +294,7 @@ function claudeFinalMessage(result) {
|
|
|
293
294
|
}
|
|
294
295
|
return typeof result.result === "string" ? result.result : null;
|
|
295
296
|
}
|
|
296
|
-
function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConflict = false) {
|
|
297
|
+
function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConflict = false, initModel = { model: null, conflict: false }) {
|
|
297
298
|
const input = result?.usage?.input_tokens;
|
|
298
299
|
const output = result?.usage?.output_tokens;
|
|
299
300
|
const cost = result?.total_cost_usd;
|
|
@@ -322,6 +323,8 @@ function claudeEnvelopeOf(result, sessionFromInit, initObserved, initSessionConf
|
|
|
322
323
|
costUsd: typeof cost === "number" && cost >= 0 ? cost : null,
|
|
323
324
|
...(result !== null && result[CLAUDE_COST_UNKNOWN] === true ? { costUnknown: true } : {}),
|
|
324
325
|
usageRaw: result?.usage === undefined ? null : JSON.stringify(result.usage).slice(0, USAGE_JSON_CAP),
|
|
326
|
+
// The model only when the session's own events name it (never the agent's words); no result, no report.
|
|
327
|
+
modelReported: result === null ? (initModel.conflict ? null : initModel.model) : claudeResultReport(result, initModel).modelReported,
|
|
325
328
|
initObserved,
|
|
326
329
|
// Structural, never prose (finding 15): consumed means the primary
|
|
327
330
|
// result says SUCCESS. An error result keeps its text as diagnostics
|
|
@@ -354,6 +357,7 @@ function claudeParse(stdout) {
|
|
|
354
357
|
let initSeen = false;
|
|
355
358
|
let sessionFromInit = null;
|
|
356
359
|
let initSessionConflict = false;
|
|
360
|
+
const initModels = new Set();
|
|
357
361
|
let primary = null;
|
|
358
362
|
let transportFailure = null;
|
|
359
363
|
for (const line of stdout.split("\n")) {
|
|
@@ -379,6 +383,9 @@ function claudeParse(stdout) {
|
|
|
379
383
|
}
|
|
380
384
|
if (String(type) === "system" && String(event["subtype"] ?? "") === "init") {
|
|
381
385
|
initSeen = true;
|
|
386
|
+
const model = reportedModelOf(event["model"]);
|
|
387
|
+
if (model !== null)
|
|
388
|
+
initModels.add(model);
|
|
382
389
|
const id = sessionIdOf(event["session_id"]);
|
|
383
390
|
if (id !== null) {
|
|
384
391
|
if (sessionFromInit === null)
|
|
@@ -402,7 +409,7 @@ function claudeParse(stdout) {
|
|
|
402
409
|
primary = result;
|
|
403
410
|
}
|
|
404
411
|
}
|
|
405
|
-
const envelope = claudeEnvelopeOf(primary, sessionFromInit, initSeen, initSessionConflict);
|
|
412
|
+
const envelope = claudeEnvelopeOf(primary, sessionFromInit, initSeen, initSessionConflict, { model: initModels.size === 1 ? [...initModels][0] : null, conflict: initModels.size > 1 });
|
|
406
413
|
if (transportFailure === null)
|
|
407
414
|
return envelope;
|
|
408
415
|
return {
|
|
@@ -8,6 +8,8 @@ import type { CheckRun, GateState, Installed, MainProtection, ReleaseAdapters }
|
|
|
8
8
|
import { Store } from "./store.js";
|
|
9
9
|
import { type CiObservation } from "./release-auto.js";
|
|
10
10
|
import { type BuildRunner } from "./release-carry.js";
|
|
11
|
+
import { type GhRead } from "./nightly-github.js";
|
|
12
|
+
export { failedBeforeTests, type JobRecord } from "./github-jobs.js";
|
|
11
13
|
import { type ReleaseRepo, type ReleaseTools } from "./release-git.js";
|
|
12
14
|
export { RELEASE_PATH, RELEASE_REMOTE, releaseConfigProblem, releaseGitArgs, releaseGitEnv, releaseRemote, releaseTools, type ReleaseRepo, type ReleaseTools } from "./release-git.js";
|
|
13
15
|
export type Exec = (command: string, args: readonly string[], options?: {
|
|
@@ -112,7 +114,10 @@ export declare function bumpFormula(text: string, url: string, sha256: string):
|
|
|
112
114
|
export declare function storeReleaseAdapter(open: () => Store, done: (store: Store) => void, now: () => Date, runner?: BuildRunner,
|
|
113
115
|
/** Releases that approve themselves (auto-approve-green.md): the plane's own look at CI on a commit, and where the
|
|
114
116
|
* check receipts are kept. Absent: no automatic approval, every release asks a person. */
|
|
115
|
-
auto?: AutoObservers
|
|
117
|
+
auto?: AutoObservers,
|
|
118
|
+
/** GitHub's nightly (nightly-github.ts): the plane's own gh reads for a boundary's one bounded read. Absent: nothing
|
|
119
|
+
* is read at the boundary, and the stored debt still holds inside every reservation. */
|
|
120
|
+
nightlyReads?: GhRead): NonNullable<ReleaseAdapters["release"]>;
|
|
116
121
|
/** What the plane itself reads for an automatic release approval. */
|
|
117
122
|
export type AutoObservers = {
|
|
118
123
|
/** The plane's executor policy, never a report field or project setting. Unknown refuses. Production currently
|
|
@@ -135,28 +140,23 @@ export declare function readCheckRuns(run: Exec, gh: string, repo: string, sha:
|
|
|
135
140
|
options?: {
|
|
136
141
|
tag?: string;
|
|
137
142
|
}): Promise<CheckRun[]>;
|
|
138
|
-
/** One job's step outcomes as GitHub's Actions API reports them. */
|
|
139
|
-
export type JobRecord = {
|
|
140
|
-
id: number;
|
|
141
|
-
status: string;
|
|
142
|
-
conclusion: string | null;
|
|
143
|
-
steps: {
|
|
144
|
-
number: number;
|
|
145
|
-
name: string;
|
|
146
|
-
status: string;
|
|
147
|
-
conclusion: string | null;
|
|
148
|
-
}[];
|
|
149
|
-
};
|
|
150
|
-
/**
|
|
151
|
-
* Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
|
|
152
|
-
* first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
|
|
153
|
-
* closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
|
|
154
|
-
* candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
|
|
155
|
-
* is not confirmed infrastructure.
|
|
156
|
-
*/
|
|
157
|
-
export declare function failedBeforeTests(job: JobRecord): boolean;
|
|
158
143
|
/** GitHub's job record for one check run (its job), read strictly: null when it can't be read whole. */
|
|
159
144
|
export declare function readJobBeforeTests(run: Exec, gh: string, repo: string, id: number): Promise<boolean | null>;
|
|
145
|
+
/**
|
|
146
|
+
* GitHub's REST API as GitHub's nightly reads it (nightly-github.ts): GET only, github.com only, a repository path, the
|
|
147
|
+
* plane's own gh with the allowlisted environment, each call bounded by its caller's deadline. A background read rides
|
|
148
|
+
* out a short network failure (release-net.ts, within that call's own share of the deadline); a boundary's read is
|
|
149
|
+
* plain exec. Never an answer that isn't GitHub's whole JSON.
|
|
150
|
+
*/
|
|
151
|
+
export declare function githubReads(run: Exec, gh: string, options?: {
|
|
152
|
+
retry?: boolean;
|
|
153
|
+
}): (path: string, timeoutMs: number) => Promise<{
|
|
154
|
+
ok: true;
|
|
155
|
+
body: unknown;
|
|
156
|
+
} | {
|
|
157
|
+
ok: false;
|
|
158
|
+
why: string;
|
|
159
|
+
}>;
|
|
160
160
|
/** GitHub lists check runs from at most the latest 1000 check suites of a commit: the history is whole only below that. */
|
|
161
161
|
export declare const CHECK_SUITE_CEILING = 1000;
|
|
162
162
|
export declare function readSuitesComplete(run: Exec, gh: string, repo: string, sha: string): Promise<boolean>;
|
package/dist/release-adapters.js
CHANGED
|
@@ -28,6 +28,9 @@ import { autoPublishHeld, holdAutoPublish, liftCiHold, authorizeAutoCheck, giveU
|
|
|
28
28
|
import { proveCarry } from "./release-carry.js";
|
|
29
29
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
30
30
|
import { deploymentGate } from "./deploy-gate.js";
|
|
31
|
+
import { failedBeforeTests } from "./github-jobs.js";
|
|
32
|
+
import { mainHistoryOf, nightlyBoundary } from "./nightly-github.js";
|
|
33
|
+
export { failedBeforeTests } from "./github-jobs.js";
|
|
31
34
|
import { extractCommit } from "./commit-extract.js";
|
|
32
35
|
import { teardownGit, refusal as gitRefusal } from "./teardown-git.js";
|
|
33
36
|
import { RELEASE_REMOTE, releaseConfigProblem, releaseGitArgs, releaseGitEnv, releaseRemote, releaseTools } from "./release-git.js";
|
|
@@ -422,7 +425,9 @@ export function releaseAdapters(options) {
|
|
|
422
425
|
upToDateRequired: () => readUpToDateRequired(reads, ciGh, releaseRepo().slug), evidenceRoot: join(dirname(database), "evidence"),
|
|
423
426
|
// A successful Actions job authenticates the job, not the candidate-writable JSON it uploads. Until the
|
|
424
427
|
// executor protects that reporting channel, production releases must take the person's approval road.
|
|
425
|
-
reportsProtected: false }
|
|
428
|
+
reportsProtected: false },
|
|
429
|
+
// A boundary's one bounded read of GitHub's nightly: plain exec, never the release's long retries (20 s in all).
|
|
430
|
+
githubReads(run, ciGh));
|
|
426
431
|
return {
|
|
427
432
|
...(existsSync(database) ? { release } : {}),
|
|
428
433
|
now: options.now ?? (() => new Date()),
|
|
@@ -925,7 +930,10 @@ function personAnswerOf(value) {
|
|
|
925
930
|
export function storeReleaseAdapter(open, done, now, runner,
|
|
926
931
|
/** Releases that approve themselves (auto-approve-green.md): the plane's own look at CI on a commit, and where the
|
|
927
932
|
* check receipts are kept. Absent: no automatic approval, every release asks a person. */
|
|
928
|
-
auto
|
|
933
|
+
auto,
|
|
934
|
+
/** GitHub's nightly (nightly-github.ts): the plane's own gh reads for a boundary's one bounded read. Absent: nothing
|
|
935
|
+
* is read at the boundary, and the stored debt still holds inside every reservation. */
|
|
936
|
+
nightlyReads) {
|
|
929
937
|
const withStore = (body) => { const store = open(); try {
|
|
930
938
|
return body(store);
|
|
931
939
|
}
|
|
@@ -962,6 +970,20 @@ auto) {
|
|
|
962
970
|
async live(taskId, action) {
|
|
963
971
|
return withStore(store => { const live = liveReleaseOf(store, taskId, action, now()); return live.applies ? { applies: true, ok: live.ok, why: live.ok ? null : live.why } : { applies: false, ok: false, why: null }; });
|
|
964
972
|
},
|
|
973
|
+
...(nightlyReads === undefined ? {} : {
|
|
974
|
+
async nightly(input) {
|
|
975
|
+
const store = open();
|
|
976
|
+
try {
|
|
977
|
+
const repo = store.lookupRef(input.taskId)?.repo ?? null;
|
|
978
|
+
if (repo === null)
|
|
979
|
+
return { ok: false, why: `Task ${input.taskId} is in no project, so GitHub's nightly debt can't be read for it.`, warning: null };
|
|
980
|
+
return await nightlyBoundary(store, { repo, taskId: input.taskId, candidate: input.candidate, pr: input.pr, phase: input.phase }, { ...mainHistoryOf(store, repo, now()), gh: nightlyReads, now });
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
done(store);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
}),
|
|
965
987
|
async lease(authority, owner) { return withStore(store => takeReleaseLease(store, authority, owner, now())); },
|
|
966
988
|
async renew(authority, owner, epoch) { return withStore(store => renewReleaseLease(store, authority, owner, epoch, now())); },
|
|
967
989
|
async carry(authority, to, requestId, epoch) {
|
|
@@ -1169,23 +1191,6 @@ options = {}) {
|
|
|
1169
1191
|
}
|
|
1170
1192
|
return runs.sort((a, b) => a.id - b.id);
|
|
1171
1193
|
}
|
|
1172
|
-
/**
|
|
1173
|
-
* Whether a finished, unsuccessful job ended before any of the workflow's own steps ran: every step after GitHub's own
|
|
1174
|
-
* first one ("Set up job", always number 1) never ran (no conclusion, or skipped) — leaving out only GitHub's own
|
|
1175
|
-
* closing "Complete job" when it is the last step. Step numbers and outcomes are the runner's records; nothing the
|
|
1176
|
-
* candidate prints is read. A job with no steps never reached one. Anything else (a step that ran, even the checkout)
|
|
1177
|
-
* is not confirmed infrastructure.
|
|
1178
|
-
*/
|
|
1179
|
-
export function failedBeforeTests(job) {
|
|
1180
|
-
if (job.status !== "completed" || job.conclusion === "success" || job.conclusion === null)
|
|
1181
|
-
return false;
|
|
1182
|
-
const steps = [...job.steps].sort((a, b) => a.number - b.number);
|
|
1183
|
-
if (new Set(steps.map(one => one.number)).size !== steps.length)
|
|
1184
|
-
return false;
|
|
1185
|
-
const last = steps.at(-1);
|
|
1186
|
-
const own = steps.filter(one => one.number > 1 && !(one === last && one.name === "Complete job" && steps.length > 1));
|
|
1187
|
-
return own.every(one => one.status !== "in_progress" && (one.conclusion === null || one.conclusion === "skipped"));
|
|
1188
|
-
}
|
|
1189
1194
|
/** GitHub's job record for one check run (its job), read strictly: null when it can't be read whole. */
|
|
1190
1195
|
export async function readJobBeforeTests(run, gh, repo, id) {
|
|
1191
1196
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !Number.isSafeInteger(id) || id <= 0)
|
|
@@ -1214,6 +1219,34 @@ export async function readJobBeforeTests(run, gh, repo, id) {
|
|
|
1214
1219
|
return failedBeforeTests({ id, status: String(job["status"]), conclusion: textOr(job["conclusion"]),
|
|
1215
1220
|
steps: steps.map(one => ({ number: Number(one["number"]), name: String(one["name"]), status: String(one["status"]), conclusion: textOr(one["conclusion"]) })) });
|
|
1216
1221
|
}
|
|
1222
|
+
/**
|
|
1223
|
+
* GitHub's REST API as GitHub's nightly reads it (nightly-github.ts): GET only, github.com only, a repository path, the
|
|
1224
|
+
* plane's own gh with the allowlisted environment, each call bounded by its caller's deadline. A background read rides
|
|
1225
|
+
* out a short network failure (release-net.ts, within that call's own share of the deadline); a boundary's read is
|
|
1226
|
+
* plain exec. Never an answer that isn't GitHub's whole JSON.
|
|
1227
|
+
*/
|
|
1228
|
+
export function githubReads(run, gh, options = {}) {
|
|
1229
|
+
return async (path, timeoutMs) => {
|
|
1230
|
+
if (!/^repos\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_./?&=%-]+$/.test(path) || timeoutMs <= 0)
|
|
1231
|
+
return { ok: false, why: "not a read this makes" };
|
|
1232
|
+
const exec = options.retry === true ? retryingExec(run, { say: () => { }, sleep, budgetMs: timeoutMs }) : run;
|
|
1233
|
+
let result;
|
|
1234
|
+
try {
|
|
1235
|
+
result = await exec(gh, ["api", "--hostname", "github.com", "--method", "GET", path], { cwd: "/", env: ciEnv(), timeoutMs });
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
return { ok: false, why: String(error?.message ?? error).slice(0, 200) };
|
|
1239
|
+
}
|
|
1240
|
+
if (result.code !== 0)
|
|
1241
|
+
return { ok: false, why: result.code === 124 ? "GitHub didn't answer in time" : result.code === 127 ? "gh isn't installed" : (result.stderr || result.stdout).trim().split("\n").slice(-1)[0].slice(0, 200) || `gh exited ${result.code}` };
|
|
1242
|
+
try {
|
|
1243
|
+
return { ok: true, body: JSON.parse(result.stdout) };
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return { ok: false, why: "GitHub's answer couldn't be read" };
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1217
1250
|
/** GitHub lists check runs from at most the latest 1000 check suites of a commit: the history is whole only below that. */
|
|
1218
1251
|
export const CHECK_SUITE_CEILING = 1000;
|
|
1219
1252
|
export async function readSuitesComplete(run, gh, repo, sha) {
|
|
@@ -1290,7 +1323,7 @@ export async function readMainProtection(run, gh, repo, branch = "main") {
|
|
|
1290
1323
|
else {
|
|
1291
1324
|
const lacking = PROTECTION_MINIMUM_CHECKS.filter(name => !checks.includes(name));
|
|
1292
1325
|
if (lacking.length > 0)
|
|
1293
|
-
own.push(`${where}: required checks lack ${lacking.join(", ")}`);
|
|
1326
|
+
own.push(`${where}: required checks lack ${lacking.join(", ")} (add ${lacking.length === 1 ? "it" : "them"} to main's required status checks: a release's proof trusts what main required)`);
|
|
1294
1327
|
const unbound = PROTECTION_MINIMUM_CHECKS.filter(name => checks.includes(name) && !bound.includes(name));
|
|
1295
1328
|
if (unbound.length > 0)
|
|
1296
1329
|
own.push(`${where}: required checks must require GitHub Actions (app 15368) as their source: ${unbound.join(", ")}`);
|
package/dist/release-auto.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type ObservedCheckRun = {
|
|
|
19
19
|
suite?: number | null;
|
|
20
20
|
head?: string | null;
|
|
21
21
|
workflow?: number | null;
|
|
22
|
-
/** The workflow's file (GitHub's workflow-run `path`): a required check counts only from
|
|
22
|
+
/** The workflow's file (GitHub's workflow-run `path`): a required check counts only from its own (REQUIRED_PR_CHECKS_WORKFLOW). */
|
|
23
23
|
workflowPath?: string | null;
|
|
24
24
|
/** GitHub's job record shows this unsuccessful attempt ended before any workflow step ran (release-adapters.ts). */
|
|
25
25
|
beforeTests?: boolean | null;
|