toolroll 0.9.8 → 0.9.10
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 +30 -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 +9 -0
- package/dist/assignment.js +42 -8
- 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/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/chat-delivery-state.d.ts +5 -0
- package/dist/chat-delivery.js +45 -8
- package/dist/chat-quiet.d.ts +10 -4
- package/dist/chat-quiet.js +87 -22
- package/dist/chat-voice.d.ts +68 -0
- package/dist/chat-voice.js +175 -0
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/discord-chat.js +1 -0
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +7 -5
- 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/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +122 -15
- package/dist/plane-review.js +4 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-delete.js +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/releases.d.ts +9 -0
- package/dist/releases.js +30 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- 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 +129 -14
- 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/slack-chat.js +2 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +207 -30
- package/dist/task-status.d.ts +18 -4
- package/dist/task-status.js +35 -18
- package/dist/teams-chat.js +1 -1
- package/dist/telegram-progress.d.ts +6 -1
- package/dist/telegram-progress.js +13 -5
- package/dist/telegram-status.d.ts +2 -1
- package/dist/telegram-status.js +6 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +93 -43
- 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/workspace-ui.js +1 -1
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
package/dist/operate.js
CHANGED
|
@@ -7,8 +7,11 @@ import { parseProtectedPaths } from "./approval-policy.js";
|
|
|
7
7
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
8
8
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
9
9
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
10
|
-
import { CLEANUP_CHOICES, bytesWords, cleanupWords, parseCleanup, storageReport } from "./storage.js";
|
|
11
|
-
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";
|
|
12
15
|
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
13
16
|
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
14
17
|
import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames, usd as spendUsd } from "./spend.js";
|
|
@@ -73,6 +76,7 @@ import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./
|
|
|
73
76
|
*/
|
|
74
77
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
75
78
|
import { basename, dirname, join, resolve } from "node:path";
|
|
79
|
+
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
76
80
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, } from "./store.js";
|
|
77
81
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
78
82
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -193,8 +197,8 @@ import { runModelsCommand } from "./models-cli.js";
|
|
|
193
197
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
194
198
|
import { beads } from "./beads.js";
|
|
195
199
|
import { githubIssues } from "./issues.js";
|
|
196
|
-
import { installationStatus, renderInstallationStatus, renderTaskWait, taskWaitSnapshot } from "./lead-status.js";
|
|
197
|
-
import { latestRelease, newerRelease, notifySecurityRelease, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
200
|
+
import { installationStatus, renderInstallationStatus, renderTaskWait, repairStaleStatuses, taskWaitSnapshot } from "./lead-status.js";
|
|
201
|
+
import { latestRelease, newerRelease, notifySecurityRelease, notifyVersionLive, recordRunnerVersion, startUpdateChecks, updateLine } from "./releases.js";
|
|
198
202
|
import { installMethod } from "./install-method.js";
|
|
199
203
|
import { PACKAGE_VERSION } from "./version.js";
|
|
200
204
|
/**
|
|
@@ -227,8 +231,8 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
227
231
|
how many of a project's tasks build at once (default 2, never past the
|
|
228
232
|
worker's capacity); an approver changes it with <n>
|
|
229
233
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
230
|
-
toolroll storage where the disk goes: database, build checkouts, releases, evidence
|
|
231
|
-
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
|
|
232
236
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
233
237
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
234
238
|
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
@@ -807,6 +811,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
807
811
|
// it matters.
|
|
808
812
|
evidenceRoot: join(dirname(file), "evidence"),
|
|
809
813
|
databaseFile: file,
|
|
814
|
+
...(options.tempRoots === undefined ? {} : { tempRoots: options.tempRoots }),
|
|
810
815
|
// The bot token's file home. Never a column; see telegram.ts.
|
|
811
816
|
telegramTokenFile: join(dirname(file), "telegram-token"),
|
|
812
817
|
...(options.desktopIdentity === undefined ? {} : { desktopIdentity: options.desktopIdentity }),
|
|
@@ -4397,10 +4402,21 @@ async function reconcileCommand(flags, context) {
|
|
|
4397
4402
|
}
|
|
4398
4403
|
}
|
|
4399
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());
|
|
4400
4411
|
// A witness reserved for a spawn that never made a process (a crash or a
|
|
4401
4412
|
// thrown spawn) settles once its run's process groups are proven gone.
|
|
4402
4413
|
store.settleUnspawnedWitnesses(clock());
|
|
4403
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 */ }
|
|
4404
4420
|
for (const one of recovered) {
|
|
4405
4421
|
for (const leaseId of one.claims) {
|
|
4406
4422
|
// Lease ids are unique forever, so each recovery is its own episode.
|
|
@@ -4452,7 +4468,11 @@ async function reconcileCommand(flags, context) {
|
|
|
4452
4468
|
}
|
|
4453
4469
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
4454
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));
|
|
4455
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 });
|
|
4456
4476
|
// Retention (Settings → Retention): once a day, what's older than its setting goes; one ledger entry says what.
|
|
4457
4477
|
let retention = null;
|
|
4458
4478
|
try {
|
|
@@ -4468,6 +4488,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4468
4488
|
adoption.adopted.length === 0 &&
|
|
4469
4489
|
adoption.forgotten.length === 0 &&
|
|
4470
4490
|
pruned.removed.length === 0 &&
|
|
4491
|
+
closedRuns.length === 0 &&
|
|
4492
|
+
closedCheckouts.length === 0 &&
|
|
4493
|
+
slimmed.slimmed.length === 0 &&
|
|
4471
4494
|
liveSwept.removed.length === 0;
|
|
4472
4495
|
return succeed(write, json, "reconcile", {
|
|
4473
4496
|
recovered,
|
|
@@ -4476,6 +4499,8 @@ async function reconcileCommand(flags, context) {
|
|
|
4476
4499
|
forgotten: adoption.forgotten,
|
|
4477
4500
|
checkoutsRemoved: pruned.removed.map(one => one.path),
|
|
4478
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),
|
|
4479
4504
|
liveViewsSwept: liveSwept.removed.length,
|
|
4480
4505
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
4481
4506
|
}, () => nothing
|
|
@@ -4486,6 +4511,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4486
4511
|
...adoption.adopted.map(path => `Adopted ${path} — released, unverified, somebody should look.`),
|
|
4487
4512
|
...adoption.forgotten.map(path => `Forgot ${path} — its directory is gone.`),
|
|
4488
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))}.`]),
|
|
4489
4517
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
4490
4518
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
4491
4519
|
]);
|
|
@@ -7259,6 +7287,8 @@ async function daemonCommand(positional, flags, context) {
|
|
|
7259
7287
|
// ---- the watch loop --------------------------------------------------------
|
|
7260
7288
|
const WATCH_LEASE_MS = 90_000;
|
|
7261
7289
|
const WATCH_HEARTBEAT_MS = 30_000;
|
|
7290
|
+
/** The start repair runs once per process, not once per project loop. */
|
|
7291
|
+
let staleStatusesRepaired = false;
|
|
7262
7292
|
async function runWatchLoop(args) {
|
|
7263
7293
|
const { context, flags, runner, token, repo, progress } = args;
|
|
7264
7294
|
const { store } = context;
|
|
@@ -7294,8 +7324,21 @@ async function runWatchLoop(args) {
|
|
|
7294
7324
|
// attributes to this episode by runner and window, and `brief
|
|
7295
7325
|
// --latest-watch` bounds itself to exactly it.
|
|
7296
7326
|
store.startWatchEpisode({ repo, runner, incarnation }, new Date());
|
|
7327
|
+
// A deploy's one chat line, "Toolroll <version> is live.", when this worker last ran an older version.
|
|
7328
|
+
try {
|
|
7329
|
+
notifyVersionLive(store, dirname(context.databaseFile), PACKAGE_VERSION, new Date());
|
|
7330
|
+
}
|
|
7331
|
+
catch { /* A display fact; never blocks the worker. */ }
|
|
7297
7332
|
recordRunnerVersion(dirname(context.databaseFile), runner, PACKAGE_VERSION, new Date());
|
|
7298
7333
|
args.onReady?.();
|
|
7334
|
+
// Once per worker start, before any pass: statuses an older build left
|
|
7335
|
+
// stale (unrecorded exits of finished runs) are re-derived and clear.
|
|
7336
|
+
if (!staleStatusesRepaired) {
|
|
7337
|
+
staleStatusesRepaired = true;
|
|
7338
|
+
const repaired = repairStaleStatuses(store, new Date());
|
|
7339
|
+
if (repaired.exitsRecorded > 0)
|
|
7340
|
+
progress(`watch: recorded ${repaired.exitsRecorded} finished process exit(s) left open by an earlier version`);
|
|
7341
|
+
}
|
|
7299
7342
|
// A false renewal is FATAL (arc 2 finding 15): the lease or the
|
|
7300
7343
|
// credential is gone, and admitting one more pass would be work done for
|
|
7301
7344
|
// an authority this process no longer holds.
|
|
@@ -7792,7 +7835,8 @@ async function watchCommand(flags, context) {
|
|
|
7792
7835
|
token,
|
|
7793
7836
|
repo,
|
|
7794
7837
|
progress,
|
|
7795
|
-
|
|
7838
|
+
// An injected fence (tests) stops admission exactly as a signal does.
|
|
7839
|
+
isStopping: () => stopping || context.shouldStop?.() === true,
|
|
7796
7840
|
onFollowController: controller => {
|
|
7797
7841
|
followAbort = controller;
|
|
7798
7842
|
},
|
|
@@ -8574,6 +8618,10 @@ async function upCommand(flags, context) {
|
|
|
8574
8618
|
});
|
|
8575
8619
|
};
|
|
8576
8620
|
observeReadiness();
|
|
8621
|
+
try {
|
|
8622
|
+
notifyVersionLive(context.store, dirname(context.databaseFile), PACKAGE_VERSION, clock());
|
|
8623
|
+
}
|
|
8624
|
+
catch { /* A display fact; never blocks the worker. */ }
|
|
8577
8625
|
recordRunnerVersion(dirname(context.databaseFile), runnerName, PACKAGE_VERSION, clock());
|
|
8578
8626
|
const runnerHeartbeat = setInterval(() => {
|
|
8579
8627
|
const beat = heartbeatRunner(store, runnerName, runnerToken, clock());
|
|
@@ -8836,6 +8884,10 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8836
8884
|
const runFor = text(flags, "for");
|
|
8837
8885
|
const timer = runFor === undefined ? null : setTimeout(() => controller.abort(), Number(runFor));
|
|
8838
8886
|
timer?.unref?.();
|
|
8887
|
+
// An injected fence (tests) ends the follow exactly as a signal does.
|
|
8888
|
+
const fence = context.shouldStop === undefined ? null : setInterval(() => { if (context.shouldStop?.() === true)
|
|
8889
|
+
controller.abort(); }, 50);
|
|
8890
|
+
fence?.unref?.();
|
|
8839
8891
|
if (!json)
|
|
8840
8892
|
write(`Following bot ${source.botId} — taps apply as they arrive. Ctrl-C stops it.`);
|
|
8841
8893
|
try {
|
|
@@ -8865,6 +8917,8 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
8865
8917
|
finally {
|
|
8866
8918
|
if (timer !== null)
|
|
8867
8919
|
clearTimeout(timer);
|
|
8920
|
+
if (fence !== null)
|
|
8921
|
+
clearInterval(fence);
|
|
8868
8922
|
process.removeListener("SIGINT", stop);
|
|
8869
8923
|
process.removeListener("SIGTERM", stop);
|
|
8870
8924
|
}
|
|
@@ -9528,10 +9582,13 @@ async function statusCommand(positional, flags, context) {
|
|
|
9528
9582
|
const brokenIntegrations = await savedIntegrations(context).then(list => list.filter(one => one.state === "broken"), () => []);
|
|
9529
9583
|
const brokenLine = integrationsBrokenLine(brokenIntegrations);
|
|
9530
9584
|
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 })) } };
|
|
9585
|
+
// An update waiting, on what, and the action: before anything else, as it holds up new work.
|
|
9586
|
+
const waiting = waitingUpdate(context.databaseFile, run => context.store.stopQuiescenceProblem(run) !== null, context.clock());
|
|
9587
|
+
const updateWaiting = waiting === null ? {} : { updateWaiting: { app: waiting.app, version: waiting.version, stopped: waiting.stopped, run: waiting.run, on: waiting.on, action: waiting.action } };
|
|
9531
9588
|
const projects = projectBuildsStatus(context);
|
|
9532
9589
|
const projectsLine = projects.length === 0 ? null
|
|
9533
9590
|
: `Builds by project: ${projects.slice(0, 8).map(one => `${one.name} ${one.running} of ${one.limit}`).join(", ")}${projects.length > 8 ? ", …" : ""}`;
|
|
9534
|
-
return succeed(context.write, context.json, command, { ...status, projects, ...update, ...integrations }, () => [...renderInstallationStatus(status), ...(projectsLine === null ? [] : [projectsLine]), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9591
|
+
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])]);
|
|
9535
9592
|
}
|
|
9536
9593
|
/** Each known project's running builds against its limit: its own number, never past its workers' capacity. */
|
|
9537
9594
|
function projectBuildsStatus(context) {
|
|
@@ -12145,13 +12202,35 @@ async function storageCommand(positional, flags, context) {
|
|
|
12145
12202
|
`A finished task's clean checkout is removed ${cleanupWords(plan.cleanup)}; its branch stays.`,
|
|
12146
12203
|
];
|
|
12147
12204
|
const itemLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${one.path}${one.why === null ? "" : ` (${whyWords(one)})`}`;
|
|
12205
|
+
// Test temp folders the tests, journeys and scripts left (a killed run, an older version): those nothing touched for a day go.
|
|
12206
|
+
const roots = context.tempRoots ?? tempRoots();
|
|
12207
|
+
// Counting is quick; sizing thousands of folders isn't, so only a clean-up measures them.
|
|
12208
|
+
const leftovers = (measure) => {
|
|
12209
|
+
const found = testTempFolders(roots, context.clock());
|
|
12210
|
+
const sizes = measure ? diskBytes(found.stale.map(one => one.path)) : new Map();
|
|
12211
|
+
return { count: found.all.length, stale: found.stale.length, staleBytes: [...sizes.values()].reduce((sum, one) => sum + one, 0) };
|
|
12212
|
+
};
|
|
12213
|
+
const leftoverLine = (temp) => temp.count === 0 ? "Test temp folders: none." :
|
|
12214
|
+
`Test temp folders: ${temp.count}; ${temp.stale === 0 ? "none older than a day." : `${temp.stale} older than a day: toolroll storage clean removes them.`}`;
|
|
12215
|
+
// Shared dependencies (shared-deps.ts): one install per lockfile, linked into checkouts.
|
|
12216
|
+
const depsRoot = sharedDepsRoot(context.databaseFile);
|
|
12217
|
+
const checkoutRows = () => store.listWorktrees().map(row => ({ path: row.path, repo: row.repo }));
|
|
12218
|
+
const sharedFacts = (copies) => {
|
|
12219
|
+
const sizes = diskBytes(copies.map(one => one.dir));
|
|
12220
|
+
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 }));
|
|
12221
|
+
};
|
|
12222
|
+
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)" : ""}`;
|
|
12148
12223
|
if (action === undefined) {
|
|
12149
12224
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12150
12225
|
const report = storageReport(store, context.databaseFile, plan.go.length === 0 ? "nothing to clean up" : `about ${bytesWords(plan.freeBytes)} to clean up`);
|
|
12151
|
-
|
|
12226
|
+
const { staleBytes: _, ...temp } = leftovers(false);
|
|
12227
|
+
const shared = sharedFacts(sharedUse(depsRoot, checkoutRows()));
|
|
12228
|
+
return succeed(context.write, context.json, "storage", { ...report, checkouts: checkoutSummary(plan), testTemp: temp, shared }, () => [
|
|
12152
12229
|
`${report.folder}: ${bytesWords(report.total)}`,
|
|
12153
|
-
...report.lines.map(one => ` ${one.what.padEnd(
|
|
12230
|
+
...report.lines.map(one => ` ${one.what.padEnd(19)} ${bytesWords(one.bytes).padStart(8)}${one.count === undefined ? "" : ` (${one.count})`}${one.note === undefined ? "" : ` ${one.note}`}`),
|
|
12231
|
+
...(shared.length === 0 ? [] : ["Shared dependencies:", ...shared.map(sharedLine)]),
|
|
12154
12232
|
...summary(plan),
|
|
12233
|
+
leftoverLine({ ...temp, staleBytes: 0 }),
|
|
12155
12234
|
]);
|
|
12156
12235
|
}
|
|
12157
12236
|
if (action === "cleanup") {
|
|
@@ -12173,20 +12252,48 @@ async function storageCommand(positional, flags, context) {
|
|
|
12173
12252
|
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
12174
12253
|
if (!flags.has("yes")) {
|
|
12175
12254
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
12176
|
-
|
|
12177
|
-
|
|
12255
|
+
const temp = leftovers(true);
|
|
12256
|
+
// A shared copy goes when no checkout uses it, counting the checkouts this clean-up would remove.
|
|
12257
|
+
const leaving = new Set(plan.go.map(one => one.path));
|
|
12258
|
+
const unused = unusedShared(depsRoot, checkoutRows().filter(row => !leaving.has(row.path)), context.clock());
|
|
12259
|
+
const sharedGo = sharedFacts(unused.copies);
|
|
12260
|
+
const sharedBytes = sharedGo.reduce((sum, one) => sum + one.bytes, 0);
|
|
12261
|
+
const anything = plan.go.length > 0 || temp.stale > 0 || sharedGo.length > 0;
|
|
12262
|
+
return succeed(context.write, context.json, command, { preview: true, go: plan.go, stay: plan.stay, freeBytes: plan.freeBytes, testTemp: temp, sharedGo, sharedFreeBytes: sharedBytes }, () => [
|
|
12263
|
+
...(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)]),
|
|
12264
|
+
...(sharedGo.length === 0 ? [] : [`Would remove ${sharedGo.length} shared dependency install${sharedGo.length === 1 ? "" : "s"} no checkout uses, about ${bytesWords(sharedBytes)}:`, ...sharedGo.map(sharedLine)]),
|
|
12178
12265
|
...(plan.stay.length === 0 ? [] : [`Stays (${plan.stay.length}):`, ...plan.stay.map(itemLine)]),
|
|
12179
|
-
|
|
12266
|
+
...(temp.stale === 0 ? [] : [`Would remove ${temp.stale} test temp folder${temp.stale === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12267
|
+
anything ? "Nothing was removed. Run toolroll storage clean --yes to remove them." : "Nothing was removed.",
|
|
12180
12268
|
]);
|
|
12181
12269
|
}
|
|
12182
12270
|
const actor = await operator();
|
|
12183
12271
|
if (actor === null)
|
|
12184
12272
|
return refused("cleans up checkouts");
|
|
12185
12273
|
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
12186
|
-
|
|
12187
|
-
|
|
12274
|
+
const temp = leftovers(true);
|
|
12275
|
+
const swept = removeStaleTestTemp(roots, context.clock());
|
|
12276
|
+
if (swept.removed.length > 0) {
|
|
12277
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: null, taskId: null, runId: null, action: "test temp folders removed", outcome: "removed", source: "request",
|
|
12278
|
+
detail: `${swept.removed.length} older than a day, about ${bytesWords(temp.staleBytes)}${swept.failed.length === 0 ? "" : `; ${swept.failed.length} couldn't be removed`}` });
|
|
12279
|
+
}
|
|
12280
|
+
// After the checkouts go: a shared copy none of the remaining ones uses goes too.
|
|
12281
|
+
const unused = unusedShared(depsRoot, checkoutRows(), context.clock());
|
|
12282
|
+
const sharedRemoved = sharedFacts(unused.copies).filter(one => removeShared(one.path));
|
|
12283
|
+
for (const one of sharedRemoved) {
|
|
12284
|
+
store.recordAction({ at: context.clock().toISOString(), actor, repo: one.repo, taskId: null, runId: null, action: "shared dependencies removed", outcome: "removed", source: "request",
|
|
12285
|
+
detail: `${one.key} (about ${bytesWords(one.bytes)}); no checkout used it` });
|
|
12286
|
+
}
|
|
12287
|
+
for (const debris of unused.debris)
|
|
12288
|
+
removeShared(debris);
|
|
12289
|
+
const sharedFreed = sharedRemoved.reduce((sum, one) => sum + one.bytes, 0);
|
|
12290
|
+
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 }, () => [
|
|
12291
|
+
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.`,
|
|
12188
12292
|
...done.removed.map(itemLine),
|
|
12293
|
+
...(sharedRemoved.length === 0 ? [] : [`Removed ${sharedRemoved.length} shared dependency install${sharedRemoved.length === 1 ? "" : "s"} no checkout used, about ${bytesWords(sharedFreed)}.`, ...sharedRemoved.map(sharedLine)]),
|
|
12189
12294
|
...done.kept.map(one => ` kept ${one.path} (${whyWords({ why: one.why })})`),
|
|
12295
|
+
...(swept.removed.length === 0 ? [] : [`Removed ${swept.removed.length} test temp folder${swept.removed.length === 1 ? "" : "s"} older than a day, about ${bytesWords(temp.staleBytes)}.`]),
|
|
12296
|
+
...(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 ? " …" : ""}`]),
|
|
12190
12297
|
]);
|
|
12191
12298
|
}
|
|
12192
12299
|
if (action === "discard") {
|
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;
|
|
@@ -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;
|
package/dist/project-delete.js
CHANGED
|
@@ -232,6 +232,7 @@ function deleteRows(store, repo, d, now) {
|
|
|
232
232
|
del("telegram_conversation", IN("id"), conversations);
|
|
233
233
|
del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
|
|
234
234
|
del("chat_card_task", IN("task_ref"), T);
|
|
235
|
+
del("chat_batch_item", IN("task_ref"), T);
|
|
235
236
|
del("push_delivery", IN("notification"), notifications);
|
|
236
237
|
del("notification_actor", IN("notification"), notifications);
|
|
237
238
|
del("notification_delivery", IN("notification"), notifications);
|
|
@@ -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;
|
package/dist/project-learning.js
CHANGED
|
@@ -44,12 +44,21 @@ CREATE TRIGGER IF NOT EXISTS learning_capture_no_delete BEFORE DELETE ON learnin
|
|
|
44
44
|
export const learningSha = (text) => createHash('sha256').update(text).digest('hex');
|
|
45
45
|
const text = (x, max) => typeof x === 'string' && x.trim().length > 0 && Buffer.byteLength(x) <= max && !/[\x00-\x1f\x7f]/.test(x);
|
|
46
46
|
const pathOk = (x) => text(x, 300) && !x.startsWith('/') && !x.includes('\\') && !/^[A-Za-z]:/.test(x) && x.split('/').every(p => p !== '.' && p !== '..' && p !== '') && !/[\*?\[\]:]/.test(x);
|
|
47
|
-
|
|
47
|
+
/** One read per distinct command within a single selection pass (`seen`), so a pass over many lessons reads the repository once. */
|
|
48
|
+
const git = (repo, args, seen) => {
|
|
49
|
+
const key = JSON.stringify([repo, args]);
|
|
50
|
+
const known = seen?.get(key);
|
|
51
|
+
if (known !== undefined)
|
|
52
|
+
return known;
|
|
53
|
+
const out = execFileSync('git', ['--no-optional-locks', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
|
|
54
|
+
seen?.set(key, out);
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
48
57
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
49
|
-
export function learningIdentity(repo) {
|
|
58
|
+
export function learningIdentity(repo, seen) {
|
|
50
59
|
if (canonicalProject(repo) !== repo)
|
|
51
60
|
throw new Error('Project identity changed.');
|
|
52
|
-
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'])));
|
|
61
|
+
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'], seen)));
|
|
53
62
|
const st = statSync(common);
|
|
54
63
|
return learningSha(JSON.stringify([repo, common, st.dev, st.ino, st.birthtimeMs]));
|
|
55
64
|
}
|
|
@@ -59,10 +68,10 @@ function environmentFingerprint(store, repo) {
|
|
|
59
68
|
}
|
|
60
69
|
const configPath = (p) => /(^|\/)(AGENTS\.md|package(-lock)?\.json|[^/]*lock[^/]*|[^/]*config[^/]*|Cargo\.toml|go\.mod|pyproject\.toml|\.tool-versions|\.nvmrc)$/.test(p);
|
|
61
70
|
/** Compare the applicable code and configuration, not model confidence or a claimed passing command. */
|
|
62
|
-
export function learningFingerprint(repo, head, paths) {
|
|
71
|
+
export function learningFingerprint(repo, head, paths, seen) {
|
|
63
72
|
if (!/^[a-f0-9]{40,64}$/.test(head))
|
|
64
73
|
throw new Error('No exact source revision.');
|
|
65
|
-
const entries = git(repo, ['ls-tree', '-r', head]).split('\n').filter(line => {
|
|
74
|
+
const entries = git(repo, ['ls-tree', '-r', head], seen).split('\n').filter(line => {
|
|
66
75
|
const p = line.slice(line.indexOf('\t') + 1);
|
|
67
76
|
return paths.includes(p) || configPath(p);
|
|
68
77
|
});
|
|
@@ -198,7 +207,7 @@ function verifyEvidence(store, root, source, evidence, catalog) {
|
|
|
198
207
|
// missing work, so newer reviews cannot starve an older recovery.
|
|
199
208
|
const recoveryCursors = new WeakMap();
|
|
200
209
|
/** Validate after core commit; both suggestions commit or neither. */
|
|
201
|
-
export function recoverLearning(store, root, repo, now = new Date()) {
|
|
210
|
+
export function recoverLearning(store, root, repo, now = new Date(), seen = new Map()) {
|
|
202
211
|
let projects = recoveryCursors.get(store);
|
|
203
212
|
if (!projects)
|
|
204
213
|
recoveryCursors.set(store, projects = new Map());
|
|
@@ -225,7 +234,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
225
234
|
if (!source || !reviewer || source.taskRef !== reviewer.taskRef)
|
|
226
235
|
continue;
|
|
227
236
|
const snapshot = reviewSnapshot(store, reviewer.id);
|
|
228
|
-
if (!snapshot || snapshot['identity'] !== learningIdentity(repo))
|
|
237
|
+
if (!snapshot || snapshot['identity'] !== learningIdentity(repo, seen))
|
|
229
238
|
continue;
|
|
230
239
|
const response = store.artifactsFor(reviewer.id).find(a => a.kind === 'structured-output' && a.captureStatus === 'ok');
|
|
231
240
|
const read = response && readVerifiedArtifact(root, response);
|
|
@@ -260,7 +269,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
260
269
|
if (!run || !reviewed || reviewed.outcome !== 'no-change' || reviewed.taskRef !== run.taskRef || reviewed.role !== 'reviewer' || reviewedSource(store, reviewer) !== source || store.refForId(run.taskRef)?.repo !== repo)
|
|
261
270
|
throw new Error('Learning review provenance is unavailable.');
|
|
262
271
|
const candidates = parseLearning(JSON.parse(String(row['payload'])));
|
|
263
|
-
const identity = learningIdentity(repo);
|
|
272
|
+
const identity = learningIdentity(repo, seen);
|
|
264
273
|
if (identity !== row['identity'])
|
|
265
274
|
throw new Error('Learning project identity changed.');
|
|
266
275
|
const head = run.headRevision ?? run.baseRevision ?? '';
|
|
@@ -273,7 +282,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
273
282
|
verifyEvidence(store, root, source, c.evidence, JSON.parse(String(row['catalog'])));
|
|
274
283
|
if (c.paths.some(p => !patch.split('\n').some(l => l === `+++ b/${p}` || l === `--- a/${p}`)))
|
|
275
284
|
throw new Error('Learning applicability must name reviewed files.');
|
|
276
|
-
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths), platform: process.platform, head });
|
|
285
|
+
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths, seen), platform: process.platform, head });
|
|
277
286
|
const finding = learningSha(JSON.stringify(c));
|
|
278
287
|
store.handle.prepare("INSERT OR IGNORE INTO project_lesson(repo,identity,source,reviewer,finding,payload,sha,status) VALUES (?,?,?,?,?,?,?,'proposed')").run(repo, identity, source, reviewer, finding, payload, learningSha(payload));
|
|
279
288
|
const id = Number(store.handle.prepare('SELECT id FROM project_lesson WHERE source=? AND finding=?').get(source, finding)['id']);
|
|
@@ -302,15 +311,15 @@ function lessonOf(row) {
|
|
|
302
311
|
parseLearning([payload]);
|
|
303
312
|
return { id: Number(row['id']), repo: String(row['repo']), source: Number(row['source']), reviewer: Number(row['reviewer']), status: String(row['status']), version: Number(row['version']), payload, sha: String(row['sha']), identity: String(row['identity']), adoptedBy: row['adopted_by'] == null ? null : String(row['adopted_by']) };
|
|
304
313
|
}
|
|
305
|
-
function supported(store, root, lesson) {
|
|
306
|
-
if (lesson.identity !== learningIdentity(lesson.repo))
|
|
314
|
+
function supported(store, root, lesson, seen) {
|
|
315
|
+
if (lesson.identity !== learningIdentity(lesson.repo, seen))
|
|
307
316
|
throw new Error('Project identity changed.');
|
|
308
317
|
const capture = store.handle.prepare('SELECT catalog FROM learning_capture WHERE source=? AND reviewer=? AND repo=?').get(lesson.source, lesson.reviewer, lesson.repo);
|
|
309
318
|
if (!capture || reviewedSource(store, lesson.reviewer) !== lesson.source)
|
|
310
319
|
throw new Error('Learning source is unavailable.');
|
|
311
320
|
verifyEvidence(store, root, lesson.source, lesson.payload.evidence, JSON.parse(String(capture['catalog'])));
|
|
312
321
|
const run = store.getRun(lesson.source);
|
|
313
|
-
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths) !== lesson.payload.fingerprint)
|
|
322
|
+
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths, seen) !== lesson.payload.fingerprint)
|
|
314
323
|
throw new Error('Learning source changed.');
|
|
315
324
|
}
|
|
316
325
|
export function learningView(store, root, repo, actor, before = 0) {
|
|
@@ -377,15 +386,17 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
377
386
|
const run = store.getRun(runId), ref = run && store.refForId(run.taskRef), repo = ref?.repo;
|
|
378
387
|
if (!run || !repo)
|
|
379
388
|
return '';
|
|
389
|
+
// One read of the repository per command for this whole pass, recovery included.
|
|
390
|
+
const seen = new Map();
|
|
380
391
|
try {
|
|
381
|
-
recoverLearning(store, root, repo, now);
|
|
392
|
+
recoverLearning(store, root, repo, now, seen);
|
|
382
393
|
return store.transact(() => {
|
|
383
394
|
if (!store.schemaCurrent() || run.outcome !== null || !['planner', 'builder', 'repair', 'reviewer'].includes(run.role))
|
|
384
395
|
return '';
|
|
385
396
|
const runner = store.getRunner(run.runner)?.runner;
|
|
386
397
|
if (!runner || runner.retiredAt !== null || !runner.repos.includes(repo) || (phase === 'review') !== (run.role === 'reviewer') || (phase === 'plan') !== (run.role === 'planner'))
|
|
387
398
|
return '';
|
|
388
|
-
const identity = learningIdentity(repo);
|
|
399
|
+
const identity = learningIdentity(repo, seen);
|
|
389
400
|
const existing = store.handle.prepare('SELECT * FROM learning_snapshot WHERE run=?').get(runId);
|
|
390
401
|
if (existing) {
|
|
391
402
|
if (existing['repo'] !== repo || existing['identity'] !== identity || learningSha(String(existing['payload'])) !== existing['sha'])
|
|
@@ -409,11 +420,11 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
409
420
|
const l = lessonOf(row);
|
|
410
421
|
if (!store.handle.prepare("SELECT 1 FROM learning_event WHERE lesson=? AND action='adopt' AND actor=?").get(l.id, l.adoptedBy ?? '') || l.source === source?.id || l.payload.kind !== 'project' || !l.adoptedBy || !store.accountCanAccess(l.adoptedBy, repo) || store.accountOf(l.adoptedBy)?.role !== 'approver' || !l.payload.phases.includes(phase) || l.payload.platform !== process.platform || (!unscopedPlan && !l.payload.paths.some(p => paths.some(t => p === t || p.startsWith(t.replace(/\/$/, '') + '/')))))
|
|
411
422
|
continue;
|
|
412
|
-
supported(store, root, l);
|
|
413
|
-
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths) !== l.payload.fingerprint)
|
|
423
|
+
supported(store, root, l, seen);
|
|
424
|
+
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths, seen) !== l.payload.fingerprint)
|
|
414
425
|
continue;
|
|
415
426
|
const cwd = run.worktree ?? repo;
|
|
416
|
-
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all']).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
427
|
+
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all'], seen).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
417
428
|
if (!dirty)
|
|
418
429
|
eligible.push(l);
|
|
419
430
|
}
|
package/dist/proof.d.ts
CHANGED
|
@@ -266,7 +266,7 @@ export type VerifyCommandFacts = {
|
|
|
266
266
|
configured: true;
|
|
267
267
|
ran: false;
|
|
268
268
|
attemptFailed: true;
|
|
269
|
-
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
269
|
+
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "own-install-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
270
270
|
} | {
|
|
271
271
|
configured: true;
|
|
272
272
|
ran: true;
|
|
@@ -390,6 +390,8 @@ export type CriterionCoverage = {
|
|
|
390
390
|
* context: eligible only with the same text, verified ancestry, and
|
|
391
391
|
* byte-identical relevant code. Never a judgement on this run. */
|
|
392
392
|
priorSupport: "eligible" | "invalid" | "none";
|
|
393
|
+
/** The change's images and other binary files in one line, when it has any. */
|
|
394
|
+
assets?: string;
|
|
393
395
|
};
|
|
394
396
|
/** v40: the three words an evidence reviewer can say about one signed
|
|
395
397
|
* criterion, and nothing else — `cannot-tell` is a CORRECT answer whenever
|
package/dist/proof.js
CHANGED
|
@@ -722,7 +722,7 @@ function assessCapturedEvidence(input) {
|
|
|
722
722
|
id: criterion.id, statement: criterion.statement, requiredEvidence: criterion.evidence,
|
|
723
723
|
assessment: { evidenceState, detail }, state: evidenceState === "pass" ? "missing" : evidenceState,
|
|
724
724
|
detail: [...detail, `criterion "${criterion.id}" awaits independent goal assessment`], answered, review: null,
|
|
725
|
-
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport } }),
|
|
725
|
+
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport, ...(coverage.assets === undefined ? {} : { assets: coverage.assets }) } }),
|
|
726
726
|
};
|
|
727
727
|
});
|
|
728
728
|
return { machineVerdict, verdict: machineVerdict === "refuted" ? "refuted" : "short", matrix,
|
|
@@ -761,6 +761,8 @@ function verificationFailureWords(failure) {
|
|
|
761
761
|
return "automatic recovery stopped because the project setup or check changed";
|
|
762
762
|
case "setup-failed":
|
|
763
763
|
return "the approved setup command failed during automatic recovery";
|
|
764
|
+
case "own-install-failed":
|
|
765
|
+
return "the checkout's own dependency install failed after its package.json or lockfile changed";
|
|
764
766
|
case "tracked-files-changed":
|
|
765
767
|
return "automatic recovery stopped because tracked files no longer matched the built result";
|
|
766
768
|
case "setup-changed-files":
|
|
@@ -828,7 +830,7 @@ export function adjudicate(input) {
|
|
|
828
830
|
// must show, never a silent "judged from the patch".
|
|
829
831
|
const coverage = found === undefined
|
|
830
832
|
? { state: "gap", inherited: false, items: [], gaps: ["the sealed review context carries no coverage entry for this criterion"], priorSupport: "none" }
|
|
831
|
-
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport };
|
|
833
|
+
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport, ...(found.assets === undefined ? {} : { assets: found.assets }) };
|
|
832
834
|
return { ...row, coverage };
|
|
833
835
|
});
|
|
834
836
|
if (!input.proofArtifactPresent) {
|
package/dist/releases.d.ts
CHANGED
|
@@ -83,3 +83,12 @@ export declare function latestVersionNow(io?: {
|
|
|
83
83
|
}): Promise<{
|
|
84
84
|
version: string;
|
|
85
85
|
}>;
|
|
86
|
+
/** What a release says about itself in one line ("Release 0.9.9: lighter tests, cleaner status"), when its notes
|
|
87
|
+
* have such a line; null otherwise. */
|
|
88
|
+
export declare function releaseSummary(notes: string, version: string): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* A deploy says one thing in chat: "Toolroll 0.9.9 is live." (with the release's own one-line summary when its
|
|
91
|
+
* notes have one). Once per version, addressed to each instance operator, and only when a worker on this
|
|
92
|
+
* computer last ran an older version: a fresh install is not news. Call before recording this runner's version.
|
|
93
|
+
*/
|
|
94
|
+
export declare function notifyVersionLive(store: Store, dir: string, version: string, now: Date): number;
|
package/dist/releases.js
CHANGED
|
@@ -236,3 +236,33 @@ export async function latestVersionNow(io = {}) {
|
|
|
236
236
|
throw new Error("npm did not name a version");
|
|
237
237
|
return { version };
|
|
238
238
|
}
|
|
239
|
+
/** What a release says about itself in one line ("Release 0.9.9: lighter tests, cleaner status"), when its notes
|
|
240
|
+
* have such a line; null otherwise. */
|
|
241
|
+
export function releaseSummary(notes, version) {
|
|
242
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
243
|
+
const line = new RegExp(`^\\s*(?:#{1,6}\\s*)?(?:Toolroll\\s+)?(?:Release\\s+)?v?${escaped}\\s*[:—–-]\\s*(.+?)\\s*$`, "im").exec(notes);
|
|
244
|
+
const summary = line?.[1]?.replace(/[*_`]/g, "").replace(/\s+/g, " ").replace(/[.\s]+$/, "").trim() ?? "";
|
|
245
|
+
if (summary === "")
|
|
246
|
+
return null;
|
|
247
|
+
return summary.length <= 80 ? summary : `${summary.slice(0, 79).replace(/[\s,;:]+\S*$/, "")}…`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* A deploy says one thing in chat: "Toolroll 0.9.9 is live." (with the release's own one-line summary when its
|
|
251
|
+
* notes have one). Once per version, addressed to each instance operator, and only when a worker on this
|
|
252
|
+
* computer last ran an older version: a fresh install is not news. Call before recording this runner's version.
|
|
253
|
+
*/
|
|
254
|
+
export function notifyVersionLive(store, dir, version, now) {
|
|
255
|
+
const before = runnerVersions(dir);
|
|
256
|
+
if (!VERSION.test(version) || !before.some(one => isNewer(version, one.version)) || before.some(one => isNewer(one.version, version)))
|
|
257
|
+
return 0;
|
|
258
|
+
const cached = cachedRelease(dir);
|
|
259
|
+
const summary = cached?.version === version ? releaseSummary(cached.notes, version) : null;
|
|
260
|
+
const line = summary === null ? `Toolroll ${version} is live.` : `Toolroll ${version} is live: ${summary}.`;
|
|
261
|
+
let queued = 0;
|
|
262
|
+
for (const recipient of store.accountFacts().map(one => one.name).filter(name => store.isInstanceOperator(name))) {
|
|
263
|
+
if (store.enqueueNotification({ dedupeKey: `release:live:${version}:${recipient}`, kind: "version-live", subject: line, body: "",
|
|
264
|
+
recipient, source: { installation: true } }, now))
|
|
265
|
+
queued++;
|
|
266
|
+
}
|
|
267
|
+
return queued;
|
|
268
|
+
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import type { Store } from "./store.js";
|
|
7
7
|
/** The ledger action a person's (or lead's) completion of a result writes. */
|
|
8
8
|
export declare const COMPLETION_ACTION = "assignment handoff checked";
|
|
9
|
+
/** Whether a task's family is marked complete. Completion is recorded on the
|
|
10
|
+
* family's root against its current version's latest result, so the root, a
|
|
11
|
+
* superseded revision and the current version all read Complete together. */
|
|
12
|
+
export declare function familyCompleted(store: Store, taskId: string): boolean;
|
|
9
13
|
/** The files a build changed, renames on both sides; null when the inventory can't be read whole. */
|
|
10
14
|
export declare function changedFilesOf(store: Store, runId: number, root: string): string[] | null;
|
|
11
15
|
/** Everything a result's branch carries: this build's changes and every build of every task it revises (a
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
2
2
|
/** The ledger action a person's (or lead's) completion of a result writes. */
|
|
3
3
|
export const COMPLETION_ACTION = "assignment handoff checked";
|
|
4
|
+
/** Whether a task's family is marked complete. Completion is recorded on the
|
|
5
|
+
* family's root against its current version's latest result, so the root, a
|
|
6
|
+
* superseded revision and the current version all read Complete together. */
|
|
7
|
+
export function familyCompleted(store, taskId) {
|
|
8
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
9
|
+
return store.handle.prepare(`SELECT 1 AS hit FROM action_ledger INDEXED BY work_completion
|
|
10
|
+
WHERE task_id IN (?, ?) AND action = ? AND source = 'work'
|
|
11
|
+
AND run_id = (SELECT run.id FROM run INDEXED BY work_result WHERE run.task_ref = ?
|
|
12
|
+
AND run.finished_at IS NOT NULL AND run.role IN ('builder','scout') ORDER BY run.id DESC LIMIT 1) LIMIT 1`)
|
|
13
|
+
.get(taskId, family?.root.id ?? taskId, COMPLETION_ACTION, family?.current.refId ?? store.lookupRef(taskId)?.id ?? -1) !== undefined;
|
|
14
|
+
}
|
|
4
15
|
/** The files a build changed, renames on both sides; null when the inventory can't be read whole. */
|
|
5
16
|
export function changedFilesOf(store, runId, root) {
|
|
6
17
|
const stat = store.artifactsFor(runId).find(one => one.kind === "diff-stat");
|
package/dist/review-context.d.ts
CHANGED
|
@@ -121,6 +121,9 @@ export type CriterionContextCoverage = {
|
|
|
121
121
|
items: string[];
|
|
122
122
|
gaps: string[];
|
|
123
123
|
priorSupport: "eligible" | "invalid" | "none";
|
|
124
|
+
/** One line for the change's images and other binary files, which are
|
|
125
|
+
* never read as text and so are neither items nor gaps. */
|
|
126
|
+
assets?: string;
|
|
124
127
|
};
|
|
125
128
|
export type ReviewContextInventory = {
|
|
126
129
|
schema: 1 | 2 | 3;
|