toolroll 0.9.60 → 0.9.62
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 +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/maintenance.js
CHANGED
|
@@ -29,7 +29,11 @@ export const REMOTE_ARGUMENTS = new Map([
|
|
|
29
29
|
['task block', args('id:task', 'key', { on: 'task' })],
|
|
30
30
|
['task unblock', args('id:task', 'key', { on: 'task' })],
|
|
31
31
|
['task next', args('id:task', 'key undo')],
|
|
32
|
-
|
|
32
|
+
// Live steering: --now pauses the running Claude build first; pause and continue name the exact attempt like stop and
|
|
33
|
+
// resume, and a repeated --request (or --key) id is the same request.
|
|
34
|
+
['task steer', args('id:task', 'note now request key', { run: 'run' })],
|
|
35
|
+
['task pause', args('id:task', 'request key', { run: 'run' })],
|
|
36
|
+
['task continue', args('id:task', 'request key', { run: 'run' })],
|
|
33
37
|
['task assign', args('id:task', 'key runner anyone')],
|
|
34
38
|
['task scope', args('id:task', 'key goal not touches budget-usd risk acceptance candidate model provider repair-model')],
|
|
35
39
|
['task plan', args('id:task', 'key provider model')],
|
package/dist/operate-remote.js
CHANGED
|
@@ -37,7 +37,7 @@ const references = { kind: "references" }, listing = { kind: "listing" };
|
|
|
37
37
|
export const REMOTE_SCOPES = new Map([
|
|
38
38
|
["status", listing], ["integrations", installation], ["ready", listing], ["grants", installation], ["sync", installation],
|
|
39
39
|
["gaps", repo], ["task add", repo],
|
|
40
|
-
...["ask", "checks", "add-tests", "show", "wait", "complete", "revise", "state", "block", "unblock", "next", "steer", "assign", "scope", "plan", "hold",
|
|
40
|
+
...["ask", "checks", "add-tests", "show", "wait", "complete", "revise", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "scope", "plan", "hold",
|
|
41
41
|
"unhold", "require", "requeue", "review", "repair", "route", "reopen", "stop", "resume"].map(action => [`task ${action}`, references]),
|
|
42
42
|
["check-progress", references],
|
|
43
43
|
["task list", listing],
|
package/dist/operate.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ type Args = {
|
|
|
102
102
|
* exports are behavior, not commentary — and the declared command guide
|
|
103
103
|
* (surface.ts) is tested for exact equality against them.
|
|
104
104
|
*/
|
|
105
|
-
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask", "checks", "add-tests"];
|
|
105
|
+
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask", "checks", "add-tests"];
|
|
106
106
|
export declare const PUBLISH_ACTIONS: readonly ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
107
107
|
export declare const CONFIG_ACTIONS: readonly ["show", "set", "clear"];
|
|
108
108
|
export declare const APPROVER_ACTIONS: readonly ["list", "add"];
|
package/dist/operate.js
CHANGED
|
@@ -18,8 +18,12 @@ import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } f
|
|
|
18
18
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
19
19
|
import { CLEANUP_CHOICES, bytesWords, cleanupWords, diskBytes, parseCleanup, storageReport } from "./storage.js";
|
|
20
20
|
import { removeStaleTestTemp, tempRoots, testTempFolders } from "./test-temp.js";
|
|
21
|
-
import {
|
|
21
|
+
import { storageSweepSupervisor } from "./storage-sweep-process.js";
|
|
22
|
+
import { idleStorageSweep, sweepSchedule, sweepScheduleWords } from "./storage-sweep-schedule.js";
|
|
23
|
+
import { lastSweep, saveSweep, sweepDetails, sweepWords as storageSweepWords } from "./storage-sweep.js";
|
|
22
24
|
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
25
|
+
import { cleanLandedBacklog, gitOf, landedBacklog, reconcileLanded, BACKLOG_DAYS } from "./landed-cleanup.js";
|
|
26
|
+
import { RemoteMains } from "./remote-main.js";
|
|
23
27
|
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
24
28
|
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
25
29
|
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
@@ -180,6 +184,7 @@ import { createInterface } from "node:readline";
|
|
|
180
184
|
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
|
|
181
185
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
182
186
|
import { WorktreePool } from "./worktree.js";
|
|
187
|
+
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
183
188
|
import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
184
189
|
import { worktreeAdoptionNotice } from "./worktree-notices.js";
|
|
185
190
|
import { workIndexPage, workIndexTask, WorkIndexCursorError } from "./work-index.js";
|
|
@@ -254,6 +259,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
254
259
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
255
260
|
toolroll storage where the disk goes: database, build checkouts, releases, evidence, leftover test temp folders
|
|
256
261
|
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay) and test temp folders older than a day; --yes removes them
|
|
262
|
+
toolroll storage clean --landed [--yes] preview work that landed in main over a week ago: marks its results complete, removes their clean checkouts and local branches; --yes does it (instance operator)
|
|
257
263
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
258
264
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
259
265
|
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
@@ -309,9 +315,17 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
309
315
|
back in filing order
|
|
310
316
|
toolroll task steer <id> --note "..."
|
|
311
317
|
guidance for the agent — a running
|
|
312
|
-
build
|
|
313
|
-
|
|
314
|
-
|
|
318
|
+
Claude build gets it in seconds,
|
|
319
|
+
another running build reads it when
|
|
320
|
+
it next checks for notes, otherwise
|
|
321
|
+
the next attempt does; --now pauses
|
|
322
|
+
the running Claude build first
|
|
323
|
+
toolroll task pause <id> [--run <id>] stop a running Claude build's
|
|
324
|
+
current turn and keep the build; the
|
|
325
|
+
next note continues it (Stop ends it)
|
|
326
|
+
toolroll task continue <id> [--run <id>]
|
|
327
|
+
resume a paused Claude build where it
|
|
328
|
+
stopped, without a note
|
|
315
329
|
toolroll task assign <id> --runner <name> | --anyone
|
|
316
330
|
reserve it for one worker (it joins
|
|
317
331
|
the back of that worker's queue) or
|
|
@@ -600,7 +614,7 @@ Exit codes
|
|
|
600
614
|
* (surface.ts) is tested for exact equality against them.
|
|
601
615
|
*/
|
|
602
616
|
export const TASK_ACTIONS = [
|
|
603
|
-
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign",
|
|
617
|
+
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign",
|
|
604
618
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
605
619
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask",
|
|
606
620
|
"checks", "add-tests",
|
|
@@ -653,6 +667,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
653
667
|
"access", "projects", "overlap",
|
|
654
668
|
// audit: a token by name where --token is a credential (remote).
|
|
655
669
|
"token-name",
|
|
670
|
+
// task pause / task steer --now: the asker's own request id, so a repeat is the same request.
|
|
671
|
+
"request",
|
|
656
672
|
]);
|
|
657
673
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
658
674
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
|
|
@@ -666,6 +682,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
666
682
|
"saved",
|
|
667
683
|
// task complete: also open the result's pull request.
|
|
668
684
|
"pull-request",
|
|
685
|
+
// storage clean --landed: work already in main.
|
|
686
|
+
"landed",
|
|
669
687
|
// up: the exact start-up details (containment) in the terminal too.
|
|
670
688
|
"verbose",
|
|
671
689
|
// lead token: end the lead's credential.
|
|
@@ -676,6 +694,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
676
694
|
"also",
|
|
677
695
|
// tokens: the person's password, piped (never an argument).
|
|
678
696
|
"password-stdin",
|
|
697
|
+
// task steer: pause the running Claude build, then send the note.
|
|
698
|
+
"now",
|
|
679
699
|
]);
|
|
680
700
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
681
701
|
const positional = [];
|
|
@@ -1035,6 +1055,19 @@ function telegramConversation(context, options = {}) {
|
|
|
1035
1055
|
phoneOrigin: () => phoneOrigin(process.env, dirname(context.databaseFile), { serverOrigin: options.serverOrigin ?? null }),
|
|
1036
1056
|
};
|
|
1037
1057
|
}
|
|
1058
|
+
/** The attempt's history says where it started when that is news: moved to current main, or kept on an older one. */
|
|
1059
|
+
function noteBaseStanding(store, runId, standing, main, now) {
|
|
1060
|
+
if (standing == null)
|
|
1061
|
+
return;
|
|
1062
|
+
const commits = (count) => `${count} commit${count === 1 ? "" : "s"}`;
|
|
1063
|
+
if (standing.moved !== null) {
|
|
1064
|
+
store.addRunNote(runId, "Toolroll", `Started from current ${main} (${standing.main.slice(0, 8)}). The branch had no commits or changes of its own and was ${commits(standing.moved.behind)} behind at ${standing.moved.from.slice(0, 8)}.`, now);
|
|
1065
|
+
}
|
|
1066
|
+
else if (standing.kept !== null && standing.behind > 0) {
|
|
1067
|
+
const why = standing.kept === "has commits" ? "it has commits of its own" : standing.kept === "has changes" ? "it has uncommitted changes" : "its checkout is not on the task branch";
|
|
1068
|
+
store.addRunNote(runId, "Toolroll", `Based on an older ${main}: ${standing.head.slice(0, 8)} is ${commits(standing.behind)} behind. It was not moved because ${why}.`, now);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1038
1071
|
/** A revision's verified start: its source build's sealed head, while the revision brief still binds that head and scope. */
|
|
1039
1072
|
function revisionSourceHead(store, evidenceRoot, taskRef, revisionOf) {
|
|
1040
1073
|
const source = store.revisionSourceOf(taskRef);
|
|
@@ -1720,9 +1753,9 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1720
1753
|
if (name === undefined || given === undefined || positional.length > 3) {
|
|
1721
1754
|
return fail(write, json, "runner capacity", "usage", "`runner capacity <name> <n> --as <you> --token <t>`", EXIT.usage);
|
|
1722
1755
|
}
|
|
1723
|
-
const capacity = parseRunnerCapacity(given);
|
|
1756
|
+
const capacity = parseRunnerCapacity(given, { allowZero: true });
|
|
1724
1757
|
if (capacity === null) {
|
|
1725
|
-
return fail(write, json, "runner capacity", "usage", `capacity is a whole number of tasks,
|
|
1758
|
+
return fail(write, json, "runner capacity", "usage", `capacity is a whole number of tasks, 0 to ${RUNNER_CAPACITY_MAX}; 0 stops new builds`, EXIT.usage);
|
|
1726
1759
|
}
|
|
1727
1760
|
const acting = await askCredentials(flags, context);
|
|
1728
1761
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
@@ -1734,6 +1767,12 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1734
1767
|
return fail(write, json, "runner capacity", changed.reason, changed.reason === "retired" ? `${name} is retired` : `no runner \`${name}\``, EXIT.refused);
|
|
1735
1768
|
}
|
|
1736
1769
|
const running = store.liveClaimCount(name, now);
|
|
1770
|
+
if (changed.after === 0) {
|
|
1771
|
+
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
1772
|
+
`${name} takes no new builds. ${running === 0 ? "Nothing is running." : `${running} running task(s) will finish.`}`,
|
|
1773
|
+
`Resume with: toolroll runner capacity ${name} <1-${RUNNER_CAPACITY_MAX}>`,
|
|
1774
|
+
]);
|
|
1775
|
+
}
|
|
1737
1776
|
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
1738
1777
|
changed.before === changed.after
|
|
1739
1778
|
? `${name} already runs up to ${changed.after} at once.`
|
|
@@ -2705,6 +2744,8 @@ async function tickCommand(flags, context) {
|
|
|
2705
2744
|
// A plan branch from an earlier plan (its checkout removed by storage retention) is reused, as its kept checkout was.
|
|
2706
2745
|
reuseBranch: true,
|
|
2707
2746
|
reclaim: { evidenceRoot: context.evidenceRoot },
|
|
2747
|
+
// Each plan starts from current main unless it revises earlier work, which keeps that work's head.
|
|
2748
|
+
...freshBaseFor(store, id, planBranch, base),
|
|
2708
2749
|
});
|
|
2709
2750
|
if (!planLeased.ok) {
|
|
2710
2751
|
release(store, lease, clock());
|
|
@@ -2728,6 +2769,7 @@ async function tickCommand(flags, context) {
|
|
|
2728
2769
|
now: clock(),
|
|
2729
2770
|
...(planStamp === null ? {} : { route: planStamp }),
|
|
2730
2771
|
});
|
|
2772
|
+
noteBaseStanding(store, planRunId, planLeased.base, base, clock());
|
|
2731
2773
|
// The source is RECORDED on the attempt before the brief is even
|
|
2732
2774
|
// composed: the exact bytes the planner reads, sealed as evidence,
|
|
2733
2775
|
// and the filed scope's digest stamped as what this run was proved
|
|
@@ -3031,6 +3073,7 @@ async function tickCommand(flags, context) {
|
|
|
3031
3073
|
continue;
|
|
3032
3074
|
}
|
|
3033
3075
|
}
|
|
3076
|
+
// An existing branch with no commits of its own starts from current main (a new one already does).
|
|
3034
3077
|
const leased = await worktrees.lease({
|
|
3035
3078
|
repo,
|
|
3036
3079
|
branch,
|
|
@@ -3039,6 +3082,7 @@ async function tickCommand(flags, context) {
|
|
|
3039
3082
|
now: clock(),
|
|
3040
3083
|
...(exists.code === 0 ? {} : { base: buildBase }),
|
|
3041
3084
|
reclaim: { evidenceRoot: context.evidenceRoot },
|
|
3085
|
+
...(exists.code === 0 ? freshBaseFor(store, id, branch, base) : {}),
|
|
3042
3086
|
});
|
|
3043
3087
|
if (!leased.ok) {
|
|
3044
3088
|
// The task is fine; this machine's pool is not. Hand the claim back so
|
|
@@ -3099,6 +3143,7 @@ async function tickCommand(flags, context) {
|
|
|
3099
3143
|
dispatched.push({ id, outcome: "skipped", reason: "admission-refused", detail: error instanceof Error ? error.message : String(error) });
|
|
3100
3144
|
continue;
|
|
3101
3145
|
}
|
|
3146
|
+
noteBaseStanding(store, runId, leased.base, base, clock());
|
|
3102
3147
|
if (leased.resumedFromRun !== undefined) {
|
|
3103
3148
|
store.addRunNote(runId, "Toolroll", `Recovered the ${leased.recoveryKind === "completed" ? "completed source draft" : "work-in-progress draft"} from interrupted attempt #${leased.resumedFromRun}. This fresh attempt is reviewing and verifying it; the safety patch is retained.`, clock());
|
|
3104
3149
|
}
|
|
@@ -3422,6 +3467,19 @@ async function reconcileCommand(flags, context) {
|
|
|
3422
3467
|
reaped: reaped.map(claim => claim.leaseId),
|
|
3423
3468
|
});
|
|
3424
3469
|
}
|
|
3470
|
+
// Landed work (landed-cleanup.ts): finish teardowns a crash cut short, then mark newly landed results complete (a
|
|
3471
|
+
// ledger entry says why) and remove their clean checkouts and local build and plan branches.
|
|
3472
|
+
let landed = { completed: [], steps: [] };
|
|
3473
|
+
// Completion trusts only the recorded remote's main, fetched by this process into a private folder it removes after.
|
|
3474
|
+
const remotes = new RemoteMains(store, context.gitRunner, context.evidenceRoot);
|
|
3475
|
+
try {
|
|
3476
|
+
landed = await reconcileLanded(store, worktrees, gitOf(context.gitRunner), remotes, repo, context.evidenceRoot, clock);
|
|
3477
|
+
}
|
|
3478
|
+
catch { /* the next pass tries again */ }
|
|
3479
|
+
finally {
|
|
3480
|
+
remotes.close();
|
|
3481
|
+
}
|
|
3482
|
+
const landedGone = landed.steps.filter(one => one.outcome === "removed");
|
|
3425
3483
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
3426
3484
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
3427
3485
|
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
@@ -3438,14 +3496,19 @@ async function reconcileCommand(flags, context) {
|
|
|
3438
3496
|
retention = null; /* the next pass tries again */
|
|
3439
3497
|
}
|
|
3440
3498
|
const retained = retention === null ? [] : retention.counts.filter(one => one.count > 0);
|
|
3441
|
-
// The
|
|
3499
|
+
// The storage sweep (storage-sweep.ts): stale test temp folders, orphans Toolroll left running, finished
|
|
3442
3500
|
// checkouts, extra staged runtimes and dependency installs unused for a week; saved for Settings → Storage.
|
|
3501
|
+
// Only the long-lived service (its watch passes `sweepOwner`) starts one, once due and while nothing is running or
|
|
3502
|
+
// waiting (storage-sweep-schedule.ts). A one-shot reconcile would exit and lose the claimed sweep, so it leaves it.
|
|
3503
|
+
// Launch only: report a result here only if it already completed. Later results save independently.
|
|
3443
3504
|
let storageSwept = null;
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3505
|
+
if (context.sweepOwner !== undefined) {
|
|
3506
|
+
try {
|
|
3507
|
+
storageSwept = idleStorageSweep(store, { databaseFile: context.databaseFile, pool: worktrees, tempRoots: context.tempRoots ?? tempRoots(), owner: context.sweepOwner }, clock);
|
|
3508
|
+
}
|
|
3509
|
+
catch {
|
|
3510
|
+
storageSwept = null; /* the next pass tries again */
|
|
3511
|
+
}
|
|
3449
3512
|
}
|
|
3450
3513
|
const swept = storageSwept === null ? [] : storageSwept.parts.filter(one => one.count > 0);
|
|
3451
3514
|
const nothing = retained.length === 0 &&
|
|
@@ -3458,6 +3521,8 @@ async function reconcileCommand(flags, context) {
|
|
|
3458
3521
|
closedRuns.length === 0 &&
|
|
3459
3522
|
closedCheckouts.length === 0 &&
|
|
3460
3523
|
slimmed.slimmed.length === 0 &&
|
|
3524
|
+
landed.completed.length === 0 &&
|
|
3525
|
+
landedGone.length === 0 &&
|
|
3461
3526
|
liveSwept.removed.length === 0;
|
|
3462
3527
|
return succeed(write, json, "reconcile", {
|
|
3463
3528
|
recovered,
|
|
@@ -3468,6 +3533,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3468
3533
|
checkoutsKept: pruned.kept,
|
|
3469
3534
|
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) }))],
|
|
3470
3535
|
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
3536
|
+
landed: { completed: landed.completed, steps: landed.steps },
|
|
3471
3537
|
liveViewsSwept: liveSwept.removed.length,
|
|
3472
3538
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
3473
3539
|
storageSweep: storageSwept,
|
|
@@ -3481,6 +3547,8 @@ async function reconcileCommand(flags, context) {
|
|
|
3481
3547
|
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} finished task checkout(s), about ${bytesWords(pruned.freed)}; their branches stay.`]),
|
|
3482
3548
|
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
3483
3549
|
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
3550
|
+
...(landed.completed.length === 0 ? [] : [`Marked ${landed.completed.length} landed result(s) complete: ${landed.completed.join(", ")}.`]),
|
|
3551
|
+
...(landedGone.length === 0 ? [] : [`Removed landed work: ${landedGone.filter(one => one.kind === "checkout").length} checkout(s), ${landedGone.filter(one => one.kind === "branch").length} local branch(es).`]),
|
|
3484
3552
|
...(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))}.`]),
|
|
3485
3553
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
3486
3554
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
@@ -4346,8 +4414,10 @@ async function providersCommand(flags, context) {
|
|
|
4346
4414
|
}
|
|
4347
4415
|
write(` isolation ${audit.isolation.flag === null
|
|
4348
4416
|
? "no hermetic flag"
|
|
4349
|
-
:
|
|
4350
|
-
|
|
4417
|
+
: audit.isolation.enforced
|
|
4418
|
+
? `builds run in their own setup (${audit.isolation.flag}) — none of your own hooks, plugins or MCP servers; reviews keep theirs`
|
|
4419
|
+
: `${audit.isolation.flag} exists, not passed${audit.isolation.resumeSafe === false ? " — it would break session resume" : audit.isolation.resumeSafe === null ? " — its effect on resume is unvalidated" : ""}`}`);
|
|
4420
|
+
write(` ${audit.isolation.enforced ? "builds get " : "config surface"} ${audit.configSurface.join("; ")}`);
|
|
4351
4421
|
write("");
|
|
4352
4422
|
}
|
|
4353
4423
|
write(" \u2192 toolroll config show which provider each phase actually resolves to");
|
|
@@ -5984,7 +6054,8 @@ async function runWatchLoop(args) {
|
|
|
5984
6054
|
if (!Number.isSafeInteger(reconcileEveryMs) || reconcileEveryMs < 1 || reconcileEveryMs > 2_147_483_647) {
|
|
5985
6055
|
throw new Error("--reconcile-every must be an integer from 1 to 2147483647 milliseconds");
|
|
5986
6056
|
}
|
|
5987
|
-
|
|
6057
|
+
// The owner carries lifetime without changing the lease schema. Bounded watches cannot host automatic sweeps.
|
|
6058
|
+
const incarnation = `${runFor === null ? "toolroll-service:" : ""}${randomUUID()}`;
|
|
5988
6059
|
const lease = acquireWatchLeaseAuthed(store, { runner, token, repo, owner: incarnation, ttlMs: WATCH_LEASE_MS }, new Date());
|
|
5989
6060
|
if (!lease.ok) {
|
|
5990
6061
|
return {
|
|
@@ -6105,8 +6176,10 @@ async function runWatchLoop(args) {
|
|
|
6105
6176
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
6106
6177
|
// Reconciliation has its own cadence and sink. Sharing tick's sink would
|
|
6107
6178
|
// interleave envelopes while a build awaits its provider or project check.
|
|
6179
|
+
const sweepOwner = {};
|
|
6108
6180
|
const maintenance = startMaintenance({
|
|
6109
6181
|
intervalMs: reconcileEveryMs,
|
|
6182
|
+
onStop: () => storageSweepSupervisor(context.databaseFile).stop(sweepOwner),
|
|
6110
6183
|
shouldStop: stopping,
|
|
6111
6184
|
onError: error => {
|
|
6112
6185
|
reconciliationReady = false;
|
|
@@ -6117,7 +6190,9 @@ async function runWatchLoop(args) {
|
|
|
6117
6190
|
},
|
|
6118
6191
|
run: async () => {
|
|
6119
6192
|
const maintenanceLines = [];
|
|
6120
|
-
const maintenanceContext = { ...quietContext, write: line => maintenanceLines.push(line) };
|
|
6193
|
+
const maintenanceContext = { ...quietContext, ...(runFor === null ? { sweepOwner } : {}), write: line => maintenanceLines.push(line) };
|
|
6194
|
+
if (runFor !== null)
|
|
6195
|
+
delete maintenanceContext.sweepOwner;
|
|
6121
6196
|
const code = await reconcileCommand(passFlags(), maintenanceContext);
|
|
6122
6197
|
if (code !== EXIT.ok)
|
|
6123
6198
|
throw new Error(maintenanceLines.join(" "));
|
|
@@ -7171,6 +7246,9 @@ async function upCommand(flags, context) {
|
|
|
7171
7246
|
}
|
|
7172
7247
|
const loopFlagsFor = (repo) => {
|
|
7173
7248
|
const copy = new Map();
|
|
7249
|
+
// A bounded `up` must not advertise its child watches as an unbounded sweep service either.
|
|
7250
|
+
if (forGiven !== undefined)
|
|
7251
|
+
copy.set("for", forGiven);
|
|
7174
7252
|
copy.set("runner", runnerName);
|
|
7175
7253
|
copy.set("token", runnerToken);
|
|
7176
7254
|
copy.set("repo", repo);
|
|
@@ -8472,7 +8550,7 @@ function taskCommand(positional, flags, context) {
|
|
|
8472
8550
|
return taskAction(action, rest, flags, context);
|
|
8473
8551
|
}
|
|
8474
8552
|
/** Task verbs that are the lead working on a task, not reading it. */
|
|
8475
|
-
const LEAD_WORK_VERBS = new Set(["state", "steer", "reopen", "requeue", "regate", "repair", "resume", "stop", "scope", "approve", "plan", "checks", "revise", "hold", "unhold", "route", "require"]);
|
|
8553
|
+
const LEAD_WORK_VERBS = new Set(["state", "steer", "pause", "continue", "reopen", "requeue", "regate", "repair", "resume", "stop", "scope", "approve", "plan", "checks", "revise", "hold", "unhold", "route", "require"]);
|
|
8476
8554
|
function taskAction(action, rest, flags, context) {
|
|
8477
8555
|
switch (action) {
|
|
8478
8556
|
case "complete":
|
|
@@ -8513,6 +8591,10 @@ function taskAction(action, rest, flags, context) {
|
|
|
8513
8591
|
return nextTask(rest, flags, context);
|
|
8514
8592
|
case "steer":
|
|
8515
8593
|
return steerTask(rest, flags, context);
|
|
8594
|
+
case "pause":
|
|
8595
|
+
return pauseTask(rest, flags, context);
|
|
8596
|
+
case "continue":
|
|
8597
|
+
return continueTask(rest, flags, context);
|
|
8516
8598
|
case "assign":
|
|
8517
8599
|
return assignTask(rest, flags, context);
|
|
8518
8600
|
case "reopen":
|
|
@@ -9009,6 +9091,7 @@ function showTask(positional, context) {
|
|
|
9009
9091
|
const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
|
|
9010
9092
|
const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
|
|
9011
9093
|
const steeringLive = store.steeringLiveBuild(ref.id, now) !== null;
|
|
9094
|
+
const base = taskBaseOf(store, id);
|
|
9012
9095
|
const detail = {
|
|
9013
9096
|
task,
|
|
9014
9097
|
work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
|
|
@@ -9024,6 +9107,8 @@ function showTask(positional, context) {
|
|
|
9024
9107
|
scope,
|
|
9025
9108
|
approval: approvalOf(scope),
|
|
9026
9109
|
route: taskRouteView(routed, readiness),
|
|
9110
|
+
// Fresh base: the commit this task's work starts from, and how far current main has moved past it.
|
|
9111
|
+
base,
|
|
9027
9112
|
runs: runs.map(one => ({ ...one, route: store.runRoute(one.id) })),
|
|
9028
9113
|
deliverable: ref.deliverable,
|
|
9029
9114
|
report: readVerifiedReport(store, context.evidenceRoot, ref.id),
|
|
@@ -9080,6 +9165,7 @@ function showTask(positional, context) {
|
|
|
9080
9165
|
]
|
|
9081
9166
|
: [` report: ${detail.report.problem} (run ${detail.report.run})`]),
|
|
9082
9167
|
...pullRequestLines(detail.pullRequest),
|
|
9168
|
+
...(base === null ? [] : [` base: ${baseStandingWords(base)}`]),
|
|
9083
9169
|
...(detail.blockedBy.length > 0 ? [` waits for ${detail.blockedBy.join(", ")}`] : []),
|
|
9084
9170
|
...(detail.position === null
|
|
9085
9171
|
? []
|
|
@@ -9454,6 +9540,9 @@ async function steerTask(positional, flags, context) {
|
|
|
9454
9540
|
if (!authed.ok) {
|
|
9455
9541
|
return fail(write, json, "task steer", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9456
9542
|
}
|
|
9543
|
+
// --now: Pause and steer — the running Claude build's turn stops, then this note continues it.
|
|
9544
|
+
if (flags.get("now") === true)
|
|
9545
|
+
return pauseRunningBuild("task steer", id, note, verifiedAuthor(acting.name), flags, context);
|
|
9457
9546
|
const filed = store.fileSteerNote(id, verifiedAuthor(acting.name), note, context.now, mutationFrom(flags, context.now));
|
|
9458
9547
|
if (!filed.ok) {
|
|
9459
9548
|
const detail = filed.reason === "unknown-task"
|
|
@@ -9465,8 +9554,87 @@ async function steerTask(positional, flags, context) {
|
|
|
9465
9554
|
}
|
|
9466
9555
|
const steered = store.lookupRef(id);
|
|
9467
9556
|
const live = steered !== null && store.steeringLiveBuild(steered.id, context.now) !== null;
|
|
9557
|
+
const liveInput = steered !== null && store.liveInputRun(steered.id, context.now) !== null;
|
|
9468
9558
|
return succeed(write, json, "task steer", { task: id, note: filed.id, standing: live ? "waiting" : "next-attempt" }, () => [
|
|
9469
|
-
`Noted. ${steerDestinationWords(live)} \`task show ${id}\` says when it has been read.`,
|
|
9559
|
+
`Noted. ${steerDestinationWords(live, liveInput)} \`task show ${id}\` says when it has been read.`,
|
|
9560
|
+
]);
|
|
9561
|
+
}
|
|
9562
|
+
/**
|
|
9563
|
+
* `toolroll task pause <id> [--run <n>] --as <you> --token <t>` (live steering): stop a running Claude build's
|
|
9564
|
+
* current turn and keep the build — its session waits for the next note. Never a stop: nothing ends, holds or
|
|
9565
|
+
* reclaims the attempt. Bound to the exact run (--run, or the one taking live input now); a repeated --request id
|
|
9566
|
+
* is the same request.
|
|
9567
|
+
*/
|
|
9568
|
+
async function pauseTask(positional, flags, context) {
|
|
9569
|
+
const { store, write, json } = context;
|
|
9570
|
+
const id = positional[0];
|
|
9571
|
+
if (id === undefined)
|
|
9572
|
+
return fail(write, json, "task pause", "usage", "`toolroll task pause <id> [--run <run-id>] --as <you> --token <t>`", EXIT.usage);
|
|
9573
|
+
const acting = await askCredentials(flags, context);
|
|
9574
|
+
if (acting === null)
|
|
9575
|
+
return fail(write, json, "task pause", "usage", "pausing takes `--as <you> --token <t>` — it is an operator's act and is recorded against your name", EXIT.usage);
|
|
9576
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9577
|
+
if (!authed.ok)
|
|
9578
|
+
return fail(write, json, "task pause", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9579
|
+
return pauseRunningBuild("task pause", id, undefined, verifiedAuthor(acting.name), flags, context);
|
|
9580
|
+
}
|
|
9581
|
+
/**
|
|
9582
|
+
* `toolroll task continue <id> [--run <n>] --as <you> --token <t>` (live steering): Resume a paused Claude build with a
|
|
9583
|
+
* short continue frame and no note. Bound to the exact run (--run, or the one taking live input now), and only while it
|
|
9584
|
+
* is paused; a repeated --request id is the same request.
|
|
9585
|
+
*/
|
|
9586
|
+
async function continueTask(positional, flags, context) {
|
|
9587
|
+
const { store, write, json } = context;
|
|
9588
|
+
const id = positional[0];
|
|
9589
|
+
if (id === undefined)
|
|
9590
|
+
return fail(write, json, "task continue", "usage", "`toolroll task continue <id> [--run <run-id>] --as <you> --token <t>`", EXIT.usage);
|
|
9591
|
+
const acting = await askCredentials(flags, context);
|
|
9592
|
+
if (acting === null)
|
|
9593
|
+
return fail(write, json, "task continue", "usage", "resuming takes `--as <you> --token <t>` — it is an operator's act and is recorded against your name", EXIT.usage);
|
|
9594
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9595
|
+
if (!authed.ok)
|
|
9596
|
+
return fail(write, json, "task continue", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9597
|
+
const ref = store.lookupRef(id);
|
|
9598
|
+
if (ref === null || store.getTask(id) === null)
|
|
9599
|
+
return fail(write, json, "task continue", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9600
|
+
const runText = text(flags, "run");
|
|
9601
|
+
const named = runText === undefined ? store.liveInputRun(ref.id, context.now) : Number(runText);
|
|
9602
|
+
if (named === null)
|
|
9603
|
+
return fail(write, json, "task continue", "not-live", `${id} has no running Claude build to resume`, EXIT.refused);
|
|
9604
|
+
if (!Number.isInteger(named) || named < 1)
|
|
9605
|
+
return fail(write, json, "task continue", "usage", "--run names one exact attempt by its number", EXIT.usage);
|
|
9606
|
+
const requestId = text(flags, "request") ?? text(flags, "key") ?? `cli-${randomUUID()}`;
|
|
9607
|
+
const asked = store.requestResume({ taskId: id, runId: named, by: verifiedAuthor(acting.name), via: "cli", requestId }, context.now);
|
|
9608
|
+
if (!asked.ok)
|
|
9609
|
+
return fail(write, json, "task continue", asked.reason, asked.detail, EXIT.refused, { run: named });
|
|
9610
|
+
return succeed(write, json, "task continue", { task: id, run: named, resume: asked.resume, repeated: asked.duplicate }, () => [
|
|
9611
|
+
asked.duplicate ? `Build #${named} of ${id} was already asked to resume with that request; nothing changed.`
|
|
9612
|
+
: `Resuming build #${named} of ${id} where it stopped.`,
|
|
9613
|
+
]);
|
|
9614
|
+
}
|
|
9615
|
+
/** Pause, or Pause and steer, through the store's one action; the words both CLI doors print. */
|
|
9616
|
+
function pauseRunningBuild(command, id, note, by, flags, context) {
|
|
9617
|
+
const { store, write, json } = context;
|
|
9618
|
+
const ref = store.lookupRef(id);
|
|
9619
|
+
if (ref === null || store.getTask(id) === null)
|
|
9620
|
+
return fail(write, json, command, "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9621
|
+
const runText = text(flags, "run");
|
|
9622
|
+
const named = runText === undefined ? store.liveInputRun(ref.id, context.now) : Number(runText);
|
|
9623
|
+
if (named === null) {
|
|
9624
|
+
return fail(write, json, command, "not-live", note === undefined
|
|
9625
|
+
? `${id} has no running Claude build to pause`
|
|
9626
|
+
: `${id} has no running Claude build to pause; send the note without --now`, EXIT.refused);
|
|
9627
|
+
}
|
|
9628
|
+
if (!Number.isInteger(named) || named < 1)
|
|
9629
|
+
return fail(write, json, command, "usage", "--run names one exact attempt by its number", EXIT.usage);
|
|
9630
|
+
const requestId = text(flags, "request") ?? text(flags, "key") ?? `cli-${randomUUID()}`;
|
|
9631
|
+
const asked = store.requestPause({ taskId: id, runId: named, by, via: "cli", requestId, ...(note === undefined ? {} : { note }) }, context.now);
|
|
9632
|
+
if (!asked.ok)
|
|
9633
|
+
return fail(write, json, command, asked.reason, asked.detail, EXIT.refused, { run: named });
|
|
9634
|
+
return succeed(write, json, command, { task: id, run: named, pause: asked.pause, note: asked.note, repeated: asked.duplicate }, () => [
|
|
9635
|
+
asked.duplicate ? `Build #${named} of ${id} was already asked to pause with that request; nothing changed.`
|
|
9636
|
+
: note === undefined ? `Pausing build #${named} of ${id}. Its next note continues it; \`task stop\` ends it.`
|
|
9637
|
+
: `Pausing build #${named} of ${id}, then sending your note. \`task show ${id}\` says when it has been read.`,
|
|
9470
9638
|
]);
|
|
9471
9639
|
}
|
|
9472
9640
|
/** The mirror of block: stop waiting. Removal cannot create a cycle. */
|
|
@@ -10991,7 +11159,7 @@ async function retentionCommand(positional, flags, context) {
|
|
|
10991
11159
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
10992
11160
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
10993
11161
|
}
|
|
10994
|
-
/** `storage`, `storage cleanup [<when>]`, `storage clean [--yes]`, `storage discard <path> --yes`: where the disk goes,
|
|
11162
|
+
/** `storage`, `storage cleanup [<when>]`, `storage clean [--landed] [--yes]`, `storage discard <path> --yes`: where the disk goes,
|
|
10995
11163
|
* and task checkouts: when a finished task's clean checkout is removed, cleaning up now, and throwing away one kept for
|
|
10996
11164
|
* its changes. Changing anything takes an instance operator. */
|
|
10997
11165
|
async function storageCommand(positional, flags, context) {
|
|
@@ -10999,7 +11167,7 @@ async function storageCommand(positional, flags, context) {
|
|
|
10999
11167
|
const command = `storage ${action ?? ""}`.trim();
|
|
11000
11168
|
const store = context.store;
|
|
11001
11169
|
const changes = (action === "cleanup" && rest.length > 0) || (action === "clean" && flags.has("yes")) || action === "discard";
|
|
11002
|
-
const allowed = new Set([...(changes ? ["as", "token", "token-file", "token-env"] : []), ...(action === "clean" || action === "discard" ? ["yes"] : []), "db", "json"]);
|
|
11170
|
+
const allowed = new Set([...(changes ? ["as", "token", "token-file", "token-env"] : []), ...(action === "clean" || action === "discard" ? ["yes"] : []), ...(action === "clean" ? ["landed"] : []), "db", "json"]);
|
|
11003
11171
|
for (const name of flags.keys())
|
|
11004
11172
|
if (!allowed.has(name))
|
|
11005
11173
|
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
@@ -11038,15 +11206,15 @@ async function storageCommand(positional, flags, context) {
|
|
|
11038
11206
|
const sweepFacts = () => {
|
|
11039
11207
|
const last = lastSweep(store);
|
|
11040
11208
|
const automatic = last?.source === "automatic" ? last : lastSweep(store, "automatic");
|
|
11041
|
-
return { last, lastAutomatic: automatic,
|
|
11209
|
+
return { last, lastAutomatic: automatic, schedule: sweepSchedule(store, context.clock()) };
|
|
11042
11210
|
};
|
|
11043
11211
|
const sweepLines = (facts) => {
|
|
11044
11212
|
const when = (at) => new Date(at).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
11045
11213
|
const one = facts.last;
|
|
11046
|
-
const head = one === null ?
|
|
11214
|
+
const head = one === null ? "Not swept yet."
|
|
11047
11215
|
: `Last swept ${when(one.at)}${one.source === "manual" ? ` by ${one.actor}` : " automatically"}: ${storageSweepWords(one.parts)}`;
|
|
11048
11216
|
const auto = one !== null && one.source === "manual" && facts.lastAutomatic !== null ? [`Last automatic sweep ${when(facts.lastAutomatic.at)}: ${storageSweepWords(facts.lastAutomatic.parts)}`] : [];
|
|
11049
|
-
return [head, ...auto, ...(one === null ? [] : sweepDetails(one.parts).slice(0, 8).map(line => ` ${line}`))];
|
|
11217
|
+
return [head, ...auto, `Automatic sweep: ${sweepScheduleWords(facts.schedule, when)}`, ...(one === null ? [] : sweepDetails(one.parts).slice(0, 8).map(line => ` ${line}`))];
|
|
11050
11218
|
};
|
|
11051
11219
|
if (action === undefined) {
|
|
11052
11220
|
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
@@ -11077,6 +11245,11 @@ async function storageCommand(positional, flags, context) {
|
|
|
11077
11245
|
const saved = store.setCheckoutCleanup(cleanup, actor, context.clock());
|
|
11078
11246
|
return succeed(context.write, context.json, command, { cleanup: saved }, () => [`Saved. A finished task's clean checkout is removed ${cleanupWords(saved)}; its branch stays.`]);
|
|
11079
11247
|
}
|
|
11248
|
+
if (action === "clean" && flags.has("landed")) {
|
|
11249
|
+
if (rest.length > 0 || flags.get("landed") !== true)
|
|
11250
|
+
return fail(context.write, context.json, "storage clean --landed", "usage", "Use storage clean --landed to preview, then storage clean --landed --yes.", EXIT.usage);
|
|
11251
|
+
return landedCommand(flags.has("yes"), operator, refused, pool, context);
|
|
11252
|
+
}
|
|
11080
11253
|
if (action === "clean") {
|
|
11081
11254
|
if (rest.length > 0)
|
|
11082
11255
|
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
@@ -11102,7 +11275,7 @@ async function storageCommand(positional, flags, context) {
|
|
|
11102
11275
|
return refused("cleans up checkouts");
|
|
11103
11276
|
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
11104
11277
|
const temp = leftovers(true);
|
|
11105
|
-
const swept = removeStaleTestTemp(roots, context.clock());
|
|
11278
|
+
const swept = await removeStaleTestTemp(roots, context.clock());
|
|
11106
11279
|
if (swept.removed.length > 0) {
|
|
11107
11280
|
store.recordAction({ at: context.clock().toISOString(), actor, repo: null, taskId: null, runId: null, action: "test temp folders removed", outcome: "removed", source: "request",
|
|
11108
11281
|
detail: `${swept.removed.length} older than a day, about ${bytesWords(temp.staleBytes)}${swept.failed.length === 0 ? "" : `; ${swept.failed.length} couldn't be removed`}` });
|
|
@@ -11150,7 +11323,57 @@ async function storageCommand(positional, flags, context) {
|
|
|
11150
11323
|
return fail(context.write, context.json, command, "refused", done.message, EXIT.refused);
|
|
11151
11324
|
return succeed(context.write, context.json, command, { discarded: target, freedBytes: done.bytes }, () => [`Discarded ${target}, about ${bytesWords(done.bytes)}; its branch stays.`]);
|
|
11152
11325
|
}
|
|
11153
|
-
return fail(context.write, context.json, command, "usage", "Use storage, storage cleanup <when>, storage clean [--yes] or storage discard <path> --yes.", EXIT.usage);
|
|
11326
|
+
return fail(context.write, context.json, command, "usage", "Use storage, storage cleanup <when>, storage clean [--landed] [--yes] or storage discard <path> --yes.", EXIT.usage);
|
|
11327
|
+
}
|
|
11328
|
+
/** `storage clean --landed [--yes]`: the one-time pass over work that piled up before reconcile did this on its own.
|
|
11329
|
+
* Results whose work landed at least a week ago are marked complete (a ledger entry says why) and their clean
|
|
11330
|
+
* checkouts and local branches go; so do Toolroll's own branches no task uses whose work landed. A preview changes
|
|
11331
|
+
* nothing; --yes takes an instance operator, records every teardown first, then carries it out. */
|
|
11332
|
+
async function landedCommand(yes, operator, refused, pool, context) {
|
|
11333
|
+
// Completion trusts only the recorded remote's main, fetched by this process into a private folder it removes after.
|
|
11334
|
+
const remotes = new RemoteMains(context.store, context.gitRunner, context.evidenceRoot);
|
|
11335
|
+
try {
|
|
11336
|
+
return await landedCommandWith(remotes, yes, operator, refused, pool, context);
|
|
11337
|
+
}
|
|
11338
|
+
finally {
|
|
11339
|
+
remotes.close();
|
|
11340
|
+
}
|
|
11341
|
+
}
|
|
11342
|
+
async function landedCommandWith(remotes, yes, operator, refused, pool, context) {
|
|
11343
|
+
const command = "storage clean --landed";
|
|
11344
|
+
const git = gitOf(context.gitRunner);
|
|
11345
|
+
const plurals = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
11346
|
+
const keepLine = (one) => ` ${projectName(one.repo)} ${one.what}: ${one.why}`;
|
|
11347
|
+
const stepLine = (one) => ` ${projectName(one.repo)} ${one.target}${one.why === null ? "" : `: ${one.why}`}`;
|
|
11348
|
+
const counts = (plan) => ({
|
|
11349
|
+
results: plan.results.length, complete: plan.results.filter(one => one.completes).length,
|
|
11350
|
+
checkouts: plan.results.reduce((sum, one) => sum + one.targets.filter(target => target.kind === "checkout").length, 0) + plan.branches.filter(one => one.checkout !== null).length,
|
|
11351
|
+
branches: plan.results.reduce((sum, one) => sum + one.targets.filter(target => target.kind === "branch").length, 0) + plan.branches.length,
|
|
11352
|
+
});
|
|
11353
|
+
if (!yes) {
|
|
11354
|
+
const plan = await landedBacklog(context.store, git, remotes, context.evidenceRoot, context.clock());
|
|
11355
|
+
const n = counts(plan);
|
|
11356
|
+
const anything = n.results + plan.branches.length + plan.pending > 0;
|
|
11357
|
+
return succeed(context.write, context.json, command, { preview: true, cutoff: plan.cutoff, results: plan.results, branches: plan.branches, kept: plan.kept, pending: plan.pending }, () => [
|
|
11358
|
+
anything ? `Would mark ${plurals(n.complete, "landed result")} complete and remove ${plurals(n.checkouts, "checkout")} and ${plurals(n.branches, "local branch", "local branches")}.` : `Nothing landed more than ${BACKLOG_DAYS} days ago is left to clean up.`,
|
|
11359
|
+
...plan.results.map(one => ` ${projectName(one.repo)} ${one.taskId}: ${one.reason}${one.completes ? "" : " (already complete)"}`),
|
|
11360
|
+
...plan.branches.map(one => ` ${projectName(one.repo)} ${one.branch}: ${one.reason}`),
|
|
11361
|
+
...(plan.pending === 0 ? [] : [`${plurals(plan.pending, "item")} from an earlier clean-up will finish too.`]),
|
|
11362
|
+
...(plan.kept.length === 0 ? [] : [`Stays (${plan.kept.length}):`, ...plan.kept.map(keepLine)]),
|
|
11363
|
+
anything ? "Nothing was changed. Run toolroll storage clean --landed --yes to do it." : "Nothing was changed.",
|
|
11364
|
+
]);
|
|
11365
|
+
}
|
|
11366
|
+
const actor = await operator();
|
|
11367
|
+
if (actor === null)
|
|
11368
|
+
return refused("cleans up landed work");
|
|
11369
|
+
const plan = await landedBacklog(context.store, git, remotes, context.evidenceRoot, context.clock());
|
|
11370
|
+
const done = await cleanLandedBacklog(context.store, pool, git, remotes, context.evidenceRoot, context.clock, plan, actor);
|
|
11371
|
+
const gone = done.steps.filter(one => one.outcome === "removed" || one.outcome === "already gone");
|
|
11372
|
+
const stays = done.steps.filter(one => one.outcome === "waits" || one.outcome === "kept");
|
|
11373
|
+
return succeed(context.write, context.json, command, { completed: done.completed, steps: done.steps, refused: done.refused, kept: plan.kept }, () => [
|
|
11374
|
+
`Marked ${plurals(done.completed.length, "landed result")} complete. Removed ${plurals(gone.filter(one => one.kind === "checkout").length, "checkout")} and ${plurals(gone.filter(one => one.kind === "branch").length, "local branch", "local branches")}.`,
|
|
11375
|
+
...(stays.length + done.refused.length === 0 ? [] : [`Stays (${stays.length + done.refused.length}):`, ...stays.map(stepLine), ...done.refused.map(keepLine)]),
|
|
11376
|
+
]);
|
|
11154
11377
|
}
|
|
11155
11378
|
function checkoutSummary(plan) {
|
|
11156
11379
|
return { count: plan.count, totalBytes: plan.totalBytes, waitingReview: plan.waitingReview, withChanges: plan.withChanges, cleanBytes: plan.freeBytes, cleanCount: plan.go.length, cleanup: plan.cleanup };
|
package/dist/orphan-sweep.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export type OrphanOptions = StopOptions & {
|
|
|
57
57
|
tempRoots?: readonly string[];
|
|
58
58
|
locate?: (pids: readonly number[]) => Map<number, Located>;
|
|
59
59
|
self?: number;
|
|
60
|
+
/** A root live work uses (storage-sweep.ts): asked at both looks; true leaves everything in it running. */
|
|
61
|
+
spare?: (path: string) => boolean;
|
|
60
62
|
};
|
|
61
63
|
/** Stop the orphans Toolroll left (see the top of this file), each in the ledger. */
|
|
62
64
|
export declare function sweepOrphans(store: Store, now: () => Date, options?: OrphanOptions): Promise<OrphanSweep>;
|