toolroll 0.9.7 → 0.9.9
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 +48 -1
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-brief.d.ts +2 -0
- package/dist/assignment-brief.js +2 -1
- package/dist/assignment-presentation.js +3 -1
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +5 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +56 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-crew.js +4 -0
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +2 -0
- package/dist/builder.js +114 -8
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/check-levels-ui.d.ts +5 -0
- package/dist/check-levels-ui.js +16 -2
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/cli.js +2 -0
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -3
- package/dist/dispose.js +12 -4
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +8 -6
- package/dist/invoke.js +5 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +4 -2
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +21 -24
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +231 -36
- package/dist/plane-review.js +4 -0
- package/dist/process-custody.js +8 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-learning.d.ts +3 -3
- package/dist/project-learning.js +28 -17
- package/dist/proof.d.ts +3 -1
- package/dist/proof.js +4 -2
- package/dist/provider.js +16 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/run-closeout.d.ts +89 -0
- package/dist/run-closeout.js +302 -0
- package/dist/serve.d.ts +15 -1
- package/dist/serve.js +158 -18
- package/dist/setup-guide.d.ts +2 -2
- package/dist/shared-deps.d.ts +94 -0
- package/dist/shared-deps.js +365 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +72 -1
- package/dist/store.js +258 -31
- package/dist/surface.js +5 -1
- package/dist/task-status.d.ts +22 -5
- package/dist/task-status.js +45 -24
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram-status.js +3 -3
- package/dist/test-temp.d.ts +19 -0
- package/dist/test-temp.js +98 -0
- package/dist/toolroll-update-ui.js +7 -2
- package/dist/toolroll-update.d.ts +21 -1
- package/dist/toolroll-update.js +135 -5
- package/dist/work-index.d.ts +11 -0
- package/dist/work-index.js +103 -16
- package/dist/work-summary.d.ts +3 -1
- package/dist/work-summary.js +2 -1
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -9
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { maybeTriggerRepair } from "./dispose.js";
|
|
2
2
|
import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
|
|
3
3
|
import { fileAddTestsTask, followUpChecksOf, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
|
|
4
|
+
import { buildReviewPass } from "./build-review.js";
|
|
5
|
+
import { buildReviewLines, buildReviewOf } from "./review-switch.js";
|
|
4
6
|
import { parseProtectedPaths } from "./approval-policy.js";
|
|
5
7
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
6
8
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
7
9
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
8
|
-
import { CLEANUP_CHOICES, bytesWords, cleanupWords, parseCleanup, storageReport } from "./storage.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { CLEANUP_CHOICES, bytesWords, cleanupWords, diskBytes, parseCleanup, storageReport } from "./storage.js";
|
|
11
|
+
import { removeStaleTestTemp, tempRoots, testTempFolders } from "./test-temp.js";
|
|
12
|
+
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
13
|
+
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
14
|
+
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
15
|
+
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
11
16
|
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
12
17
|
import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames, usd as spendUsd } from "./spend.js";
|
|
13
18
|
import { spendCsv } from "./spend-ui.js";
|
|
@@ -71,6 +76,7 @@ import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./
|
|
|
71
76
|
*/
|
|
72
77
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
73
78
|
import { basename, dirname, join, resolve } from "node:path";
|
|
79
|
+
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
74
80
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, } from "./store.js";
|
|
75
81
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
76
82
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -191,7 +197,7 @@ import { runModelsCommand } from "./models-cli.js";
|
|
|
191
197
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
192
198
|
import { beads } from "./beads.js";
|
|
193
199
|
import { githubIssues } from "./issues.js";
|
|
194
|
-
import { installationStatus, renderInstallationStatus, renderTaskWait, taskWaitSnapshot } from "./lead-status.js";
|
|
200
|
+
import { installationStatus, renderInstallationStatus, renderTaskWait, repairStaleStatuses, taskWaitSnapshot } from "./lead-status.js";
|
|
195
201
|
import { latestRelease, newerRelease, notifySecurityRelease, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
196
202
|
import { installMethod } from "./install-method.js";
|
|
197
203
|
import { PACKAGE_VERSION } from "./version.js";
|
|
@@ -225,8 +231,8 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
225
231
|
how many of a project's tasks build at once (default 2, never past the
|
|
226
232
|
worker's capacity); an approver changes it with <n>
|
|
227
233
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
228
|
-
toolroll storage where the disk goes: database, build checkouts, releases, evidence
|
|
229
|
-
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay); --yes removes them
|
|
234
|
+
toolroll storage where the disk goes: database, build checkouts, releases, evidence, leftover test temp folders
|
|
235
|
+
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay) and test temp folders older than a day; --yes removes them
|
|
230
236
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
231
237
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
232
238
|
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
@@ -239,9 +245,10 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
239
245
|
toolroll notifications digest <HH:MM>|off one evening message: what finished, what waits, what failed
|
|
240
246
|
toolroll notifications mute|unmute --repo <p> no pings for a project; the console and digest keep it
|
|
241
247
|
toolroll retention show|preview how long evidence, checkout records, chat and notifications are kept; what the daily sweep would remove
|
|
242
|
-
toolroll retention set <kind> <period> evidence|checkouts|chat|notifications, 30d|1y|forever (instance operator)
|
|
248
|
+
toolroll retention set <kind> <period> evidence|checkouts|chat|notifications, 1d|30d|1y|forever (instance operator)
|
|
243
249
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
244
250
|
toolroll restore <file> [--dry-run] put a backup back (Toolroll stopped; the current database is kept)
|
|
251
|
+
toolroll review show|on|off --repo <path> one automatic review of each finished build; only HIGH findings send it back
|
|
245
252
|
toolroll policy show|set the organisation policy; an instance operator sets it with --providers claude,codex|any,
|
|
246
253
|
--models <m,…>|any, --tools <t,…>|any, --ceiling safe|standard|escalated
|
|
247
254
|
toolroll ledger verify check the action ledger's hash chain (--checkpoint <n:hash> to compare a copied head)
|
|
@@ -804,6 +811,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
804
811
|
// it matters.
|
|
805
812
|
evidenceRoot: join(dirname(file), "evidence"),
|
|
806
813
|
databaseFile: file,
|
|
814
|
+
...(options.tempRoots === undefined ? {} : { tempRoots: options.tempRoots }),
|
|
807
815
|
// The bot token's file home. Never a column; see telegram.ts.
|
|
808
816
|
telegramTokenFile: join(dirname(file), "telegram-token"),
|
|
809
817
|
...(options.desktopIdentity === undefined ? {} : { desktopIdentity: options.desktopIdentity }),
|
|
@@ -932,6 +940,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
932
940
|
return storageCommand(positional, flags, context);
|
|
933
941
|
case "integrations":
|
|
934
942
|
return integrationsCommand(positional, flags, context);
|
|
943
|
+
case "review":
|
|
944
|
+
return reviewSwitchCommand(positional, flags, context);
|
|
935
945
|
case "monitoring": {
|
|
936
946
|
// Where the audit stream and traces go, and how each destination is doing. Settings → Monitoring changes it.
|
|
937
947
|
const settings = readMonitoring(dirname(context.databaseFile));
|
|
@@ -992,6 +1002,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
992
1002
|
return tickCommand(flags, context);
|
|
993
1003
|
case "reconcile":
|
|
994
1004
|
return reconcileCommand(flags, context);
|
|
1005
|
+
case "run":
|
|
1006
|
+
return runCommand(positional, flags, context);
|
|
995
1007
|
case "cap":
|
|
996
1008
|
return capCommand(positional, flags, context);
|
|
997
1009
|
case "gaps":
|
|
@@ -4277,19 +4289,20 @@ async function tickCommand(flags, context) {
|
|
|
4277
4289
|
broke++;
|
|
4278
4290
|
}
|
|
4279
4291
|
}
|
|
4280
|
-
//
|
|
4281
|
-
//
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
}
|
|
4291
|
-
}
|
|
4292
|
+
// The one automatic review each finished build gets when its project's
|
|
4293
|
+
// switch is on, before the result is handed on below. Other queued review
|
|
4294
|
+
// asks close unrun. Reauthenticated first; only admitted projects.
|
|
4295
|
+
if (!buildsOnly && authenticate(store, runner, token).ok) {
|
|
4296
|
+
const reviewed = await buildReviewPass(store, {
|
|
4297
|
+
runner, token, repos: auth.runner.repos, clock,
|
|
4298
|
+
root: context.evidenceRoot,
|
|
4299
|
+
...(text(flags, "incarnation") === undefined ? {} : { watchIncarnation: text(flags, "incarnation") }),
|
|
4300
|
+
...(context.agentRunner === undefined ? {} : { agent: context.agentRunner }),
|
|
4301
|
+
shouldStop: () => context.shouldStop?.() === true || context.shouldPauseAdmission?.() === true,
|
|
4292
4302
|
});
|
|
4303
|
+
for (const one of reviewed)
|
|
4304
|
+
dispatched.push({ id: `review of run ${one.run}`, outcome: one.outcome === "not-reviewed" ? "not-reviewed" : "reviewed", reason: one.detail });
|
|
4305
|
+
}
|
|
4293
4306
|
// Existing worker pass owns durable lead handoffs; reads never create events.
|
|
4294
4307
|
if (!buildsOnly)
|
|
4295
4308
|
syncAssignmentHandoffs(store, clock(), auth.runner.repos, context.evidenceRoot);
|
|
@@ -4327,7 +4340,7 @@ async function tickCommand(flags, context) {
|
|
|
4327
4340
|
routines,
|
|
4328
4341
|
});
|
|
4329
4342
|
}
|
|
4330
|
-
if (built > 0 || parked > 0 || dispatched.some(one => one.outcome === "planned" || one.outcome === "reported" || one.outcome === "held")) {
|
|
4343
|
+
if (built > 0 || parked > 0 || dispatched.some(one => one.outcome === "planned" || one.outcome === "reported" || one.outcome === "held" || one.outcome === "reviewed" || one.outcome === "not-reviewed")) {
|
|
4331
4344
|
return succeed(write, json, "tick", { considered, dispatched, routines, ...flows }, summary);
|
|
4332
4345
|
}
|
|
4333
4346
|
if (considered === 0) {
|
|
@@ -4389,7 +4402,21 @@ async function reconcileCommand(flags, context) {
|
|
|
4389
4402
|
}
|
|
4390
4403
|
}
|
|
4391
4404
|
const recovered = recoverDead(store, clock());
|
|
4405
|
+
// A run that ended, whatever its outcome, leaves nothing running: what is still in its process groups is stopped
|
|
4406
|
+
// (SIGTERM, then SIGKILL after 10 s) and the ledger names it. Never the live service.
|
|
4407
|
+
const closedRuns = await closeOutRuns(store, clock, { repo });
|
|
4408
|
+
// A finished run's processes that have since gone get their exits recorded,
|
|
4409
|
+
// so its task stops waiting on a person by itself.
|
|
4410
|
+
store.recordFinishedRunExits(clock());
|
|
4411
|
+
// A witness reserved for a spawn that never made a process (a crash or a
|
|
4412
|
+
// thrown spawn) settles once its run's process groups are proven gone.
|
|
4413
|
+
store.settleUnspawnedWitnesses(clock());
|
|
4392
4414
|
store.settleQuiescentStops(clock());
|
|
4415
|
+
// An update whose updater ended while letting work finish must not leave new work paused.
|
|
4416
|
+
try {
|
|
4417
|
+
releaseStalledUpdate(dirname(context.databaseFile), clock());
|
|
4418
|
+
}
|
|
4419
|
+
catch { /* status and the console say it */ }
|
|
4393
4420
|
for (const one of recovered) {
|
|
4394
4421
|
for (const leaseId of one.claims) {
|
|
4395
4422
|
// Lease ids are unique forever, so each recovery is its own episode.
|
|
@@ -4441,7 +4468,11 @@ async function reconcileCommand(flags, context) {
|
|
|
4441
4468
|
}
|
|
4442
4469
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
4443
4470
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
4471
|
+
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
4472
|
+
const closedCheckouts = await closeOutCheckouts(store, clock, finishedCheckouts(store, clock(), repo));
|
|
4444
4473
|
const pruned = await cleanCheckouts(store, worktrees, clock, { manual: false, actor: "worker", repo });
|
|
4474
|
+
// What stays for review or for its changes drops its dependencies and build output; the next run's setup restores them.
|
|
4475
|
+
const slimmed = await slimKeptCheckouts(store, worktrees, clock, { actor: "worker", repo });
|
|
4445
4476
|
// Retention (Settings → Retention): once a day, what's older than its setting goes; one ledger entry says what.
|
|
4446
4477
|
let retention = null;
|
|
4447
4478
|
try {
|
|
@@ -4457,6 +4488,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4457
4488
|
adoption.adopted.length === 0 &&
|
|
4458
4489
|
adoption.forgotten.length === 0 &&
|
|
4459
4490
|
pruned.removed.length === 0 &&
|
|
4491
|
+
closedRuns.length === 0 &&
|
|
4492
|
+
closedCheckouts.length === 0 &&
|
|
4493
|
+
slimmed.slimmed.length === 0 &&
|
|
4460
4494
|
liveSwept.removed.length === 0;
|
|
4461
4495
|
return succeed(write, json, "reconcile", {
|
|
4462
4496
|
recovered,
|
|
@@ -4465,6 +4499,8 @@ async function reconcileCommand(flags, context) {
|
|
|
4465
4499
|
forgotten: adoption.forgotten,
|
|
4466
4500
|
checkoutsRemoved: pruned.removed.map(one => one.path),
|
|
4467
4501
|
checkoutsKept: pruned.kept,
|
|
4502
|
+
processesStopped: [...closedRuns.map(one => ({ run: one.runId, pids: one.done.stopped.map(row => row.pid) })), ...closedCheckouts.map(one => ({ checkout: one.path, pids: one.done.stopped.map(row => row.pid) }))],
|
|
4503
|
+
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
4468
4504
|
liveViewsSwept: liveSwept.removed.length,
|
|
4469
4505
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
4470
4506
|
}, () => nothing
|
|
@@ -4475,6 +4511,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4475
4511
|
...adoption.adopted.map(path => `Adopted ${path} — released, unverified, somebody should look.`),
|
|
4476
4512
|
...adoption.forgotten.map(path => `Forgot ${path} — its directory is gone.`),
|
|
4477
4513
|
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} finished task checkout(s), about ${bytesWords(pruned.freed)}; their branches stay.`]),
|
|
4514
|
+
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
4515
|
+
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
4516
|
+
...(slimmed.slimmed.length === 0 ? [] : [`Dropped dependencies and build output from ${slimmed.slimmed.length} kept checkout(s), about ${bytesWords(slimmed.slimmed.reduce((sum, one) => sum + one.bytes, 0))}.`]),
|
|
4478
4517
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
4479
4518
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
4480
4519
|
]);
|
|
@@ -5045,13 +5084,18 @@ async function planTaskCommand(positional, flags, context) {
|
|
|
5045
5084
|
}
|
|
5046
5085
|
/** The retired command is a typed refusal, never a new provider request. */
|
|
5047
5086
|
async function reviewTaskCommand(positional, flags, context) {
|
|
5048
|
-
const { store, write, json
|
|
5087
|
+
const { store, write, json } = context;
|
|
5049
5088
|
const [runText] = positional;
|
|
5050
5089
|
const runId = Number(runText ?? "");
|
|
5051
5090
|
if (runText === undefined || !Number.isInteger(runId) || runId < 1) {
|
|
5052
5091
|
return fail(write, json, "task review", "usage", "`toolroll task review <run-id>`", EXIT.usage);
|
|
5053
5092
|
}
|
|
5054
|
-
|
|
5093
|
+
// Read-only: the run's one automatic review, when it had one.
|
|
5094
|
+
const review = buildReviewOf(store, runId);
|
|
5095
|
+
if (review !== null) {
|
|
5096
|
+
return succeed(write, json, "task review", { run: runId, review }, () => [`Run ${runId}`, ...buildReviewLines(review)]);
|
|
5097
|
+
}
|
|
5098
|
+
return fail(write, json, "task review", "model-review-retired", `Run ${runId} has no automatic review. Manual review requests have been removed; a project's builds are reviewed automatically when its switch is on (\`toolroll review on --repo <path>\`). Inspect the saved work and checks, then give feedback or request a revision.`, EXIT.refused);
|
|
5055
5099
|
}
|
|
5056
5100
|
/**
|
|
5057
5101
|
* `toolroll incident list|resolve <id>` — the parks that never became
|
|
@@ -7243,6 +7287,8 @@ async function daemonCommand(positional, flags, context) {
|
|
|
7243
7287
|
// ---- the watch loop --------------------------------------------------------
|
|
7244
7288
|
const WATCH_LEASE_MS = 90_000;
|
|
7245
7289
|
const WATCH_HEARTBEAT_MS = 30_000;
|
|
7290
|
+
/** The start repair runs once per process, not once per project loop. */
|
|
7291
|
+
let staleStatusesRepaired = false;
|
|
7246
7292
|
async function runWatchLoop(args) {
|
|
7247
7293
|
const { context, flags, runner, token, repo, progress } = args;
|
|
7248
7294
|
const { store } = context;
|
|
@@ -7280,6 +7326,14 @@ async function runWatchLoop(args) {
|
|
|
7280
7326
|
store.startWatchEpisode({ repo, runner, incarnation }, new Date());
|
|
7281
7327
|
recordRunnerVersion(dirname(context.databaseFile), runner, PACKAGE_VERSION, new Date());
|
|
7282
7328
|
args.onReady?.();
|
|
7329
|
+
// Once per worker start, before any pass: statuses an older build left
|
|
7330
|
+
// stale (unrecorded exits of finished runs) are re-derived and clear.
|
|
7331
|
+
if (!staleStatusesRepaired) {
|
|
7332
|
+
staleStatusesRepaired = true;
|
|
7333
|
+
const repaired = repairStaleStatuses(store, new Date());
|
|
7334
|
+
if (repaired.exitsRecorded > 0)
|
|
7335
|
+
progress(`watch: recorded ${repaired.exitsRecorded} finished process exit(s) left open by an earlier version`);
|
|
7336
|
+
}
|
|
7283
7337
|
// A false renewal is FATAL (arc 2 finding 15): the lease or the
|
|
7284
7338
|
// credential is gone, and admitting one more pass would be work done for
|
|
7285
7339
|
// an authority this process no longer holds.
|
|
@@ -7776,7 +7830,8 @@ async function watchCommand(flags, context) {
|
|
|
7776
7830
|
token,
|
|
7777
7831
|
repo,
|
|
7778
7832
|
progress,
|
|
7779
|
-
|
|
7833
|
+
// An injected fence (tests) stops admission exactly as a signal does.
|
|
7834
|
+
isStopping: () => stopping || context.shouldStop?.() === true,
|
|
7780
7835
|
onFollowController: controller => {
|
|
7781
7836
|
followAbort = controller;
|
|
7782
7837
|
},
|
|
@@ -8820,6 +8875,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8820
8875
|
const runFor = text(flags, "for");
|
|
8821
8876
|
const timer = runFor === undefined ? null : setTimeout(() => controller.abort(), Number(runFor));
|
|
8822
8877
|
timer?.unref?.();
|
|
8878
|
+
// An injected fence (tests) ends the follow exactly as a signal does.
|
|
8879
|
+
const fence = context.shouldStop === undefined ? null : setInterval(() => { if (context.shouldStop?.() === true)
|
|
8880
|
+
controller.abort(); }, 50);
|
|
8881
|
+
fence?.unref?.();
|
|
8823
8882
|
if (!json)
|
|
8824
8883
|
write(`Following bot ${source.botId} — taps apply as they arrive. Ctrl-C stops it.`);
|
|
8825
8884
|
try {
|
|
@@ -8849,6 +8908,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8849
8908
|
finally {
|
|
8850
8909
|
if (timer !== null)
|
|
8851
8910
|
clearTimeout(timer);
|
|
8911
|
+
if (fence !== null)
|
|
8912
|
+
clearInterval(fence);
|
|
8852
8913
|
process.removeListener("SIGINT", stop);
|
|
8853
8914
|
process.removeListener("SIGTERM", stop);
|
|
8854
8915
|
}
|
|
@@ -9512,10 +9573,13 @@ async function statusCommand(positional, flags, context) {
|
|
|
9512
9573
|
const brokenIntegrations = await savedIntegrations(context).then(list => list.filter(one => one.state === "broken"), () => []);
|
|
9513
9574
|
const brokenLine = integrationsBrokenLine(brokenIntegrations);
|
|
9514
9575
|
const integrations = brokenIntegrations.length === 0 ? {} : { integrations: { broken: brokenIntegrations.map(one => ({ key: one.key, name: one.name, fix: one.action.kind === "fix" ? one.action.words : null })) } };
|
|
9576
|
+
// An update waiting, on what, and the action: before anything else, as it holds up new work.
|
|
9577
|
+
const waiting = waitingUpdate(context.databaseFile, run => context.store.stopQuiescenceProblem(run) !== null, context.clock());
|
|
9578
|
+
const updateWaiting = waiting === null ? {} : { updateWaiting: { app: waiting.app, version: waiting.version, stopped: waiting.stopped, run: waiting.run, on: waiting.on, action: waiting.action } };
|
|
9515
9579
|
const projects = projectBuildsStatus(context);
|
|
9516
9580
|
const projectsLine = projects.length === 0 ? null
|
|
9517
9581
|
: `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
|
|
9518
|
-
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations }, () => [...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9582
|
+
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations, ...updateWaiting }, () => [...(waiting === null ? [] : [waiting.words]), ...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9519
9583
|
}
|
|
9520
9584
|
/** Each known project's running builds against its limit: its own number, never past its workers' capacity. */
|
|
9521
9585
|
function projectBuildsStatus(context) {
|
|
@@ -10147,6 +10211,9 @@ function showTask(positional, context) {
|
|
|
10147
10211
|
// v50: the latest build's bounded review history — every root attempt
|
|
10148
10212
|
// in order, the open request, and the one state they add up to.
|
|
10149
10213
|
review: latestFinished === null ? null : store.reviewRetryStateOf(latestFinished.id),
|
|
10214
|
+
// The one automatic review of the latest build: HIGH findings, suggested
|
|
10215
|
+
// follow-ups (MEDIUM/LOW), or why it was not reviewed.
|
|
10216
|
+
automaticReview: latestFinished === null ? null : buildReviewOf(store, latestFinished.id),
|
|
10150
10217
|
dispatch: diagnoseTaskDispatch(store, id, now),
|
|
10151
10218
|
// v52: the exact-run control the console shows — Stop, Stopping,
|
|
10152
10219
|
// Paused (resume), or the review-retry door — and every stop on record.
|
|
@@ -10199,7 +10266,7 @@ function showTask(positional, context) {
|
|
|
10199
10266
|
: detail.control.kind === "paused"
|
|
10200
10267
|
? [` control: paused — run #${detail.control.run} was stopped by ${detail.control.stop.requestedBy} (${detail.control.stop.settlement ?? "?"})${detail.control.committed ? "; its commit is on the branch" : ""}; work preserved${detail.control.worktree === null ? "" : ` in ${detail.control.worktree}`} — \`task resume ${task.id} --run ${detail.control.run} --as <you> --token <t>\``]
|
|
10201
10268
|
: [` control: review #${detail.control.run} was stopped by ${detail.control.stop.requestedBy} — inspect its saved result and give feedback; separate model review has been removed`]),
|
|
10202
|
-
...reviewStatusLines(detail.review, latestFinished?.id ?? null),
|
|
10269
|
+
...(detail.automaticReview === null ? reviewStatusLines(detail.review, latestFinished?.id ?? null) : buildReviewLines(detail.automaticReview)),
|
|
10203
10270
|
...(scope === null
|
|
10204
10271
|
? [" no scope — nothing will build this until one is written and approved"]
|
|
10205
10272
|
: describeScope(scope, readiness)),
|
|
@@ -11662,6 +11729,48 @@ async function resumeTaskCommand(positional, flags, context) {
|
|
|
11662
11729
|
: ` Not starting yet — ${resumed.gate.summary}: ${resumed.gate.detail}`,
|
|
11663
11730
|
]);
|
|
11664
11731
|
}
|
|
11732
|
+
/**
|
|
11733
|
+
* `toolroll run settle <id> --why "<text>" --as <you> --token <t>`: an
|
|
11734
|
+
* approver's last resort for a finished run whose process witnesses can be
|
|
11735
|
+
* proven neither exited nor alive (a spawn interrupted before its pid was
|
|
11736
|
+
* written). Refuses while anything of the run is alive; the reason is kept
|
|
11737
|
+
* in the ledger against the approver's name. Signals nothing.
|
|
11738
|
+
*/
|
|
11739
|
+
async function runCommand(positional, flags, context) {
|
|
11740
|
+
const { store, write, json, clock } = context;
|
|
11741
|
+
const [action, id, ...extra] = positional;
|
|
11742
|
+
const command = "run settle";
|
|
11743
|
+
const usage = "`toolroll run settle <run-id> --why \"<reason>\" --as <you> --token <t>`";
|
|
11744
|
+
if (action !== "settle")
|
|
11745
|
+
return fail(write, json, "run", "usage", usage, EXIT.usage);
|
|
11746
|
+
const runId = Number(id ?? "");
|
|
11747
|
+
const why = text(flags, "why")?.trim();
|
|
11748
|
+
if (id === undefined || extra.length > 0 || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(runId))
|
|
11749
|
+
return fail(write, json, command, "usage", usage, EXIT.usage);
|
|
11750
|
+
if (why === undefined || why === "" || why.length > 300 || /[\x00-\x1f\x7f]/.test(why)) {
|
|
11751
|
+
return fail(write, json, command, "usage", "say why in one line with --why (at most 300 characters) — it is kept in the ledger", EXIT.usage);
|
|
11752
|
+
}
|
|
11753
|
+
const run = store.getRun(runId);
|
|
11754
|
+
if (run === null)
|
|
11755
|
+
return fail(write, json, command, "unknown-run", `no run #${runId}`, EXIT.refused);
|
|
11756
|
+
const acting = await askCredentials(flags, context);
|
|
11757
|
+
if (acting === null) {
|
|
11758
|
+
return fail(write, json, command, "usage", "settling takes `--as <you> --token <t>` — it is an approver's act and is recorded against your name", EXIT.usage);
|
|
11759
|
+
}
|
|
11760
|
+
const authenticated = authenticateApprover(store, acting.name, acting.token);
|
|
11761
|
+
const repo = store.refById(run.taskRef)?.repo ?? null;
|
|
11762
|
+
if (!authenticated.ok || !store.accountCanAccess(acting.name, repo)) {
|
|
11763
|
+
return fail(write, json, command, "not-an-approver", "that is not an approver for this run's project, or the token does not match", EXIT.refused);
|
|
11764
|
+
}
|
|
11765
|
+
const settled = store.settleRunWitnessesByApprover({ runId, by: acting.name, why }, clock());
|
|
11766
|
+
if (!settled.ok)
|
|
11767
|
+
return fail(write, json, command, settled.reason, settled.detail, EXIT.refused, { run: runId });
|
|
11768
|
+
return succeed(write, json, command, { run: runId, witnesses: settled.witnesses, repeated: settled.repeated }, () => [
|
|
11769
|
+
settled.repeated
|
|
11770
|
+
? `Run #${runId} has no unsettled process witness; nothing changed.`
|
|
11771
|
+
: `Settled run #${runId}: ${settled.witnesses.length} process witness${settled.witnesses.length === 1 ? "" : "es"} recorded as ended, with your reason in the ledger.`,
|
|
11772
|
+
]);
|
|
11773
|
+
}
|
|
11665
11774
|
// ---- shared ---------------------------------------------------------------
|
|
11666
11775
|
/**
|
|
11667
11776
|
* `project rules --repo <path>` shows a project's approval rules (v102);
|
|
@@ -12026,9 +12135,11 @@ async function retentionCommand(positional, flags, context) {
|
|
|
12026
12135
|
const label = (kind) => RETENTION_KINDS.find(one => one.kind === kind).label;
|
|
12027
12136
|
if (action === undefined || action === "show") {
|
|
12028
12137
|
const periods = store.retentionPeriods();
|
|
12138
|
+
const chosen = store.retentionChosen();
|
|
12139
|
+
const defaulted = RETENTION_KINDS.map(one => one.kind).filter(kind => !(kind in chosen));
|
|
12029
12140
|
const last = lastSweepAt(store);
|
|
12030
|
-
return succeed(context.write, context.json, command, { periods, lastSweep: last }, () => [
|
|
12031
|
-
...RETENTION_KINDS.map(one => ` ${one.kind.padEnd(14)} ${
|
|
12141
|
+
return succeed(context.write, context.json, command, { periods, defaulted, lastSweep: last }, () => [
|
|
12142
|
+
...RETENTION_KINDS.map(one => ` ${one.kind.padEnd(14)} ${periodLabel(one.kind, periods[one.kind], one.kind in chosen).toLowerCase().padEnd(18)} ${one.label}`),
|
|
12032
12143
|
last === null ? "No sweep yet; the worker runs one a day." : `Last sweep ${last.slice(0, 16).replace("T", " ")} UTC.`,
|
|
12033
12144
|
]);
|
|
12034
12145
|
}
|
|
@@ -12044,11 +12155,11 @@ async function retentionCommand(positional, flags, context) {
|
|
|
12044
12155
|
return fail(context.write, context.json, command, "usage", "Use retention show, retention preview or retention set <kind> <period>.", EXIT.usage);
|
|
12045
12156
|
const [kind, period, ...extra] = rest;
|
|
12046
12157
|
if (kind === undefined || !isRetentionKind(kind) || period === undefined || extra.length > 0) {
|
|
12047
|
-
return fail(context.write, context.json, command, "usage", `Use retention set <${RETENTION_KINDS.map(one => one.kind).join("|")}> <days, like 90d or 1y, or forever>.`, EXIT.usage);
|
|
12158
|
+
return fail(context.write, context.json, command, "usage", `Use retention set <${RETENTION_KINDS.map(one => one.kind).join("|")}> <days, like 1d, 90d or 1y, or forever>.`, EXIT.usage);
|
|
12048
12159
|
}
|
|
12049
12160
|
const days = parsePeriod(period);
|
|
12050
12161
|
if (days === undefined)
|
|
12051
|
-
return fail(context.write, context.json, command, "usage", `A period is forever, or ${MIN_DAYS}
|
|
12162
|
+
return fail(context.write, context.json, command, "usage", `A period is forever, or ${MIN_DAYS} day to 10 years (1d, 90d, 12w, 1y).`, EXIT.usage);
|
|
12052
12163
|
const acting = await askCredentials(flags, context);
|
|
12053
12164
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
12054
12165
|
if (acting === null || verified === null || !verified.ok || !store.isInstanceOperator(acting.name)) {
|
|
@@ -12082,13 +12193,35 @@ async function storageCommand(positional, flags, context) {
|
|
|
12082
12193
|
`A finished task's clean checkout is removed ${cleanupWords(plan.cleanup)}; its branch stays.`,
|
|
12083
12194
|
];
|
|
12084
12195
|
const itemLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${one.path}${one.why === null ? "" : ` (${whyWords(one)})`}`;
|
|
12196
|
+
// Test temp folders the tests, journeys and scripts left (a killed run, an older version): those nothing touched for a day go.
|
|
12197
|
+
const roots = context.tempRoots ?? tempRoots();
|
|
12198
|
+
// Counting is quick; sizing thousands of folders isn't, so only a clean-up measures them.
|
|
12199
|
+
const leftovers = (measure) => {
|
|
12200
|
+
const found = testTempFolders(roots, context.clock());
|
|
12201
|
+
const sizes = measure ? diskBytes(found.stale.map(one => one.path)) : new Map();
|
|
12202
|
+
return { count: found.all.length, stale: found.stale.length, staleBytes: [...sizes.values()].reduce((sum, one) => sum + one, 0) };
|
|
12203
|
+
};
|
|
12204
|
+
const leftoverLine = (temp) => temp.count === 0 ? "Test temp folders: none." :
|
|
12205
|
+
`Test temp folders: ${temp.count}; ${temp.stale === 0 ? "none older than a day." : `${temp.stale} older than a day: toolroll storage clean removes them.`}`;
|
|
12206
|
+
// Shared dependencies (shared-deps.ts): one install per lockfile, linked into checkouts.
|
|
12207
|
+
const depsRoot = sharedDepsRoot(context.databaseFile);
|
|
12208
|
+
const checkoutRows = () => store.listWorktrees().map(row => ({ path: row.path, repo: row.repo }));
|
|
12209
|
+
const sharedFacts = (copies) => {
|
|
12210
|
+
const sizes = diskBytes(copies.map(one => one.dir));
|
|
12211
|
+
return copies.map(one => ({ key: one.key, repo: one.repo, path: one.dir, bytes: sizes.get(one.dir) ?? 0, checkouts: one.checkouts.length, usedBy: one.checkouts, retired: one.retired, node: one.node, createdAt: one.createdAt, usedAt: one.usedAt }));
|
|
12212
|
+
};
|
|
12213
|
+
const sharedLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${projectName(one.repo)} ${one.key.slice(0, 8)}: ${one.checkouts === 1 ? "1 checkout uses it" : `${one.checkouts} checkouts use it`}${one.retired ? " (changed after install; never linked again)" : ""}`;
|
|
12085
12214
|
if (action === undefined) {
|
|
12086
12215
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12087
12216
|
const report = storageReport(store, context.databaseFile, plan.go.length === 0 ? "nothing to clean up" : `about ${bytesWords(plan.freeBytes)} to clean up`);
|
|
12088
|
-
|
|
12217
|
+
const { staleBytes: _, ...temp } = leftovers(false);
|
|
12218
|
+
const shared = sharedFacts(sharedUse(depsRoot, checkoutRows()));
|
|
12219
|
+
return succeed(context.write, context.json, "storage", { ...report, checkouts: checkoutSummary(plan), testTemp: temp, shared }, () => [
|
|
12089
12220
|
`${report.folder}: ${bytesWords(report.total)}`,
|
|
12090
|
-
...report.lines.map(one => ` ${one.what.padEnd(
|
|
12221
|
+
...report.lines.map(one => ` ${one.what.padEnd(19)} ${bytesWords(one.bytes).padStart(8)}${one.count === undefined ? "" : ` (${one.count})`}${one.note === undefined ? "" : ` ${one.note}`}`),
|
|
12222
|
+
...(shared.length === 0 ? [] : ["Shared dependencies:", ...shared.map(sharedLine)]),
|
|
12091
12223
|
...summary(plan),
|
|
12224
|
+
leftoverLine({ ...temp, staleBytes: 0 }),
|
|
12092
12225
|
]);
|
|
12093
12226
|
}
|
|
12094
12227
|
if (action === "cleanup") {
|
|
@@ -12110,20 +12243,48 @@ async function storageCommand(positional, flags, context) {
|
|
|
12110
12243
|
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
12111
12244
|
if (!flags.has("yes")) {
|
|
12112
12245
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12113
|
-
|
|
12114
|
-
|
|
12246
|
+
const temp = leftovers(true);
|
|
12247
|
+
// A shared copy goes when no checkout uses it, counting the checkouts this clean-up would remove.
|
|
12248
|
+
const leaving = new Set(plan.go.map(one => one.path));
|
|
12249
|
+
const unused = unusedShared(depsRoot, checkoutRows().filter(row => !leaving.has(row.path)), context.clock());
|
|
12250
|
+
const sharedGo = sharedFacts(unused.copies);
|
|
12251
|
+
const sharedBytes = sharedGo.reduce((sum, one) => sum + one.bytes, 0);
|
|
12252
|
+
const anything = plan.go.length > 0 || temp.stale > 0 || sharedGo.length > 0;
|
|
12253
|
+
return succeed(context.write, context.json, command, { preview: true, go: plan.go, stay: plan.stay, freeBytes: plan.freeBytes, testTemp: temp, sharedGo, sharedFreeBytes: sharedBytes }, () => [
|
|
12254
|
+
...(plan.go.length === 0 ? [anything ? "No checkouts to clean up." : "Nothing to clean up."] : [`Would remove ${plan.go.length} checkout${plan.go.length === 1 ? "" : "s"}, freeing about ${bytesWords(plan.freeBytes)} (their branches stay):`, ...plan.go.map(itemLine)]),
|
|
12255
|
+
...(sharedGo.length === 0 ? [] : [`Would remove ${sharedGo.length} shared dependency install${sharedGo.length === 1 ? "" : "s"} no checkout uses, about ${bytesWords(sharedBytes)}:`, ...sharedGo.map(sharedLine)]),
|
|
12115
12256
|
...(plan.stay.length === 0 ? [] : [`Stays (${plan.stay.length}):`, ...plan.stay.map(itemLine)]),
|
|
12116
|
-
|
|
12257
|
+
...(temp.stale === 0 ? [] : [`Would remove ${temp.stale} test temp folder${temp.stale === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12258
|
+
anything ? "Nothing was removed. Run toolroll storage clean --yes to remove them." : "Nothing was removed.",
|
|
12117
12259
|
]);
|
|
12118
12260
|
}
|
|
12119
12261
|
const actor = await operator();
|
|
12120
12262
|
if (actor === null)
|
|
12121
12263
|
return refused("cleans up checkouts");
|
|
12122
12264
|
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
12123
|
-
|
|
12124
|
-
|
|
12265
|
+
const temp = leftovers(true);
|
|
12266
|
+
const swept = removeStaleTestTemp(roots, context.clock());
|
|
12267
|
+
if (swept.removed.length > 0) {
|
|
12268
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: null, taskId: null, runId: null, action: "test temp folders removed", outcome: "removed", source: "request",
|
|
12269
|
+
detail: `${swept.removed.length} older than a day, about ${bytesWords(temp.staleBytes)}${swept.failed.length === 0 ? "" : `; ${swept.failed.length} couldn't be removed`}` });
|
|
12270
|
+
}
|
|
12271
|
+
// After the checkouts go: a shared copy none of the remaining ones uses goes too.
|
|
12272
|
+
const unused = unusedShared(depsRoot, checkoutRows(), context.clock());
|
|
12273
|
+
const sharedRemoved = sharedFacts(unused.copies).filter(one => removeShared(one.path));
|
|
12274
|
+
for (const one of sharedRemoved) {
|
|
12275
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: one.repo, taskId: null, runId: null, action: "shared dependencies removed", outcome: "removed", source: "request",
|
|
12276
|
+
detail: `${one.key} (about ${bytesWords(one.bytes)}); no checkout used it` });
|
|
12277
|
+
}
|
|
12278
|
+
for (const debris of unused.debris)
|
|
12279
|
+
removeShared(debris);
|
|
12280
|
+
const sharedFreed = sharedRemoved.reduce((sum, one) => sum + one.bytes, 0);
|
|
12281
|
+
return succeed(context.write, context.json, command, { removed: done.removed, kept: done.kept, freedBytes: done.freed, testTempRemoved: swept.removed.length, testTempFailed: swept.failed, sharedRemoved, sharedFreedBytes: sharedFreed }, () => [
|
|
12282
|
+
done.removed.length === 0 ? (swept.removed.length === 0 && sharedRemoved.length === 0 ? "Nothing to clean up." : "No checkouts to clean up.") : `Removed ${done.removed.length} checkout${done.removed.length === 1 ? "" : "s"}, about ${bytesWords(done.freed)}; their branches stay.`,
|
|
12125
12283
|
...done.removed.map(itemLine),
|
|
12284
|
+
...(sharedRemoved.length === 0 ? [] : [`Removed ${sharedRemoved.length} shared dependency install${sharedRemoved.length === 1 ? "" : "s"} no checkout used, about ${bytesWords(sharedFreed)}.`, ...sharedRemoved.map(sharedLine)]),
|
|
12126
12285
|
...done.kept.map(one => ` kept ${one.path} (${whyWords({ why: one.why })})`),
|
|
12286
|
+
...(swept.removed.length === 0 ? [] : [`Removed ${swept.removed.length} test temp folder${swept.removed.length === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12287
|
+
...(swept.failed.length === 0 ? [] : [`Couldn't remove ${swept.failed.length} test temp folder${swept.failed.length === 1 ? "" : "s"}: ${swept.failed.slice(0, 3).join(", ")}${swept.failed.length > 3 ? " …" : ""}`]),
|
|
12127
12288
|
]);
|
|
12128
12289
|
}
|
|
12129
12290
|
if (action === "discard") {
|
|
@@ -12148,6 +12309,40 @@ async function storageCommand(positional, flags, context) {
|
|
|
12148
12309
|
function checkoutSummary(plan) {
|
|
12149
12310
|
return { count: plan.count, totalBytes: plan.totalBytes, waitingReview: plan.waitingReview, withChanges: plan.withChanges, cleanBytes: plan.freeBytes, cleanCount: plan.go.length, cleanup: plan.cleanup };
|
|
12150
12311
|
}
|
|
12312
|
+
/** `review [show] | on | off --repo <path>`: the project's one automatic review
|
|
12313
|
+
* of finished builds. Changing it takes an approver of that project; the
|
|
12314
|
+
* policy log keeps before → after. */
|
|
12315
|
+
async function reviewSwitchCommand(positional, flags, context) {
|
|
12316
|
+
const { store, write, json } = context;
|
|
12317
|
+
const [action = "show", ...extra] = positional;
|
|
12318
|
+
const command = `review ${action}`;
|
|
12319
|
+
if (extra.length > 0 || !["show", "on", "off"].includes(action))
|
|
12320
|
+
return fail(write, json, command, "usage", "Use toolroll review show|on|off --repo <path>.", EXIT.usage);
|
|
12321
|
+
const allowed = new Set(["repo", "db", "json", ...(action === "show" ? [] : ["as", "token", "token-file", "token-env"])]);
|
|
12322
|
+
for (const name of flags.keys())
|
|
12323
|
+
if (!allowed.has(name))
|
|
12324
|
+
return fail(write, json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
12325
|
+
const given = text(flags, "repo");
|
|
12326
|
+
if (given === undefined)
|
|
12327
|
+
return fail(write, json, command, "usage", "Name the project: toolroll review " + action + " --repo <path>.", EXIT.usage);
|
|
12328
|
+
const repo = canonicalProject(given) ?? resolve(given);
|
|
12329
|
+
const words = (on, source) => `${projectName(repo)}: automatic review is ${on ? "on" : "off"}${source === "hands-off" ? " (on while the hands-off mode lasts)" : source === "default" ? " (the default)" : ""}.`;
|
|
12330
|
+
if (action === "show") {
|
|
12331
|
+
const state = store.reviewSwitch(repo, context.clock());
|
|
12332
|
+
return succeed(write, json, command, { repo, ...state }, () => [words(state.on, state.source)]);
|
|
12333
|
+
}
|
|
12334
|
+
const acting = await askCredentials(flags, context);
|
|
12335
|
+
if (acting === null)
|
|
12336
|
+
return fail(write, json, command, "usage", `Sign in first, or pass --as <you> --token <t>: toolroll review ${action} --repo <path>.`, EXIT.usage);
|
|
12337
|
+
const verified = authenticateApprover(store, acting.name, acting.token, repo);
|
|
12338
|
+
if (!verified.ok)
|
|
12339
|
+
return fail(write, json, command, verified.reason, `Only an approver of ${projectName(repo)} can change its review switch.`, EXIT.refused);
|
|
12340
|
+
const changed = store.setReviewSwitch(repo, action === "on", acting.name, context.clock());
|
|
12341
|
+
return succeed(write, json, command, { repo, ...changed.after, before: changed.before }, () => [
|
|
12342
|
+
words(changed.after.on, changed.after.source),
|
|
12343
|
+
changed.after.on ? "Each finished build whose check passes gets one read-only review; only HIGH findings send it back, once." : "Finished builds go straight to you.",
|
|
12344
|
+
]);
|
|
12345
|
+
}
|
|
12151
12346
|
/** `policy show | set` (sprint 8): the organisation policy. Setting it takes an instance operator's credentials; each rule
|
|
12152
12347
|
* that changed is in the ledger, before → after. A flag left out keeps its rule; "any" lifts it. */
|
|
12153
12348
|
async function policyCommand(positional, flags, context) {
|
package/dist/plane-review.js
CHANGED
|
@@ -148,6 +148,10 @@ export function reviewPlane(store, now, canSee = () => true) {
|
|
|
148
148
|
for (const row of tasks) {
|
|
149
149
|
if (!canSee(str(row["repo"])))
|
|
150
150
|
continue;
|
|
151
|
+
// A version a later revision replaced never waits on its own.
|
|
152
|
+
const family = store.taskFamilyOf(String(row["id"]), null, true);
|
|
153
|
+
if (family !== null && family.current.id !== String(row["id"]))
|
|
154
|
+
continue;
|
|
151
155
|
const snapshot = taskWaitSnapshot(store, String(row["id"]), now);
|
|
152
156
|
if (snapshot === null || (snapshot.reason !== "needs-person" && snapshot.reason !== "ready"))
|
|
153
157
|
continue;
|
package/dist/process-custody.js
CHANGED
|
@@ -69,6 +69,14 @@ export function witnessedRunner(store, runId, clock, runner) {
|
|
|
69
69
|
store.recordRunProcess(runId, pid, clock(), options.processGroup === true, witnesses.at(-1));
|
|
70
70
|
options.onSpawn?.(pid);
|
|
71
71
|
},
|
|
72
|
+
// The spawn made no process (ENOENT, EAGAIN, a throw): its reservation
|
|
73
|
+
// settles as never started now, not only if the transport returns.
|
|
74
|
+
onSpawnFailed: () => {
|
|
75
|
+
const witness = witnesses.at(-1);
|
|
76
|
+
if (witness !== undefined)
|
|
77
|
+
store.finishUnspawnedProcess(witness, clock());
|
|
78
|
+
options.onSpawnFailed?.();
|
|
79
|
+
},
|
|
72
80
|
onContainer: info => {
|
|
73
81
|
native = true;
|
|
74
82
|
const witness = witnesses.at(-1);
|
|
@@ -95,6 +95,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
95
95
|
readonly items: readonly string[];
|
|
96
96
|
readonly gaps: readonly string[];
|
|
97
97
|
readonly priorSupport: "eligible" | "invalid" | "none";
|
|
98
|
+
readonly assets?: string;
|
|
98
99
|
};
|
|
99
100
|
}[];
|
|
100
101
|
readonly machineVerdict: import("./store.js").ProofVerdictRow["verdict"] | null;
|
|
@@ -48,9 +48,9 @@ export type LearningEvent = {
|
|
|
48
48
|
};
|
|
49
49
|
export declare const learningSha: (text: string) => string;
|
|
50
50
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
51
|
-
export declare function learningIdentity(repo: string): string;
|
|
51
|
+
export declare function learningIdentity(repo: string, seen?: Map<string, string>): string;
|
|
52
52
|
/** Compare the applicable code and configuration, not model confidence or a claimed passing command. */
|
|
53
|
-
export declare function learningFingerprint(repo: string, head: string, paths: readonly string[]): string;
|
|
53
|
+
export declare function learningFingerprint(repo: string, head: string, paths: readonly string[], seen?: Map<string, string>): string;
|
|
54
54
|
export declare function parseLearning(value: unknown): LearningCandidate[];
|
|
55
55
|
/** A write problem is optional: preserve core completion, leave a visible diagnostic, retry on the next read/admission. */
|
|
56
56
|
export declare function learningFailure(store: Store, repo: string, run: number | null, now: Date): void;
|
|
@@ -60,7 +60,7 @@ export declare function queueLearning(store: Store, source: number, reviewer: nu
|
|
|
60
60
|
sha256: string;
|
|
61
61
|
}[], now: Date, assessment?: unknown): void;
|
|
62
62
|
/** Validate after core commit; both suggestions commit or neither. */
|
|
63
|
-
export declare function recoverLearning(store: Store, root: string, repo: string, now?: Date): void;
|
|
63
|
+
export declare function recoverLearning(store: Store, root: string, repo: string, now?: Date, seen?: Map<string, string>): void;
|
|
64
64
|
export declare function learningView(store: Store, root: string, repo: string, actor: string, before?: number): {
|
|
65
65
|
repo: string;
|
|
66
66
|
identity: string;
|