toolroll 0.9.60 → 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 +15 -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/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 +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 +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 +188 -16
- 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 +65 -8
- package/dist/provider.js +80 -21
- 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 +12 -2
- package/dist/server/render-tasks.js +27 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +2 -1
- package/dist/server/task-views.js +9 -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 +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 +163 -2
- package/dist/store.js +350 -15
- package/dist/surface.js +4 -2
- 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/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +3 -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 +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/server/pages.js
CHANGED
|
@@ -21,7 +21,8 @@ import { run as execRun } from "../exec.js";
|
|
|
21
21
|
import { computeGaps } from "../gaps.js";
|
|
22
22
|
import { LEDGER_CSV_HEADER, ledgerCsvRows } from "../ledger-csv.js";
|
|
23
23
|
import { ledgerBody } from "../ledger-view.js";
|
|
24
|
-
import {
|
|
24
|
+
import { agentRowOf, agentsBoardOf, agentFactsFor, sealedDiffOf } from "../agents-board.js";
|
|
25
|
+
import { markAgentSeen } from "../agent-board.js";
|
|
25
26
|
import { prometheusMetrics } from "../metrics.js";
|
|
26
27
|
import { readMonitoring } from "../monitoring-settings.js";
|
|
27
28
|
import { targetOf } from "../monitoring.js";
|
|
@@ -374,42 +375,64 @@ export function createPagesHandlers(runtime) {
|
|
|
374
375
|
now,
|
|
375
376
|
}));
|
|
376
377
|
}
|
|
377
|
-
//
|
|
378
|
-
// ceiling admits, each fed by the run page's own transcript poller
|
|
379
|
-
// (the byte-offset JSON protocol — text into textContent, never
|
|
380
|
-
// markup). The pane SET is re-checked on a slow cadence; when it
|
|
381
|
-
// changes the page reloads rather than swapping pollers mid-flight.
|
|
378
|
+
// Peek became the Agents board: one place to check on every running agent (agents-board.ts).
|
|
382
379
|
async function peekPage(ctx) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
380
|
+
return redirect(ctx.response, "/agents");
|
|
381
|
+
}
|
|
382
|
+
/** What the board says after a Reply or a steer, in fixed words (never text from the address). */
|
|
383
|
+
const AGENT_SAID = {
|
|
384
|
+
replied: { tone: "info", text: "Reply sent. The agent reads it at its next check." },
|
|
385
|
+
steered: { tone: "info", text: "Note sent. The agent reads it at its next check." },
|
|
386
|
+
"reply-closed": { tone: "error", text: "That question was already answered." },
|
|
387
|
+
"reply-stale": { tone: "error", text: "This attempt ended before your reply, so nothing was sent." },
|
|
388
|
+
"reply-invalid": { tone: "error", text: "Write a reply of up to 1,500 characters." },
|
|
389
|
+
};
|
|
390
|
+
// The Agents board (one place to check on running agents): rows by state, and the opened agent's live view.
|
|
391
|
+
async function agentsPage(ctx) {
|
|
392
|
+
const { url, who, response, project } = ctx;
|
|
388
393
|
const now = clock();
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
394
|
+
const access = workAccess();
|
|
395
|
+
const asked = url.searchParams.get("run");
|
|
396
|
+
let selected = null;
|
|
397
|
+
if (asked !== null && /^[1-9][0-9]{0,9}$/.test(asked)) {
|
|
398
|
+
const run = store.getRun(Number(asked));
|
|
399
|
+
const facts = run === null || !runVisible(run) ? null : agentFactsFor(store, run.id, access, who.name);
|
|
400
|
+
if (facts !== null) {
|
|
401
|
+
// Opening a finished agent is viewing it: it leaves this person's unread Done.
|
|
402
|
+
if (facts.outcome !== null && markAgentSeen(store, who.name, facts.runId, now))
|
|
403
|
+
facts.seen = true;
|
|
404
|
+
const live = facts.outcome === null;
|
|
405
|
+
const sealed = live ? null : sealedDiffOf(store, evidenceRoot, facts.runId);
|
|
406
|
+
// A reply's own word first; otherwise the note that the agent asked past the questions cap.
|
|
407
|
+
const said = AGENT_SAID[url.searchParams.get("said") ?? ""] ?? (facts.questionNote === null ? null : { tone: "info", text: facts.questionNote });
|
|
408
|
+
selected = {
|
|
409
|
+
...agentRowOf(store, facts, now, evidenceRoot), live,
|
|
410
|
+
question: live && facts.question !== null ? { ...facts.question, action: `${taskHref(facts.taskId)}/reply` } : null,
|
|
411
|
+
transcript: options.localRunner === undefined ? null : `/r/${facts.runId}?fragment=transcript`,
|
|
412
|
+
// The live look at its checkout needs this machine's runner and pool, as the run page's does.
|
|
413
|
+
peek: live && options.localRunner !== undefined && options.poolRoot !== undefined ? `/r/${facts.runId}?fragment=peek` : null,
|
|
414
|
+
files: (sealed?.files ?? []).slice(0, 30).map(one => ({ path: one.path, added: one.additions, removed: one.deletions })),
|
|
415
|
+
filesMore: Math.max(0, (sealed?.fileCount ?? 0) - 30),
|
|
416
|
+
steer: live ? { action: `${taskHref(facts.taskId)}/steer`, run: facts.runId } : null,
|
|
417
|
+
canAct: who.role === "approver" && !store.isDemo(),
|
|
418
|
+
taskHref: taskHref(facts.taskId),
|
|
419
|
+
// The run's own address: a finished result opens on its result page, anything else on its record.
|
|
420
|
+
resultHref: live ? null : `/r/${facts.runId}`,
|
|
421
|
+
room: `agent:${facts.runId}`, said,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const groups = agentsBoardOf(store, now, access, who.name, evidenceRoot);
|
|
426
|
+
const view = { kind: "agents", groups, selected,
|
|
427
|
+
empty: groups.length === 0 ? "No agents in the last day. They show here the moment one starts." : null };
|
|
428
|
+
const fallback = joinHtml([
|
|
429
|
+
html `<h1>Agents</h1>`,
|
|
430
|
+
groups.length === 0 ? html `<p class="meta">${view.empty ?? ""}</p>` : joinHtml(groups.map(group => joinHtml([
|
|
431
|
+
html `<h2>${group.label}</h2>`,
|
|
432
|
+
...group.rows.map(row => html `<p class="row"><a href="${row.href}"><strong>${row.title}</strong></a> <span class="meta">${[row.label, row.line].filter(Boolean).join(" \u00b7 ")}</span></p>`),
|
|
433
|
+
], "\n")), "\n"),
|
|
434
|
+
], "\n");
|
|
435
|
+
return sendScreen(response, 200, screen("Agents", fallback, { chrome: chromeFor(project, "runs"), workspace: { view } }));
|
|
413
436
|
}
|
|
414
437
|
async function fleetPage(ctx) {
|
|
415
438
|
const { url, who, response, project } = ctx;
|
|
@@ -901,6 +924,7 @@ export function createPagesHandlers(runtime) {
|
|
|
901
924
|
"activity": activityPage,
|
|
902
925
|
"system": systemScreen,
|
|
903
926
|
"peek": peekPage,
|
|
927
|
+
"agents": agentsPage,
|
|
904
928
|
"fleet": fleetPage,
|
|
905
929
|
"ledger.export": ledgerExport,
|
|
906
930
|
"menu": menuPage,
|
|
@@ -570,6 +570,18 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
570
570
|
else if (view.kind === "unhold") {
|
|
571
571
|
what = html `<h3>Release <a href="${taskHref(task)}">${task}</a> from its hold</h3>${facts(["project", mono(repoId)])}`;
|
|
572
572
|
}
|
|
573
|
+
else if (view.kind === "steer" && payload["mode"] === "pause") {
|
|
574
|
+
const taskTitle = text("taskTitle") || task;
|
|
575
|
+
what = html `<h3>Pause <a href="${taskHref(task)}">${taskTitle}</a></h3>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">Its current turn stops and the build keeps running; its next note continues it.</p>`;
|
|
576
|
+
}
|
|
577
|
+
else if (view.kind === "steer" && payload["mode"] === "resume") {
|
|
578
|
+
const taskTitle = text("taskTitle") || task;
|
|
579
|
+
what = html `<h3>Resume <a href="${taskHref(task)}">${taskTitle}</a></h3>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">It goes on where it stopped. No note is sent.</p>`;
|
|
580
|
+
}
|
|
581
|
+
else if (view.kind === "steer" && payload["mode"] === "pause-and-steer") {
|
|
582
|
+
const taskTitle = text("taskTitle") || task;
|
|
583
|
+
what = html `<h3>Pause <a href="${taskHref(task)}">${taskTitle}</a> and send a note</h3><p class="proposal-summary">${text("note")}</p>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">Its current turn stops first. The task’s scope doesn’t change.</p>`;
|
|
584
|
+
}
|
|
573
585
|
else if (view.kind === "steer") {
|
|
574
586
|
const taskTitle = text("taskTitle") || task;
|
|
575
587
|
what = html `<h3>Guidance for <a href="${taskHref(task)}">${taskTitle}</a></h3><p class="proposal-summary">${text("note")}</p>${facts(["project", mono(repoId)])}<p class="meta proposal-disclosure">A running agent reads it when it next checks for notes; otherwise the next attempt does. The task’s scope doesn’t change.</p>`;
|
|
@@ -264,10 +264,18 @@ export declare function taskStatusCard(status: DisplayStatus & {
|
|
|
264
264
|
diagnostics?: WorkStatus["diagnostics"];
|
|
265
265
|
}, taskId: string, dispatch: DispatchDiagnosis | null, runId: number | null, approvalAction?: boolean): Html;
|
|
266
266
|
export declare function checkProgressHtml(progress: CheckProgress | null, id?: string): Html;
|
|
267
|
-
export declare function taskControlDetailsHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean): Html;
|
|
267
|
+
export declare function taskControlDetailsHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean, livePause?: LivePauseFact | null): Html;
|
|
268
268
|
/** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details.
|
|
269
269
|
* `csrf` is only a guard now: an empty one (a read-only surface) renders no forms; postForm writes the field. */
|
|
270
|
-
|
|
270
|
+
/** Live steering: the Claude build that takes Pause right now, its latest pause (null: none, or it continued), and
|
|
271
|
+
* until when a paused build can wait before it ends (null: no idle limit). */
|
|
272
|
+
export type LivePauseFact = {
|
|
273
|
+
run: number;
|
|
274
|
+
state: "pausing" | "paused" | null;
|
|
275
|
+
until?: string | null;
|
|
276
|
+
limitMs?: number | null;
|
|
277
|
+
};
|
|
278
|
+
export declare function taskControlHtml(control: TaskControlView, taskId: string, csrf: string, surface: "task" | "chat", inert?: boolean, stopElsewhere?: boolean, livePause?: LivePauseFact | null): Html;
|
|
271
279
|
/** The resume confirmation (v52): the exact run restated, what resuming
|
|
272
280
|
* does and does not do, the gate that would still keep work from
|
|
273
281
|
* starting, and the password typed again. */
|
|
@@ -489,6 +497,8 @@ export declare function taskBodyParts(data: {
|
|
|
489
497
|
liveRunId?: number | null;
|
|
490
498
|
/** What the agent did last on that live run, and when (task-activity.ts). */
|
|
491
499
|
activity?: RunActivity | null;
|
|
500
|
+
/** Live steering: the Claude build that takes Pause right now, and its latest pause (null: none, or it continued). */
|
|
501
|
+
livePause?: LivePauseFact | null;
|
|
492
502
|
/** Workers that are both alive and authorized for this task's project. */
|
|
493
503
|
worker?: {
|
|
494
504
|
answering: number;
|
|
@@ -34,7 +34,8 @@ import { runCostWords } from "../summary.js";
|
|
|
34
34
|
import {} from "../task-control.js";
|
|
35
35
|
import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_MORE, taskStatusOf } from "../task-status.js";
|
|
36
36
|
import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, workStatusOf } from "../workspace-ui.js";
|
|
37
|
-
import { createHash } from "node:crypto";
|
|
37
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
38
|
+
import { TEXT_LIMITS } from "../text-limits.js";
|
|
38
39
|
import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, transcriptScript, whenTime } from "./chrome.js";
|
|
39
40
|
import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
|
|
40
41
|
import { reviewHref, taskChatHref, taskHref } from "./http.js";
|
|
@@ -808,14 +809,26 @@ export function checkProgressHtml(progress, id = "check-progress") {
|
|
|
808
809
|
: Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
|
|
809
810
|
return html `<p id="${id}" class="check-progress mono" data-check-progress data-final="${state}" role="status" aria-live="polite">${progress.line}</p>`;
|
|
810
811
|
}
|
|
811
|
-
export function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
812
|
-
const card = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
|
|
812
|
+
export function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false, livePause = null) {
|
|
813
|
+
const card = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere, livePause);
|
|
813
814
|
return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
|
|
814
815
|
? html `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${card}</details>` : card;
|
|
815
816
|
}
|
|
816
|
-
/**
|
|
817
|
-
*
|
|
818
|
-
|
|
817
|
+
/** Pause and Pause and steer beside the task page's Stop (live steering): the same exact run, each form under its own
|
|
818
|
+
* request id so a repeated submit is the same request. Pause hides once asked; a paused build says until when it can
|
|
819
|
+
* wait and offers Resume, or the note to continue with. */
|
|
820
|
+
function pauseFormsHtml(taskId, livePause) {
|
|
821
|
+
const form = (fields, button, control, extra = {}) => postForm(`${taskHref(taskId)}/pause`, html `${fields}${button}`, { attrs: { class: "inline task-control-form", "data-task-control": control }, returnTo: "task", hidden: { run: livePause.run, request: `web-${randomUUID()}`, ...extra } });
|
|
822
|
+
const paused = livePause.state === "paused";
|
|
823
|
+
const until = livePause.until ?? null, limitMs = livePause.limitMs ?? null;
|
|
824
|
+
return joinHtml([
|
|
825
|
+
paused ? html `<p class="meta" role="status" data-live-pause="paused">${until === null || limitMs === null ? "Paused." : html `Paused. It can wait until ${whenTime(until)} (${Math.max(1, Math.round(limitMs / 60_000))} min) before it ends.`}</p>` : "",
|
|
826
|
+
livePause.state === null ? form(html ``, html `<button type="submit" class="task-control-button">Pause</button>`, "pause") : "",
|
|
827
|
+
paused ? form(html ``, html `<button type="submit" class="task-control-button">Resume</button>`, "resume", { mode: "resume" }) : "",
|
|
828
|
+
form(html `<input type="text" name="note" required maxlength="${TEXT_LIMITS.note}" placeholder="${paused ? "note to continue with" : "note for the agent"}" aria-label="${paused ? "note to continue with" : "note for the agent"}">`, html `<button type="submit" class="task-control-button">${paused ? "Continue" : "Pause and steer"}</button>`, "pause-and-steer"),
|
|
829
|
+
]);
|
|
830
|
+
}
|
|
831
|
+
export function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false, livePause = null) {
|
|
819
832
|
if (control.kind === "none")
|
|
820
833
|
return html ``;
|
|
821
834
|
const guarded = csrf !== "" && !inert;
|
|
@@ -833,6 +846,7 @@ export function taskControlHtml(control, taskId, csrf, surface, inert = false, s
|
|
|
833
846
|
return joinHtml([
|
|
834
847
|
html `<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">`,
|
|
835
848
|
html `<div class="task-control-copy"><details><summary>Stop details · ${role(control)} #${control.run}</summary><p class="meta">Stopping ends only this attempt's own processes. Its branch, uncommitted work, evidence, and decisions stay preserved; other tasks keep running.</p></details></div>`,
|
|
849
|
+
stopElsewhere || !guarded || livePause === null || livePause.run !== control.run ? "" : pauseFormsHtml(taskId, livePause),
|
|
836
850
|
stopElsewhere ? "" : guarded
|
|
837
851
|
? runForm("stop", "inline task-control-form task-stop-form", html `<button type="submit" class="danger task-control-button">Stop</button>`)
|
|
838
852
|
: html `<button type="button" class="danger task-control-button" disabled>Stop</button>`,
|
|
@@ -1894,7 +1908,7 @@ export function taskBodyParts(data) {
|
|
|
1894
1908
|
const statusHtml = html `${data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && data.dispatch?.action === "approve-scope")}${checkProgressHtml(data.checkProgress ?? null)}`;
|
|
1895
1909
|
// The exact-run control (v52), directly under the scheduler's answer:
|
|
1896
1910
|
// the one place a person stops or resumes THIS attempt.
|
|
1897
|
-
const controlHtml = taskControlDetailsHtml(data.control ?? { kind: "none" }, task.id, data.csrf, "task");
|
|
1911
|
+
const controlHtml = taskControlDetailsHtml(data.control ?? { kind: "none" }, task.id, data.csrf, "task", false, false, data.livePause ?? null);
|
|
1898
1912
|
const previousResult = data.completion?.receipt == null || receiptLeads ? null : {
|
|
1899
1913
|
title: data.assignment != null && task.state === "done" ? "Result" : "Previous result",
|
|
1900
1914
|
html: (completionReceiptCard(data.completion.receipt, task.id, "task", data.assignment == null ? receiptStatusOf(data.completion.receipt) : assignmentStatusOf(data.assignment), data.assignment ?? null, data.assignment == null)),
|
|
@@ -2175,6 +2189,11 @@ export function taskBodyParts(data) {
|
|
|
2175
2189
|
// The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
|
|
2176
2190
|
const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
|
|
2177
2191
|
? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
|
|
2192
|
+
// Pause and Pause and steer sit beside that Stop, for the same run, when it is a Claude build that takes live input.
|
|
2193
|
+
// Each form carries its own request id, so a repeated submit is the same request and the two never collide.
|
|
2194
|
+
const pause = stop !== null && data.livePause?.run === stop.run
|
|
2195
|
+
? { action: `${taskHref(task.id)}/pause`, run: stop.run, state: data.livePause.state, requests: [`web-${randomUUID()}`, `web-${randomUUID()}`, `web-${randomUUID()}`],
|
|
2196
|
+
until: data.livePause.until ?? null, limitMs: data.livePause.limitMs ?? null } : null;
|
|
2178
2197
|
// The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
|
|
2179
2198
|
const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
|
|
2180
2199
|
const earlier = earlierStopped.length === 0 ? null : {
|
|
@@ -2204,6 +2223,7 @@ export function taskBodyParts(data) {
|
|
|
2204
2223
|
// One quiet line under the step: what the agent did last, and when.
|
|
2205
2224
|
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
2206
2225
|
stop,
|
|
2226
|
+
pause,
|
|
2207
2227
|
record,
|
|
2208
2228
|
earlier,
|
|
2209
2229
|
approval: fragmentString(approvalHtml),
|
|
@@ -129,7 +129,8 @@ get("people", "people.page", "/people", { limited: "unscoped", ...free });
|
|
|
129
129
|
get("pages", "system", "/system");
|
|
130
130
|
get("tasks", "tasks", "/tasks", { limited: "collection" });
|
|
131
131
|
get("tasks", "queue", "/queue");
|
|
132
|
-
get("pages", "peek", "/peek");
|
|
132
|
+
get("pages", "peek", "/peek", free);
|
|
133
|
+
get("pages", "agents", "/agents", { limited: "unscoped", ...free });
|
|
133
134
|
get("pages", "fleet", "/fleet", free);
|
|
134
135
|
get("tasks", "tasks.new", "/tasks/new", { limited: "collection", ...free });
|
|
135
136
|
get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
|
|
@@ -330,6 +331,7 @@ post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "
|
|
|
330
331
|
post("tasks", "task.act.next", `^\\/t\\/${TASK}\\/next$`, { project: "task", limited: "resource", sample: "/t/one/next" });
|
|
331
332
|
post("tasks", "task.act.reopen", `^\\/t\\/${TASK}\\/reopen$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reopen" });
|
|
332
333
|
post("tasks", "task.act.steer", `^\\/t\\/${TASK}\\/steer$`, { project: "task", limited: "resource", sample: "/t/one/steer" });
|
|
334
|
+
post("tasks", "task.act.reply", `^\\/t\\/${TASK}\\/reply$`, { project: "task", limited: "resource", sample: "/t/one/reply" });
|
|
333
335
|
post("tasks", "task.act.accept-proof", `^\\/t\\/${TASK}\\/accept-proof$`, { project: "task", limited: "resource", sample: "/t/one/accept-proof" });
|
|
334
336
|
post("tasks", "task.act.accept-revision", `^\\/t\\/${TASK}\\/accept-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/accept-revision" });
|
|
335
337
|
post("tasks", "task.act.reject-revision", `^\\/t\\/${TASK}\\/reject-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reject-revision" });
|
|
@@ -339,6 +341,7 @@ post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "ta
|
|
|
339
341
|
post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
|
|
340
342
|
post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
|
|
341
343
|
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
344
|
+
post("tasks", "task.act.pause", `^\\/t\\/${TASK}\\/pause$`, { project: "task", limited: "resource", sample: "/t/one/pause" });
|
|
342
345
|
post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
|
|
343
346
|
post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
|
|
344
347
|
for (const one of ["block", "unblock", "repair-dependency", "follow-up"])
|
package/dist/server/settings.js
CHANGED
|
@@ -105,6 +105,7 @@ import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from
|
|
|
105
105
|
import { ssoSettingsHtml } from "../sso-ui.js";
|
|
106
106
|
import { lastSweep, saveSweep, storageSweepOff } from "../storage-sweep.js";
|
|
107
107
|
import { storageHtml } from "../storage-ui.js";
|
|
108
|
+
import { landedSummary } from "../landed-cleanup.js";
|
|
108
109
|
import { bytesWords, parseCleanup } from "../storage.js";
|
|
109
110
|
import { isDigestTime, RESULT_SCREENSHOTS } from "../store.js";
|
|
110
111
|
import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
|
|
@@ -398,7 +399,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
398
399
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
399
400
|
const plan = await checkoutPlan(store, storagePool(databaseFile), now, { manual: true });
|
|
400
401
|
const sweep = { last: lastSweep(store), off: storageSweepOff() };
|
|
401
|
-
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
402
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
402
403
|
}
|
|
403
404
|
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
404
405
|
// exactly what turning on would do, or what to fix first.
|
|
@@ -418,6 +418,15 @@ export function createTaskViews(runtime) {
|
|
|
418
418
|
const found = runtime.store.runsFor(ref.id).find(one => runIsLive(one));
|
|
419
419
|
return found === undefined ? null : found.id;
|
|
420
420
|
})(),
|
|
421
|
+
// Live steering: the Claude build that takes Pause right now, and where its latest pause stands.
|
|
422
|
+
livePause: (() => {
|
|
423
|
+
const run = ref === null ? null : runtime.store.liveInputRun(ref.id, now);
|
|
424
|
+
if (run === null)
|
|
425
|
+
return null;
|
|
426
|
+
const pause = runtime.store.livePauseOf(run);
|
|
427
|
+
const until = pause?.pausedAt == null || pause.limitMs === null ? null : new Date(Date.parse(pause.pausedAt) + pause.limitMs).toISOString();
|
|
428
|
+
return { run, state: pause?.state ?? null, until, limitMs: pause?.limitMs ?? null };
|
|
429
|
+
})(),
|
|
421
430
|
worker: (() => {
|
|
422
431
|
const runners = runtime.store.listRunners().filter(one => one.retiredAt === null);
|
|
423
432
|
const eligible = ref?.repo === null || ref?.repo === undefined
|
package/dist/server/tasks.js
CHANGED
|
@@ -46,6 +46,7 @@ import { runCostWords } from "../summary.js";
|
|
|
46
46
|
import { requestTaskStop, resumeTaskStop } from "../task-control.js";
|
|
47
47
|
import { templateByName } from "../templates.js";
|
|
48
48
|
import { TEXT_LIMITS } from "../text-limits.js";
|
|
49
|
+
import { agentQuestion, answerAgentQuestion, markAgentSeen } from "../agent-board.js";
|
|
49
50
|
import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
50
51
|
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
51
52
|
import { requestContext } from "./request-context.js";
|
|
@@ -458,6 +459,9 @@ export function createTasksHandlers(runtime) {
|
|
|
458
459
|
return sendScreen(response, 409, screen("Result changed", html `<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>`, { chrome: chromeFor(project, "runs") }));
|
|
459
460
|
}
|
|
460
461
|
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
462
|
+
// Opening a run's own result page is viewing it: it leaves this person's unread Done on the Agents board.
|
|
463
|
+
if (wantedId !== null && selected?.run != null)
|
|
464
|
+
markAgentSeen(store, who.name, selected.run.id, now);
|
|
461
465
|
// A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
|
|
462
466
|
// is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
|
|
463
467
|
// a reason.
|
|
@@ -1621,8 +1625,31 @@ export function createTasksHandlers(runtime) {
|
|
|
1621
1625
|
: "no such task";
|
|
1622
1626
|
return taskScreen(response, who, taskId, said, 400);
|
|
1623
1627
|
}
|
|
1628
|
+
// From the Agents board, back to the same agent (allow-listed: a run of this task, never a URL from the form).
|
|
1629
|
+
const run = Number(body.get("run") ?? "");
|
|
1630
|
+
if (body.get("return") === "agents" && Number.isSafeInteger(run) && store.getRun(run)?.taskRef === ref.id)
|
|
1631
|
+
return redirect(response, `/agents?run=${run}&said=steered`);
|
|
1624
1632
|
return redirect(response, taskHref(taskId));
|
|
1625
1633
|
}
|
|
1634
|
+
case "reply": {
|
|
1635
|
+
const body = readForm(posted, CONSOLE_FORMS.taskReply);
|
|
1636
|
+
// A reply is steering, so it is a browser session's act too.
|
|
1637
|
+
if (who.via !== "cookie")
|
|
1638
|
+
return refuse(response, who, 403, "replying to an agent is a browser session's act");
|
|
1639
|
+
// The same gates as a pause: the route's act scope already refuses a viewer; these say why, and the demo answers nothing.
|
|
1640
|
+
if (who.role !== "approver")
|
|
1641
|
+
return taskScreen(response, who, taskId, "your login can watch — replying to an agent is an approver's act", 403);
|
|
1642
|
+
if (store.isDemo())
|
|
1643
|
+
return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
|
|
1644
|
+
const id = Number(body.get("question") ?? "");
|
|
1645
|
+
const asked = Number.isSafeInteger(id) && id > 0 ? agentQuestion(store, id) : null;
|
|
1646
|
+
if (asked === null || asked.taskRef !== ref.id)
|
|
1647
|
+
return taskScreen(response, who, taskId, "that question is not on this task", 404);
|
|
1648
|
+
const answered = answerAgentQuestion(store, { question: id, answer: body.get("answer") ?? "", author: who.name, at: now,
|
|
1649
|
+
file: (task, note) => store.fileSteerNote(task, verifiedAuthor(who.name), note, now) });
|
|
1650
|
+
const said = answered.ok ? "replied" : answered.reason === "invalid-answer" || answered.reason === "not-saved" ? "reply-invalid" : answered.reason === "closed" ? "reply-closed" : "reply-stale";
|
|
1651
|
+
return redirect(response, `/agents?run=${asked.run}&said=${said}`);
|
|
1652
|
+
}
|
|
1626
1653
|
case "hold": {
|
|
1627
1654
|
const body = readForm(posted, CONSOLE_FORMS.taskHold);
|
|
1628
1655
|
const reason = (body.get("reason") ?? "").trim() || "held from the console";
|
|
@@ -2306,6 +2333,40 @@ export function createTasksHandlers(runtime) {
|
|
|
2306
2333
|
const back = body.get("return") ?? "";
|
|
2307
2334
|
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
2308
2335
|
}
|
|
2336
|
+
case "pause": {
|
|
2337
|
+
const body = readForm(posted, CONSOLE_FORMS.taskPause);
|
|
2338
|
+
// Pause, and Pause and steer (live steering): a browser session's act, an approver's act, naming the ONE run
|
|
2339
|
+
// the page showed under the page's own request id. It stops the running Claude build's turn and keeps the
|
|
2340
|
+
// build; a note that rides along is filed in the same transaction and sent right after. mode=resume continues a
|
|
2341
|
+
// paused build with no note. Never a stop.
|
|
2342
|
+
if (who.via !== "cookie")
|
|
2343
|
+
return refuse(response, who, 403, "pausing a build is a browser session's act");
|
|
2344
|
+
if (who.role !== "approver")
|
|
2345
|
+
return taskScreen(response, who, taskId, "your login can watch — pausing a build is an approver's act", 403);
|
|
2346
|
+
if (store.isDemo())
|
|
2347
|
+
return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
|
|
2348
|
+
const named = (body.get("run") ?? "").trim();
|
|
2349
|
+
if (!/^[0-9]{1,15}$/.test(named))
|
|
2350
|
+
return taskScreen(response, who, taskId, "which build? a pause names one exact run", 400);
|
|
2351
|
+
const note = (body.get("note") ?? "").trim();
|
|
2352
|
+
// Resume: the paused build goes on where it stopped; no note is filed.
|
|
2353
|
+
const asked = body.get("mode") === "resume"
|
|
2354
|
+
? store.requestResume({ taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web", requestId: body.get("request") ?? "" }, now)
|
|
2355
|
+
: store.requestPause({ taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web", requestId: body.get("request") ?? "", ...(note === "" ? {} : { note }) }, now);
|
|
2356
|
+
if (!asked.ok) {
|
|
2357
|
+
const words = {
|
|
2358
|
+
stale: `build #${named} has stopped taking notes — reload to see the current attempt`,
|
|
2359
|
+
"not-paused": `build #${named} isn't paused any more — reload to see where it stands`,
|
|
2360
|
+
"invalid-request": "that form was incomplete — reload and try again",
|
|
2361
|
+
duplicate: "that form was already used for another build — reload and try again",
|
|
2362
|
+
"task-finished": "this task is finished",
|
|
2363
|
+
"unknown-task": "no such task",
|
|
2364
|
+
};
|
|
2365
|
+
return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, asked.reason === "invalid-note" ? 400 : 409);
|
|
2366
|
+
}
|
|
2367
|
+
const back = body.get("return") ?? "";
|
|
2368
|
+
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
2369
|
+
}
|
|
2309
2370
|
case "resume-arm": {
|
|
2310
2371
|
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
2311
2372
|
return armTaskResume(response, who, taskId, (body.get("run") ?? "").trim(), body.get("return") === "chat" ? "chat" : "task", now);
|
|
@@ -2715,6 +2776,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2715
2776
|
"task.act.next": taskAct("next"),
|
|
2716
2777
|
"task.act.reopen": taskAct("reopen"),
|
|
2717
2778
|
"task.act.steer": taskAct("steer"),
|
|
2779
|
+
"task.act.reply": taskAct("reply"),
|
|
2718
2780
|
"task.act.accept-proof": taskAct("accept-proof"),
|
|
2719
2781
|
"task.act.accept-revision": taskAct("accept-revision"),
|
|
2720
2782
|
"task.act.reject-revision": taskAct("reject-revision"),
|
|
@@ -2724,6 +2786,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2724
2786
|
"task.act.merge": taskMerge,
|
|
2725
2787
|
"task.act.confirm-stopped": taskConfirmStopped,
|
|
2726
2788
|
"task.act.stop": taskAct("stop"),
|
|
2789
|
+
"task.act.pause": taskAct("pause"),
|
|
2727
2790
|
"task.act.resume-arm": taskAct("resume-arm"),
|
|
2728
2791
|
"task.act.resume": taskAct("resume"),
|
|
2729
2792
|
"task.act.block": taskAct("block"),
|
package/dist/shared-deps.d.ts
CHANGED
|
@@ -98,13 +98,18 @@ export declare function unusedShared(root: string, checkouts: readonly {
|
|
|
98
98
|
export declare function removeShared(path: string): boolean;
|
|
99
99
|
/**
|
|
100
100
|
* The daily sweep: remove each copy no checkout uses and none has linked for `unusedMs` (a week), and old debris.
|
|
101
|
-
* Right before a copy goes, who uses it
|
|
102
|
-
* between stays.
|
|
101
|
+
* Right before a copy goes, who uses it, when it was last linked and whether live work uses it (`spare`,
|
|
102
|
+
* storage-sweep.ts) are read again; a copy a checkout picked up in between stays. Removal never holds the event loop.
|
|
103
103
|
*/
|
|
104
104
|
export declare function pruneUnusedShared(root: string, checkouts: () => readonly {
|
|
105
105
|
path: string;
|
|
106
106
|
repo: string;
|
|
107
|
-
}[], now: Date, unusedMs?: number
|
|
107
|
+
}[], now: Date, unusedMs?: number, options?: {
|
|
108
|
+
spare?: (path: string) => boolean;
|
|
109
|
+
/** Deterministic race/failure seams; never supplied by the worker. */
|
|
110
|
+
quarantined?: (original: string, trash: string) => void | Promise<void>;
|
|
111
|
+
remove?: (path: string) => Promise<void>;
|
|
112
|
+
}): Promise<{
|
|
108
113
|
removed: SharedUse[];
|
|
109
114
|
failed: string[];
|
|
110
|
-
}
|
|
115
|
+
}>;
|