toolroll 0.9.59 → 0.9.61
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 +26 -0
- package/README.md +8 -6
- 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 +2 -1
- package/dist/assignment-adapters.js +1 -1
- package/dist/assignment-delivery.d.ts +16 -0
- package/dist/assignment-delivery.js +54 -28
- package/dist/assignment-status.d.ts +14 -5
- package/dist/assignment-status.js +70 -39
- package/dist/assignment.d.ts +10 -2
- package/dist/assignment.js +21 -14
- package/dist/bin.js +11 -2
- package/dist/brief-fence.d.ts +9 -0
- package/dist/brief-fence.js +22 -0
- package/dist/browser/workspace.css +2 -2
- 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 +178 -29
- package/dist/chat-channel.js +10 -1
- 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-fast.d.ts +8 -0
- package/dist/cli-fast.js +18 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.js +91 -0
- package/dist/cli.d.ts +6 -5
- package/dist/cli.js +17 -96
- 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/design/tokens.css +234 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord.js +2 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +28 -0
- package/dist/evidence.js +87 -17
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/flow-actions.js +2 -1
- package/dist/guides.js +14 -11
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +157 -0
- package/dist/landed-cleanup.js +707 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.js +1 -1
- package/dist/lead-doors.js +49 -2
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +138 -88
- package/dist/lead-tools.js +48 -8
- 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/mailbox.js +3 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/mcp-core.js +1 -1
- 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 +201 -19
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-delete-ui.d.ts +11 -0
- package/dist/project-delete-ui.js +29 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +73 -8
- package/dist/provider.js +87 -17
- 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 +22 -3
- package/dist/server/chrome.d.ts +6 -3
- package/dist/server/chrome.js +8 -4
- package/dist/server/console.css +9 -64
- 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 +14 -2
- package/dist/server/render-tasks.d.ts +14 -2
- package/dist/server/render-tasks.js +38 -20
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +2 -1
- package/dist/server/task-views.js +10 -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/slack-socket.d.ts +12 -0
- package/dist/slack-socket.js +29 -0
- package/dist/slack.d.ts +0 -7
- package/dist/slack.js +2 -24
- 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-mailbox.d.ts +39 -0
- package/dist/steer-mailbox.js +207 -0
- package/dist/steer-status.d.ts +25 -0
- package/dist/steer-status.js +44 -0
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +41 -0
- package/dist/storage-sweep-process.js +187 -0
- package/dist/storage-sweep.d.ts +30 -6
- package/dist/storage-sweep.js +214 -28
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +16 -4
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +202 -2
- package/dist/store.js +440 -13
- package/dist/surface.js +5 -3
- package/dist/sweep-gate.d.ts +10 -0
- package/dist/sweep-gate.js +18 -0
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/team-updates.js +3 -3
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +15 -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 +3 -1
- package/dist/worktree.js +93 -10
- package/package.json +21 -21
- package/scripts/provider-canary.mjs +235 -147
package/dist/operate.js
CHANGED
|
@@ -10,14 +10,18 @@ import { runBatchChecks } from "./batch-checks.js";
|
|
|
10
10
|
import { BATCH_HINT, BATCH_WINDOW_MS, projectBatchChecks, setProjectBatchChecks } from "./batch-policy.js";
|
|
11
11
|
import { buildReviewPass } from "./build-review.js";
|
|
12
12
|
import { buildReviewLines, buildReviewOf } from "./review-switch.js";
|
|
13
|
+
import { steerDestinationWords, steerStandingOf, steerStandingWords } from "./steer-status.js";
|
|
14
|
+
import { utcMinute } from "./when-html.js";
|
|
13
15
|
import { parseProtectedPaths } from "./approval-policy.js";
|
|
14
16
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
15
17
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
16
18
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
17
19
|
import { CLEANUP_CHOICES, bytesWords, cleanupWords, diskBytes, parseCleanup, storageReport } from "./storage.js";
|
|
18
20
|
import { removeStaleTestTemp, tempRoots, testTempFolders } from "./test-temp.js";
|
|
21
|
+
import { storageSweepSupervisor } from "./storage-sweep-process.js";
|
|
19
22
|
import { dailyStorageSweep, lastSweep, nextSweepAt, saveSweep, storageSweepOff as storageSweepOffNow, sweepDetails, sweepWords as storageSweepWords } from "./storage-sweep.js";
|
|
20
23
|
import { removeShared, sharedDepsRoot, sharedUse, unusedShared } from "./shared-deps.js";
|
|
24
|
+
import { cleanLandedBacklog, gitOf, landedBacklog, reconcileLanded, BACKLOG_DAYS } from "./landed-cleanup.js";
|
|
21
25
|
import { checkoutPlan, cleanCheckouts, discardCheckout, finishedCheckouts, slimKeptCheckouts, whyWords } from "./checkout-cleanup.js";
|
|
22
26
|
import { closeOutCheckouts, closeOutRuns, stoppedWords } from "./run-closeout.js";
|
|
23
27
|
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodLabel, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
@@ -252,6 +256,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
252
256
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
253
257
|
toolroll storage where the disk goes: database, build checkouts, releases, evidence, leftover test temp folders
|
|
254
258
|
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay) and test temp folders older than a day; --yes removes them
|
|
259
|
+
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)
|
|
255
260
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
256
261
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
257
262
|
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
@@ -289,7 +294,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
289
294
|
toolroll task add-tests <id> file a small task to write tests for that change
|
|
290
295
|
toolroll task revise <id> --feedback "requested change"
|
|
291
296
|
toolroll assignment show <task> root, current work and exact handoff
|
|
292
|
-
toolroll assignment updates
|
|
297
|
+
toolroll assignment updates history of handoffs sent to people (--after <cursor>)
|
|
293
298
|
toolroll assignment brief catch up from the local database
|
|
294
299
|
toolroll assignment inbox --consumer <name> receive saved status changes
|
|
295
300
|
toolroll assignment ack --consumer <name> --batch <id> acknowledge delivery
|
|
@@ -306,9 +311,18 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
306
311
|
is still required); --undo puts it
|
|
307
312
|
back in filing order
|
|
308
313
|
toolroll task steer <id> --note "..."
|
|
309
|
-
guidance for the
|
|
310
|
-
|
|
311
|
-
running
|
|
314
|
+
guidance for the agent — a running
|
|
315
|
+
Claude build gets it in seconds,
|
|
316
|
+
another running build reads it when
|
|
317
|
+
it next checks for notes, otherwise
|
|
318
|
+
the next attempt does; --now pauses
|
|
319
|
+
the running Claude build first
|
|
320
|
+
toolroll task pause <id> [--run <id>] stop a running Claude build's
|
|
321
|
+
current turn and keep the build; the
|
|
322
|
+
next note continues it (Stop ends it)
|
|
323
|
+
toolroll task continue <id> [--run <id>]
|
|
324
|
+
resume a paused Claude build where it
|
|
325
|
+
stopped, without a note
|
|
312
326
|
toolroll task assign <id> --runner <name> | --anyone
|
|
313
327
|
reserve it for one worker (it joins
|
|
314
328
|
the back of that worker's queue) or
|
|
@@ -597,7 +611,7 @@ Exit codes
|
|
|
597
611
|
* (surface.ts) is tested for exact equality against them.
|
|
598
612
|
*/
|
|
599
613
|
export const TASK_ACTIONS = [
|
|
600
|
-
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign",
|
|
614
|
+
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign",
|
|
601
615
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
602
616
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask",
|
|
603
617
|
"checks", "add-tests",
|
|
@@ -650,6 +664,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
650
664
|
"access", "projects", "overlap",
|
|
651
665
|
// audit: a token by name where --token is a credential (remote).
|
|
652
666
|
"token-name",
|
|
667
|
+
// task pause / task steer --now: the asker's own request id, so a repeat is the same request.
|
|
668
|
+
"request",
|
|
653
669
|
]);
|
|
654
670
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
655
671
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file",
|
|
@@ -663,6 +679,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
663
679
|
"saved",
|
|
664
680
|
// task complete: also open the result's pull request.
|
|
665
681
|
"pull-request",
|
|
682
|
+
// storage clean --landed: work already in main.
|
|
683
|
+
"landed",
|
|
666
684
|
// up: the exact start-up details (containment) in the terminal too.
|
|
667
685
|
"verbose",
|
|
668
686
|
// lead token: end the lead's credential.
|
|
@@ -673,6 +691,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
673
691
|
"also",
|
|
674
692
|
// tokens: the person's password, piped (never an argument).
|
|
675
693
|
"password-stdin",
|
|
694
|
+
// task steer: pause the running Claude build, then send the note.
|
|
695
|
+
"now",
|
|
676
696
|
]);
|
|
677
697
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
678
698
|
const positional = [];
|
|
@@ -1717,9 +1737,9 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1717
1737
|
if (name === undefined || given === undefined || positional.length > 3) {
|
|
1718
1738
|
return fail(write, json, "runner capacity", "usage", "`runner capacity <name> <n> --as <you> --token <t>`", EXIT.usage);
|
|
1719
1739
|
}
|
|
1720
|
-
const capacity = parseRunnerCapacity(given);
|
|
1740
|
+
const capacity = parseRunnerCapacity(given, { allowZero: true });
|
|
1721
1741
|
if (capacity === null) {
|
|
1722
|
-
return fail(write, json, "runner capacity", "usage", `capacity is a whole number of tasks,
|
|
1742
|
+
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);
|
|
1723
1743
|
}
|
|
1724
1744
|
const acting = await askCredentials(flags, context);
|
|
1725
1745
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
@@ -1731,6 +1751,12 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1731
1751
|
return fail(write, json, "runner capacity", changed.reason, changed.reason === "retired" ? `${name} is retired` : `no runner \`${name}\``, EXIT.refused);
|
|
1732
1752
|
}
|
|
1733
1753
|
const running = store.liveClaimCount(name, now);
|
|
1754
|
+
if (changed.after === 0) {
|
|
1755
|
+
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
1756
|
+
`${name} takes no new builds. ${running === 0 ? "Nothing is running." : `${running} running task(s) will finish.`}`,
|
|
1757
|
+
`Resume with: toolroll runner capacity ${name} <1-${RUNNER_CAPACITY_MAX}>`,
|
|
1758
|
+
]);
|
|
1759
|
+
}
|
|
1734
1760
|
return succeed(write, json, "runner capacity", { name, before: changed.before, after: changed.after, running }, () => [
|
|
1735
1761
|
changed.before === changed.after
|
|
1736
1762
|
? `${name} already runs up to ${changed.after} at once.`
|
|
@@ -3419,6 +3445,14 @@ async function reconcileCommand(flags, context) {
|
|
|
3419
3445
|
reaped: reaped.map(claim => claim.leaseId),
|
|
3420
3446
|
});
|
|
3421
3447
|
}
|
|
3448
|
+
// Landed work (landed-cleanup.ts): finish teardowns a crash cut short, then mark newly landed results complete (a
|
|
3449
|
+
// ledger entry says why) and remove their clean checkouts and local build and plan branches.
|
|
3450
|
+
let landed = { completed: [], steps: [] };
|
|
3451
|
+
try {
|
|
3452
|
+
landed = await reconcileLanded(store, worktrees, gitOf(context.gitRunner), repo, context.evidenceRoot, clock);
|
|
3453
|
+
}
|
|
3454
|
+
catch { /* the next pass tries again */ }
|
|
3455
|
+
const landedGone = landed.steps.filter(one => one.outcome === "removed");
|
|
3422
3456
|
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
3423
3457
|
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
3424
3458
|
// Completing or cancelling a task stops anything still running from its checkout first.
|
|
@@ -3437,9 +3471,10 @@ async function reconcileCommand(flags, context) {
|
|
|
3437
3471
|
const retained = retention === null ? [] : retention.counts.filter(one => one.count > 0);
|
|
3438
3472
|
// The daily storage sweep (storage-sweep.ts): stale test temp folders, orphans Toolroll left running, finished
|
|
3439
3473
|
// checkouts, extra staged runtimes and dependency installs unused for a week; saved for Settings → Storage.
|
|
3474
|
+
// Launch only: report a result here only if it already completed. Later results save independently.
|
|
3440
3475
|
let storageSwept = null;
|
|
3441
3476
|
try {
|
|
3442
|
-
storageSwept =
|
|
3477
|
+
storageSwept = dailyStorageSweep(store, { databaseFile: context.databaseFile, pool: worktrees, tempRoots: context.tempRoots ?? tempRoots(), ...(context.sweepOwner === undefined ? {} : { owner: context.sweepOwner }) }, clock);
|
|
3443
3478
|
}
|
|
3444
3479
|
catch {
|
|
3445
3480
|
storageSwept = null; /* the next pass tries again */
|
|
@@ -3455,6 +3490,8 @@ async function reconcileCommand(flags, context) {
|
|
|
3455
3490
|
closedRuns.length === 0 &&
|
|
3456
3491
|
closedCheckouts.length === 0 &&
|
|
3457
3492
|
slimmed.slimmed.length === 0 &&
|
|
3493
|
+
landed.completed.length === 0 &&
|
|
3494
|
+
landedGone.length === 0 &&
|
|
3458
3495
|
liveSwept.removed.length === 0;
|
|
3459
3496
|
return succeed(write, json, "reconcile", {
|
|
3460
3497
|
recovered,
|
|
@@ -3465,6 +3502,7 @@ async function reconcileCommand(flags, context) {
|
|
|
3465
3502
|
checkoutsKept: pruned.kept,
|
|
3466
3503
|
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) }))],
|
|
3467
3504
|
checkoutsSlimmed: slimmed.slimmed.map(one => one.path),
|
|
3505
|
+
landed: { completed: landed.completed, steps: landed.steps },
|
|
3468
3506
|
liveViewsSwept: liveSwept.removed.length,
|
|
3469
3507
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
3470
3508
|
storageSweep: storageSwept,
|
|
@@ -3478,6 +3516,8 @@ async function reconcileCommand(flags, context) {
|
|
|
3478
3516
|
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} finished task checkout(s), about ${bytesWords(pruned.freed)}; their branches stay.`]),
|
|
3479
3517
|
...closedRuns.map(one => `Run #${one.runId} ended; stopped ${stoppedWords(one.done)}.`),
|
|
3480
3518
|
...closedCheckouts.map(one => `${one.path}: its task finished; stopped ${stoppedWords(one.done)}.`),
|
|
3519
|
+
...(landed.completed.length === 0 ? [] : [`Marked ${landed.completed.length} landed result(s) complete: ${landed.completed.join(", ")}.`]),
|
|
3520
|
+
...(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).`]),
|
|
3481
3521
|
...(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))}.`]),
|
|
3482
3522
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
3483
3523
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
@@ -4343,8 +4383,10 @@ async function providersCommand(flags, context) {
|
|
|
4343
4383
|
}
|
|
4344
4384
|
write(` isolation ${audit.isolation.flag === null
|
|
4345
4385
|
? "no hermetic flag"
|
|
4346
|
-
:
|
|
4347
|
-
|
|
4386
|
+
: audit.isolation.enforced
|
|
4387
|
+
? `builds run in their own setup (${audit.isolation.flag}) — none of your own hooks, plugins or MCP servers; reviews keep theirs`
|
|
4388
|
+
: `${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" : ""}`}`);
|
|
4389
|
+
write(` ${audit.isolation.enforced ? "builds get " : "config surface"} ${audit.configSurface.join("; ")}`);
|
|
4348
4390
|
write("");
|
|
4349
4391
|
}
|
|
4350
4392
|
write(" \u2192 toolroll config show which provider each phase actually resolves to");
|
|
@@ -6102,8 +6144,10 @@ async function runWatchLoop(args) {
|
|
|
6102
6144
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
6103
6145
|
// Reconciliation has its own cadence and sink. Sharing tick's sink would
|
|
6104
6146
|
// interleave envelopes while a build awaits its provider or project check.
|
|
6147
|
+
const sweepOwner = {};
|
|
6105
6148
|
const maintenance = startMaintenance({
|
|
6106
6149
|
intervalMs: reconcileEveryMs,
|
|
6150
|
+
onStop: () => storageSweepSupervisor(context.databaseFile).stop(sweepOwner),
|
|
6107
6151
|
shouldStop: stopping,
|
|
6108
6152
|
onError: error => {
|
|
6109
6153
|
reconciliationReady = false;
|
|
@@ -6114,7 +6158,7 @@ async function runWatchLoop(args) {
|
|
|
6114
6158
|
},
|
|
6115
6159
|
run: async () => {
|
|
6116
6160
|
const maintenanceLines = [];
|
|
6117
|
-
const maintenanceContext = { ...quietContext, write: line => maintenanceLines.push(line) };
|
|
6161
|
+
const maintenanceContext = { ...quietContext, sweepOwner, write: line => maintenanceLines.push(line) };
|
|
6118
6162
|
const code = await reconcileCommand(passFlags(), maintenanceContext);
|
|
6119
6163
|
if (code !== EXIT.ok)
|
|
6120
6164
|
throw new Error(maintenanceLines.join(" "));
|
|
@@ -8469,7 +8513,7 @@ function taskCommand(positional, flags, context) {
|
|
|
8469
8513
|
return taskAction(action, rest, flags, context);
|
|
8470
8514
|
}
|
|
8471
8515
|
/** Task verbs that are the lead working on a task, not reading it. */
|
|
8472
|
-
const LEAD_WORK_VERBS = new Set(["state", "steer", "reopen", "requeue", "regate", "repair", "resume", "stop", "scope", "approve", "plan", "checks", "revise", "hold", "unhold", "route", "require"]);
|
|
8516
|
+
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"]);
|
|
8473
8517
|
function taskAction(action, rest, flags, context) {
|
|
8474
8518
|
switch (action) {
|
|
8475
8519
|
case "complete":
|
|
@@ -8510,6 +8554,10 @@ function taskAction(action, rest, flags, context) {
|
|
|
8510
8554
|
return nextTask(rest, flags, context);
|
|
8511
8555
|
case "steer":
|
|
8512
8556
|
return steerTask(rest, flags, context);
|
|
8557
|
+
case "pause":
|
|
8558
|
+
return pauseTask(rest, flags, context);
|
|
8559
|
+
case "continue":
|
|
8560
|
+
return continueTask(rest, flags, context);
|
|
8513
8561
|
case "assign":
|
|
8514
8562
|
return assignTask(rest, flags, context);
|
|
8515
8563
|
case "reopen":
|
|
@@ -9005,6 +9053,7 @@ function showTask(positional, context) {
|
|
|
9005
9053
|
const listed = workIndexTask(store, id, now, access, context.evidenceRoot);
|
|
9006
9054
|
const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
|
|
9007
9055
|
const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
|
|
9056
|
+
const steeringLive = store.steeringLiveBuild(ref.id, now) !== null;
|
|
9008
9057
|
const detail = {
|
|
9009
9058
|
task,
|
|
9010
9059
|
work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
|
|
@@ -9044,6 +9093,9 @@ function showTask(positional, context) {
|
|
|
9044
9093
|
stops: store.stopsForTask(ref.id),
|
|
9045
9094
|
// The pull request Complete opened (any version), with its CI state and merge commit — the console's view.
|
|
9046
9095
|
pullRequest: newestPullRequestOf(store, store.taskFamilyOf(id, null, true)?.versions.map(one => one.id) ?? [id]),
|
|
9096
|
+
// Operator steering (v120), in the task page's words: read (and when), waiting, or for the next attempt.
|
|
9097
|
+
steering: store.listSteerNotes(ref.id).map(one => ({ id: one.id, author: one.author, createdAt: one.createdAt, note: one.note,
|
|
9098
|
+
standing: steerStandingOf(one, steeringLive), words: steerStandingWords(one, steeringLive, utcMinute) })),
|
|
9047
9099
|
};
|
|
9048
9100
|
return succeed(write, json, "task show", detail, () => [
|
|
9049
9101
|
`${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
|
|
@@ -9078,6 +9130,7 @@ function showTask(positional, context) {
|
|
|
9078
9130
|
? []
|
|
9079
9131
|
: [` position ${detail.position.position} of ${detail.position.total}${detail.position.column === null ? " in the shared queue" : ` in ${detail.position.column}'s queue`}`]),
|
|
9080
9132
|
...(detail.hold === null ? [] : [` held: ${detail.hold.reason}`]),
|
|
9133
|
+
...detail.steering.slice(-5).map(one => ` note #${one.id} to the agent: ${one.words}`),
|
|
9081
9134
|
...(detail.claim === null ? [] : [` claimed by ${detail.claim.runner} until ${detail.claim.expiresAt}`]),
|
|
9082
9135
|
...(detail.dispatch === null ? [] : [` dispatch: ${detail.dispatch.summary} — ${detail.dispatch.detail}`]),
|
|
9083
9136
|
...(detail.control.kind === "none"
|
|
@@ -9433,7 +9486,7 @@ async function steerTask(positional, flags, context) {
|
|
|
9433
9486
|
const id = positional[0];
|
|
9434
9487
|
const note = text(flags, "note");
|
|
9435
9488
|
if (id === undefined || note === undefined) {
|
|
9436
|
-
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; the note
|
|
9489
|
+
return fail(write, json, "task steer", "usage", "`toolroll task steer <id> --note \"...\" --as <you> --token <t>` — steering speaks with the operator's voice, so it takes your credential; a running build's agent reads the note when it next checks for notes, otherwise the next attempt's brief quotes it, fenced, inside the approved scope", EXIT.usage);
|
|
9437
9490
|
}
|
|
9438
9491
|
// Ruling 11: authorship derives from a VERIFIED principal, never a flag.
|
|
9439
9492
|
// Missing credentials are usage (the invocation is incomplete); present
|
|
@@ -9446,6 +9499,9 @@ async function steerTask(positional, flags, context) {
|
|
|
9446
9499
|
if (!authed.ok) {
|
|
9447
9500
|
return fail(write, json, "task steer", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9448
9501
|
}
|
|
9502
|
+
// --now: Pause and steer — the running Claude build's turn stops, then this note continues it.
|
|
9503
|
+
if (flags.get("now") === true)
|
|
9504
|
+
return pauseRunningBuild("task steer", id, note, verifiedAuthor(acting.name), flags, context);
|
|
9449
9505
|
const filed = store.fileSteerNote(id, verifiedAuthor(acting.name), note, context.now, mutationFrom(flags, context.now));
|
|
9450
9506
|
if (!filed.ok) {
|
|
9451
9507
|
const detail = filed.reason === "unknown-task"
|
|
@@ -9455,8 +9511,89 @@ async function steerTask(positional, flags, context) {
|
|
|
9455
9511
|
: (filed.problem ?? "that note will not store");
|
|
9456
9512
|
return fail(write, json, "task steer", filed.reason, detail, EXIT.refused);
|
|
9457
9513
|
}
|
|
9458
|
-
|
|
9459
|
-
|
|
9514
|
+
const steered = store.lookupRef(id);
|
|
9515
|
+
const live = steered !== null && store.steeringLiveBuild(steered.id, context.now) !== null;
|
|
9516
|
+
const liveInput = steered !== null && store.liveInputRun(steered.id, context.now) !== null;
|
|
9517
|
+
return succeed(write, json, "task steer", { task: id, note: filed.id, standing: live ? "waiting" : "next-attempt" }, () => [
|
|
9518
|
+
`Noted. ${steerDestinationWords(live, liveInput)} \`task show ${id}\` says when it has been read.`,
|
|
9519
|
+
]);
|
|
9520
|
+
}
|
|
9521
|
+
/**
|
|
9522
|
+
* `toolroll task pause <id> [--run <n>] --as <you> --token <t>` (live steering): stop a running Claude build's
|
|
9523
|
+
* current turn and keep the build — its session waits for the next note. Never a stop: nothing ends, holds or
|
|
9524
|
+
* reclaims the attempt. Bound to the exact run (--run, or the one taking live input now); a repeated --request id
|
|
9525
|
+
* is the same request.
|
|
9526
|
+
*/
|
|
9527
|
+
async function pauseTask(positional, flags, context) {
|
|
9528
|
+
const { store, write, json } = context;
|
|
9529
|
+
const id = positional[0];
|
|
9530
|
+
if (id === undefined)
|
|
9531
|
+
return fail(write, json, "task pause", "usage", "`toolroll task pause <id> [--run <run-id>] --as <you> --token <t>`", EXIT.usage);
|
|
9532
|
+
const acting = await askCredentials(flags, context);
|
|
9533
|
+
if (acting === null)
|
|
9534
|
+
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);
|
|
9535
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9536
|
+
if (!authed.ok)
|
|
9537
|
+
return fail(write, json, "task pause", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9538
|
+
return pauseRunningBuild("task pause", id, undefined, verifiedAuthor(acting.name), flags, context);
|
|
9539
|
+
}
|
|
9540
|
+
/**
|
|
9541
|
+
* `toolroll task continue <id> [--run <n>] --as <you> --token <t>` (live steering): Resume a paused Claude build with a
|
|
9542
|
+
* short continue frame and no note. Bound to the exact run (--run, or the one taking live input now), and only while it
|
|
9543
|
+
* is paused; a repeated --request id is the same request.
|
|
9544
|
+
*/
|
|
9545
|
+
async function continueTask(positional, flags, context) {
|
|
9546
|
+
const { store, write, json } = context;
|
|
9547
|
+
const id = positional[0];
|
|
9548
|
+
if (id === undefined)
|
|
9549
|
+
return fail(write, json, "task continue", "usage", "`toolroll task continue <id> [--run <run-id>] --as <you> --token <t>`", EXIT.usage);
|
|
9550
|
+
const acting = await askCredentials(flags, context);
|
|
9551
|
+
if (acting === null)
|
|
9552
|
+
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);
|
|
9553
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
9554
|
+
if (!authed.ok)
|
|
9555
|
+
return fail(write, json, "task continue", "not-an-approver", "that is not an approver, or the token does not match", EXIT.refused);
|
|
9556
|
+
const ref = store.lookupRef(id);
|
|
9557
|
+
if (ref === null || store.getTask(id) === null)
|
|
9558
|
+
return fail(write, json, "task continue", "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9559
|
+
const runText = text(flags, "run");
|
|
9560
|
+
const named = runText === undefined ? store.liveInputRun(ref.id, context.now) : Number(runText);
|
|
9561
|
+
if (named === null)
|
|
9562
|
+
return fail(write, json, "task continue", "not-live", `${id} has no running Claude build to resume`, EXIT.refused);
|
|
9563
|
+
if (!Number.isInteger(named) || named < 1)
|
|
9564
|
+
return fail(write, json, "task continue", "usage", "--run names one exact attempt by its number", EXIT.usage);
|
|
9565
|
+
const requestId = text(flags, "request") ?? text(flags, "key") ?? `cli-${randomUUID()}`;
|
|
9566
|
+
const asked = store.requestResume({ taskId: id, runId: named, by: verifiedAuthor(acting.name), via: "cli", requestId }, context.now);
|
|
9567
|
+
if (!asked.ok)
|
|
9568
|
+
return fail(write, json, "task continue", asked.reason, asked.detail, EXIT.refused, { run: named });
|
|
9569
|
+
return succeed(write, json, "task continue", { task: id, run: named, resume: asked.resume, repeated: asked.duplicate }, () => [
|
|
9570
|
+
asked.duplicate ? `Build #${named} of ${id} was already asked to resume with that request; nothing changed.`
|
|
9571
|
+
: `Resuming build #${named} of ${id} where it stopped.`,
|
|
9572
|
+
]);
|
|
9573
|
+
}
|
|
9574
|
+
/** Pause, or Pause and steer, through the store's one action; the words both CLI doors print. */
|
|
9575
|
+
function pauseRunningBuild(command, id, note, by, flags, context) {
|
|
9576
|
+
const { store, write, json } = context;
|
|
9577
|
+
const ref = store.lookupRef(id);
|
|
9578
|
+
if (ref === null || store.getTask(id) === null)
|
|
9579
|
+
return fail(write, json, command, "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
9580
|
+
const runText = text(flags, "run");
|
|
9581
|
+
const named = runText === undefined ? store.liveInputRun(ref.id, context.now) : Number(runText);
|
|
9582
|
+
if (named === null) {
|
|
9583
|
+
return fail(write, json, command, "not-live", note === undefined
|
|
9584
|
+
? `${id} has no running Claude build to pause`
|
|
9585
|
+
: `${id} has no running Claude build to pause; send the note without --now`, EXIT.refused);
|
|
9586
|
+
}
|
|
9587
|
+
if (!Number.isInteger(named) || named < 1)
|
|
9588
|
+
return fail(write, json, command, "usage", "--run names one exact attempt by its number", EXIT.usage);
|
|
9589
|
+
const requestId = text(flags, "request") ?? text(flags, "key") ?? `cli-${randomUUID()}`;
|
|
9590
|
+
const asked = store.requestPause({ taskId: id, runId: named, by, via: "cli", requestId, ...(note === undefined ? {} : { note }) }, context.now);
|
|
9591
|
+
if (!asked.ok)
|
|
9592
|
+
return fail(write, json, command, asked.reason, asked.detail, EXIT.refused, { run: named });
|
|
9593
|
+
return succeed(write, json, command, { task: id, run: named, pause: asked.pause, note: asked.note, repeated: asked.duplicate }, () => [
|
|
9594
|
+
asked.duplicate ? `Build #${named} of ${id} was already asked to pause with that request; nothing changed.`
|
|
9595
|
+
: note === undefined ? `Pausing build #${named} of ${id}. Its next note continues it; \`task stop\` ends it.`
|
|
9596
|
+
: `Pausing build #${named} of ${id}, then sending your note. \`task show ${id}\` says when it has been read.`,
|
|
9460
9597
|
]);
|
|
9461
9598
|
}
|
|
9462
9599
|
/** The mirror of block: stop waiting. Removal cannot create a cycle. */
|
|
@@ -10981,7 +11118,7 @@ async function retentionCommand(positional, flags, context) {
|
|
|
10981
11118
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
10982
11119
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
10983
11120
|
}
|
|
10984
|
-
/** `storage`, `storage cleanup [<when>]`, `storage clean [--yes]`, `storage discard <path> --yes`: where the disk goes,
|
|
11121
|
+
/** `storage`, `storage cleanup [<when>]`, `storage clean [--landed] [--yes]`, `storage discard <path> --yes`: where the disk goes,
|
|
10985
11122
|
* and task checkouts: when a finished task's clean checkout is removed, cleaning up now, and throwing away one kept for
|
|
10986
11123
|
* its changes. Changing anything takes an instance operator. */
|
|
10987
11124
|
async function storageCommand(positional, flags, context) {
|
|
@@ -10989,7 +11126,7 @@ async function storageCommand(positional, flags, context) {
|
|
|
10989
11126
|
const command = `storage ${action ?? ""}`.trim();
|
|
10990
11127
|
const store = context.store;
|
|
10991
11128
|
const changes = (action === "cleanup" && rest.length > 0) || (action === "clean" && flags.has("yes")) || action === "discard";
|
|
10992
|
-
const allowed = new Set([...(changes ? ["as", "token", "token-file", "token-env"] : []), ...(action === "clean" || action === "discard" ? ["yes"] : []), "db", "json"]);
|
|
11129
|
+
const allowed = new Set([...(changes ? ["as", "token", "token-file", "token-env"] : []), ...(action === "clean" || action === "discard" ? ["yes"] : []), ...(action === "clean" ? ["landed"] : []), "db", "json"]);
|
|
10993
11130
|
for (const name of flags.keys())
|
|
10994
11131
|
if (!allowed.has(name))
|
|
10995
11132
|
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
@@ -11067,6 +11204,11 @@ async function storageCommand(positional, flags, context) {
|
|
|
11067
11204
|
const saved = store.setCheckoutCleanup(cleanup, actor, context.clock());
|
|
11068
11205
|
return succeed(context.write, context.json, command, { cleanup: saved }, () => [`Saved. A finished task's clean checkout is removed ${cleanupWords(saved)}; its branch stays.`]);
|
|
11069
11206
|
}
|
|
11207
|
+
if (action === "clean" && flags.has("landed")) {
|
|
11208
|
+
if (rest.length > 0 || flags.get("landed") !== true)
|
|
11209
|
+
return fail(context.write, context.json, "storage clean --landed", "usage", "Use storage clean --landed to preview, then storage clean --landed --yes.", EXIT.usage);
|
|
11210
|
+
return landedCommand(flags.has("yes"), operator, refused, pool, context);
|
|
11211
|
+
}
|
|
11070
11212
|
if (action === "clean") {
|
|
11071
11213
|
if (rest.length > 0)
|
|
11072
11214
|
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
@@ -11092,7 +11234,7 @@ async function storageCommand(positional, flags, context) {
|
|
|
11092
11234
|
return refused("cleans up checkouts");
|
|
11093
11235
|
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
11094
11236
|
const temp = leftovers(true);
|
|
11095
|
-
const swept = removeStaleTestTemp(roots, context.clock());
|
|
11237
|
+
const swept = await removeStaleTestTemp(roots, context.clock());
|
|
11096
11238
|
if (swept.removed.length > 0) {
|
|
11097
11239
|
store.recordAction({ at: context.clock().toISOString(), actor, repo: null, taskId: null, runId: null, action: "test temp folders removed", outcome: "removed", source: "request",
|
|
11098
11240
|
detail: `${swept.removed.length} older than a day, about ${bytesWords(temp.staleBytes)}${swept.failed.length === 0 ? "" : `; ${swept.failed.length} couldn't be removed`}` });
|
|
@@ -11140,7 +11282,47 @@ async function storageCommand(positional, flags, context) {
|
|
|
11140
11282
|
return fail(context.write, context.json, command, "refused", done.message, EXIT.refused);
|
|
11141
11283
|
return succeed(context.write, context.json, command, { discarded: target, freedBytes: done.bytes }, () => [`Discarded ${target}, about ${bytesWords(done.bytes)}; its branch stays.`]);
|
|
11142
11284
|
}
|
|
11143
|
-
return fail(context.write, context.json, command, "usage", "Use storage, storage cleanup <when>, storage clean [--yes] or storage discard <path> --yes.", EXIT.usage);
|
|
11285
|
+
return fail(context.write, context.json, command, "usage", "Use storage, storage cleanup <when>, storage clean [--landed] [--yes] or storage discard <path> --yes.", EXIT.usage);
|
|
11286
|
+
}
|
|
11287
|
+
/** `storage clean --landed [--yes]`: the one-time pass over work that piled up before reconcile did this on its own.
|
|
11288
|
+
* Results whose work landed at least a week ago are marked complete (a ledger entry says why) and their clean
|
|
11289
|
+
* checkouts and local branches go; so do Toolroll's own branches no task uses whose work landed. A preview changes
|
|
11290
|
+
* nothing; --yes takes an instance operator, records every teardown first, then carries it out. */
|
|
11291
|
+
async function landedCommand(yes, operator, refused, pool, context) {
|
|
11292
|
+
const command = "storage clean --landed";
|
|
11293
|
+
const git = gitOf(context.gitRunner);
|
|
11294
|
+
const plurals = (count, one, many = `${one}s`) => `${count} ${count === 1 ? one : many}`;
|
|
11295
|
+
const keepLine = (one) => ` ${projectName(one.repo)} ${one.what}: ${one.why}`;
|
|
11296
|
+
const stepLine = (one) => ` ${projectName(one.repo)} ${one.target}${one.why === null ? "" : `: ${one.why}`}`;
|
|
11297
|
+
const counts = (plan) => ({
|
|
11298
|
+
results: plan.results.length, complete: plan.results.filter(one => one.completes).length,
|
|
11299
|
+
checkouts: plan.results.reduce((sum, one) => sum + one.targets.filter(target => target.kind === "checkout").length, 0) + plan.branches.filter(one => one.checkout !== null).length,
|
|
11300
|
+
branches: plan.results.reduce((sum, one) => sum + one.targets.filter(target => target.kind === "branch").length, 0) + plan.branches.length,
|
|
11301
|
+
});
|
|
11302
|
+
if (!yes) {
|
|
11303
|
+
const plan = await landedBacklog(context.store, git, context.evidenceRoot, context.clock());
|
|
11304
|
+
const n = counts(plan);
|
|
11305
|
+
const anything = n.results + plan.branches.length + plan.pending > 0;
|
|
11306
|
+
return succeed(context.write, context.json, command, { preview: true, cutoff: plan.cutoff, results: plan.results, branches: plan.branches, kept: plan.kept, pending: plan.pending }, () => [
|
|
11307
|
+
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.`,
|
|
11308
|
+
...plan.results.map(one => ` ${projectName(one.repo)} ${one.taskId}: ${one.reason}${one.completes ? "" : " (already complete)"}`),
|
|
11309
|
+
...plan.branches.map(one => ` ${projectName(one.repo)} ${one.branch}: ${one.reason}`),
|
|
11310
|
+
...(plan.pending === 0 ? [] : [`${plurals(plan.pending, "item")} from an earlier clean-up will finish too.`]),
|
|
11311
|
+
...(plan.kept.length === 0 ? [] : [`Stays (${plan.kept.length}):`, ...plan.kept.map(keepLine)]),
|
|
11312
|
+
anything ? "Nothing was changed. Run toolroll storage clean --landed --yes to do it." : "Nothing was changed.",
|
|
11313
|
+
]);
|
|
11314
|
+
}
|
|
11315
|
+
const actor = await operator();
|
|
11316
|
+
if (actor === null)
|
|
11317
|
+
return refused("cleans up landed work");
|
|
11318
|
+
const plan = await landedBacklog(context.store, git, context.evidenceRoot, context.clock());
|
|
11319
|
+
const done = await cleanLandedBacklog(context.store, pool, git, context.evidenceRoot, context.clock, plan, actor);
|
|
11320
|
+
const gone = done.steps.filter(one => one.outcome === "removed" || one.outcome === "already gone");
|
|
11321
|
+
const stays = done.steps.filter(one => one.outcome === "waits" || one.outcome === "kept");
|
|
11322
|
+
return succeed(context.write, context.json, command, { completed: done.completed, steps: done.steps, refused: done.refused, kept: plan.kept }, () => [
|
|
11323
|
+
`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")}.`,
|
|
11324
|
+
...(stays.length + done.refused.length === 0 ? [] : [`Stays (${stays.length + done.refused.length}):`, ...stays.map(stepLine), ...done.refused.map(keepLine)]),
|
|
11325
|
+
]);
|
|
11144
11326
|
}
|
|
11145
11327
|
function checkoutSummary(plan) {
|
|
11146
11328
|
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>;
|
package/dist/orphan-sweep.js
CHANGED
|
@@ -180,7 +180,8 @@ export async function sweepOrphans(store, now, options = {}) {
|
|
|
180
180
|
catch {
|
|
181
181
|
return { stopped: [], failed: ["the process list couldn't be read"] };
|
|
182
182
|
}
|
|
183
|
-
const
|
|
183
|
+
const rootsNow = (rows) => sweepRoots(store, options.pool ?? null, rows, options.tempRoots ?? [], now()).filter(root => options.spare?.(root.path) !== true);
|
|
184
|
+
const roots = rootsNow(census);
|
|
184
185
|
if (roots.length === 0)
|
|
185
186
|
return { stopped: [], failed };
|
|
186
187
|
const guard = protectedProcesses(census, options.self);
|
|
@@ -195,14 +196,15 @@ export async function sweepOrphans(store, now, options = {}) {
|
|
|
195
196
|
const first = orphanTargets(census, located, roots, guard);
|
|
196
197
|
if (first.length === 0)
|
|
197
198
|
return { stopped: [], failed };
|
|
198
|
-
// Right before the signal, everything again: the same process, still an orphan (or under one), still inside
|
|
199
|
+
// Right before the signal, everything again: the same process, still an orphan (or under one), still inside a root
|
|
200
|
+
// that is still one (no run or lease took its checkout since, nothing live uses it).
|
|
199
201
|
let again;
|
|
200
202
|
try {
|
|
201
203
|
const fresh = read();
|
|
202
204
|
const freshGuard = protectedProcesses(fresh, options.self);
|
|
203
205
|
const freshLocated = locate(fresh.filter(row => row.ppid === 1).map(row => row.pid));
|
|
204
206
|
const before = new Map(first.map(one => [one.row.pid, one]));
|
|
205
|
-
again = orphanTargets(fresh, freshLocated,
|
|
207
|
+
again = orphanTargets(fresh, freshLocated, rootsNow(fresh), freshGuard).filter(one => {
|
|
206
208
|
const was = before.get(one.row.pid);
|
|
207
209
|
return was !== undefined && Math.abs(was.row.bornAt - one.row.bornAt) <= BIRTH_SLACK_MS && was.root.path === one.root.path;
|
|
208
210
|
});
|
|
@@ -20,6 +20,17 @@ export type ProjectSettingsView = {
|
|
|
20
20
|
building: number;
|
|
21
21
|
canChange: boolean;
|
|
22
22
|
};
|
|
23
|
+
/** What its builds can reach: the project's named tools and whether each loads (names and words only, never a
|
|
24
|
+
* secret), or why they couldn't be read. */
|
|
25
|
+
access?: {
|
|
26
|
+
tools: {
|
|
27
|
+
name: string;
|
|
28
|
+
loads: boolean;
|
|
29
|
+
words: string;
|
|
30
|
+
}[];
|
|
31
|
+
} | {
|
|
32
|
+
problem: string;
|
|
33
|
+
};
|
|
23
34
|
};
|
|
24
35
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
25
36
|
export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
|
|
@@ -14,7 +14,34 @@ export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:7
|
|
|
14
14
|
`.project-settings button,.project-delete button{justify-self:start;min-height:36px;white-space:nowrap}.project-delete .actions a{min-height:36px;display:inline-flex;align-items:center}` +
|
|
15
15
|
`.project-settings .builds-at-once{margin-top:20px}.project-settings .builds-at-once h2{font-size:1rem;margin:0 0 4px}.project-settings .builds-at-once form{margin-top:4px;gap:4px}` +
|
|
16
16
|
`.project-settings .builds-at-once .builds-row{display:flex;gap:8px;align-items:stretch}.project-settings .builds-at-once input{width:5.5rem;min-height:44px;margin:0;box-sizing:border-box}.project-settings .builds-at-once button{min-height:44px;margin:0}` +
|
|
17
|
-
`.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}
|
|
17
|
+
`.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}` +
|
|
18
|
+
`.project-settings .build-access{margin-top:20px;min-width:0;overflow-wrap:anywhere}.project-settings .build-access h2{font:var(--so-typography-title-sm);margin:0 0 4px}.project-settings .build-access p{margin:0}` +
|
|
19
|
+
`.project-settings .build-access .tool-name{font:var(--so-typography-mono)}.project-settings .build-access details{margin:4px 0 0}.project-settings .build-access summary{min-height:44px;display:inline-flex;align-items:center;cursor:pointer;color:var(--so-muted);font:var(--so-typography-label)}` +
|
|
20
|
+
`.project-settings .build-access ul{margin:0 0 4px;padding-left:20px;font:var(--so-typography-body);color:var(--so-ink)}.project-settings .build-access li{margin:2px 0}.project-settings .build-access li .meta{color:var(--so-muted)}` +
|
|
21
|
+
`.project-settings .build-access a.manage-tools{display:inline-flex;align-items:center;justify-content:center;min-height:44px;margin-top:4px;padding:0 14px;white-space:nowrap;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line);border-radius:var(--so-radius-md);font:var(--so-typography-label);text-decoration:none}` +
|
|
22
|
+
`.project-settings .build-access a.manage-tools:hover{background:var(--so-raised)}.project-settings .build-access a.manage-tools:focus-visible,.project-settings .build-access summary:focus-visible{outline:2px solid var(--so-ink);outline-offset:2px}`;
|
|
23
|
+
/** Build access: one sentence for what a build gets, the detail on demand, and the way to change it. */
|
|
24
|
+
function accessHtml(view) {
|
|
25
|
+
const access = view.access;
|
|
26
|
+
if (access === undefined)
|
|
27
|
+
return "";
|
|
28
|
+
const manage = html `<a class="manage-tools" href="/settings/tools?repo=${encodeURIComponent(view.repo)}">Manage tools</a>`;
|
|
29
|
+
const always = [
|
|
30
|
+
"Your other projects, keys and sign-in files are out of reach.",
|
|
31
|
+
"Your own Claude and Codex hooks, plugins and MCP servers don't load.",
|
|
32
|
+
"Builds sign in with your existing Claude or Codex login.",
|
|
33
|
+
];
|
|
34
|
+
if ("problem" in access) {
|
|
35
|
+
return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2><p class="problem" role="alert">${access.problem}</p><details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}</ul></details>${manage}</section>`;
|
|
36
|
+
}
|
|
37
|
+
const loading = access.tools.filter(one => one.loads);
|
|
38
|
+
const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}<span class="tool-name">${one.name}</span>`);
|
|
39
|
+
const outcome = loading.length === 0
|
|
40
|
+
? html `<p>Builds work in their task's folder, with no extra tools.</p>`
|
|
41
|
+
: html `<p>Builds work in their task's folder, with ${loading.length === 1 ? "one tool" : `${loading.length} tools`}: ${names}.</p>`;
|
|
42
|
+
const tools = access.tools.map(one => html `<li><span class="tool-name">${one.name}</span> <span class="meta">· ${one.words}</span></li>`);
|
|
43
|
+
return html `<section class="build-access" aria-labelledby="build-access-title"><h2 id="build-access-title">Build access</h2>${outcome}<details><summary>Details</summary><ul>${always.map(one => html `<li>${one}</li>`)}${tools}</ul></details>${manage}</section>`;
|
|
44
|
+
}
|
|
18
45
|
/** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
|
|
19
46
|
function buildsHtml(view) {
|
|
20
47
|
const b = view.builds;
|
|
@@ -30,7 +57,7 @@ const note = (notice) => notice.problem ? html `<p class="problem" role="alert">
|
|
|
30
57
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
31
58
|
export function projectSettingsHtml(view, notice) {
|
|
32
59
|
const held = html `<p>Toolroll holds ${holdingsWords(view.holdings)} for <strong>${view.name}</strong>.</p>`;
|
|
33
|
-
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${buildsHtml(view)}`;
|
|
60
|
+
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${buildsHtml(view)}${accessHtml(view)}`;
|
|
34
61
|
if (!view.canDelete)
|
|
35
62
|
return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
|
|
36
63
|
const body = view.running.length > 0
|
package/dist/project-tools.d.ts
CHANGED
|
@@ -142,22 +142,19 @@ export type ToolLaunchArgs = {
|
|
|
142
142
|
cleanup: () => void;
|
|
143
143
|
privateDir?: string;
|
|
144
144
|
};
|
|
145
|
-
/** Claude: only these servers
|
|
145
|
+
/** Claude: only these servers, written to a private 0600 file removed after the run (the provider adds `--strict-mcp-config`). */
|
|
146
146
|
export declare function claudeToolArgs(launch: ToolLaunch): ToolLaunchArgs;
|
|
147
|
-
export declare function carriedCodexSettings(includeModel: boolean, codexHome?: string): string[];
|
|
148
147
|
/** Where the tool launcher lives beside this module, built. */
|
|
149
148
|
export declare function toolLauncherPath(): string;
|
|
150
149
|
/**
|
|
151
|
-
* Codex:
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
150
|
+
* Codex: each tool is named by `-c` and started through the launcher. The
|
|
151
|
+
* operator's config.toml, rules, apps, plugins and hooks are already off
|
|
152
|
+
* (the provider's CODEX_BUILD_ISOLATION_ARGV), and nothing of config.toml
|
|
153
|
+
* is carried. Codex gives its own environment to the agent's shell whatever
|
|
155
154
|
* its policy says, so no secret rides it: each tool's values sit in the
|
|
156
155
|
* run's private 0600 file and reach only that tool's process.
|
|
157
156
|
*/
|
|
158
|
-
export declare function codexToolArgs(launch: ToolLaunch, options
|
|
159
|
-
includeModel: boolean;
|
|
160
|
-
codexHome?: string;
|
|
157
|
+
export declare function codexToolArgs(launch: ToolLaunch, options?: {
|
|
161
158
|
launcher?: string;
|
|
162
159
|
node?: string;
|
|
163
160
|
enabledTools?: Readonly<Record<string, readonly string[]>>;
|
|
@@ -182,12 +179,11 @@ export declare function toolLaunchFor(store: Pick<Store, "projectTools" | "getRu
|
|
|
182
179
|
export declare function prepareRunTools(store: Pick<Store, "projectTools" | "getRun" | "refById" | "getScope" | "toolSealFor" | "recordRunTools" | "orgPolicy">, runId: number, provider: "claude" | "codex" | "openrouter" | "gemini", options: {
|
|
183
180
|
home?: string;
|
|
184
181
|
now: Date;
|
|
185
|
-
includeModel: boolean;
|
|
186
182
|
readOnly?: Readonly<Record<string, readonly string[]>>;
|
|
187
183
|
withheld?: Readonly<Record<string, string>>;
|
|
188
184
|
}): ToolLaunchArgs;
|
|
189
185
|
/** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
|
|
190
|
-
export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini"
|
|
186
|
+
export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini"): ToolLaunchArgs;
|
|
191
187
|
type ToolWrites = Pick<Store, "projectTools" | "addProjectTool" | "removeProjectTool" | "recordProjectToolTest">;
|
|
192
188
|
/** Add one tool (and, for an import, the values it brings) to a project's builds. */
|
|
193
189
|
export declare function addToolTo(store: ToolWrites, repo: string, input: ToolSpec, source: string, by: string, now: Date, options?: {
|