toolroll 0.9.60 → 0.9.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/agent-board.d.ts +208 -0
- package/dist/agent-board.js +454 -0
- package/dist/agent-fence.d.ts +19 -0
- package/dist/agent-fence.js +100 -8
- package/dist/agent-pulse.d.ts +43 -0
- package/dist/agent-pulse.js +214 -0
- package/dist/agent-turns.d.ts +52 -0
- package/dist/agent-turns.js +137 -0
- package/dist/agents-board.d.ts +101 -0
- package/dist/agents-board.js +225 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment.d.ts +4 -0
- package/dist/assignment.js +8 -6
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +29 -25
- package/dist/browser-crew.js +2 -0
- package/dist/browser-workspace.d.ts +79 -1
- package/dist/browser-workspace.js +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.js +113 -13
- package/dist/chat-channel.js +9 -0
- package/dist/chat-core.d.ts +6 -0
- package/dist/chat-core.js +8 -0
- package/dist/chat-delivery.js +22 -0
- package/dist/chat-messages.js +8 -1
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +2 -0
- package/dist/chat-voice.js +2 -0
- package/dist/checkout-cleanup.d.ts +3 -0
- package/dist/checkout-cleanup.js +11 -2
- package/dist/cli.js +28 -11
- package/dist/contracts/console-api.d.ts +3 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/contracts/lead-tools.d.ts +16 -2
- package/dist/contracts/lead-tools.js +5 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +14 -3
- package/dist/exec.d.ts +32 -0
- package/dist/exec.js +229 -10
- package/dist/invoke.js +110 -18
- package/dist/landed-cleanup.d.ts +170 -0
- package/dist/landed-cleanup.js +814 -0
- package/dist/lead-cli.js +22 -19
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +4 -4
- package/dist/lead-doors.js +45 -1
- package/dist/lead-tools.js +40 -7
- package/dist/live-input.d.ts +68 -0
- package/dist/live-input.js +254 -0
- package/dist/live.d.ts +1 -1
- package/dist/live.js +4 -2
- package/dist/maintenance.d.ts +2 -0
- package/dist/maintenance.js +5 -2
- package/dist/operate-remote-arguments.js +5 -1
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -26
- package/dist/orphan-sweep.d.ts +2 -0
- package/dist/orphan-sweep.js +5 -3
- package/dist/project-admission.d.ts +10 -6
- package/dist/project-admission.js +33 -11
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +32 -2
- package/dist/project-tools.d.ts +7 -11
- package/dist/project-tools.js +13 -37
- package/dist/provider.d.ts +65 -8
- package/dist/provider.js +80 -21
- package/dist/remote-main.d.ts +57 -0
- package/dist/remote-main.js +179 -0
- package/dist/result-completion.d.ts +7 -0
- package/dist/result-completion.js +9 -0
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +3 -3
- package/dist/serve.js +19 -1
- package/dist/server/live.d.ts +3 -0
- package/dist/server/live.js +35 -2
- package/dist/server/pages.js +59 -35
- package/dist/server/render-chat.js +12 -0
- package/dist/server/render-tasks.d.ts +15 -2
- package/dist/server/render-tasks.js +30 -7
- package/dist/server/route-table.js +4 -1
- package/dist/server/settings.js +5 -3
- package/dist/server/task-views.js +11 -0
- package/dist/server/tasks.js +63 -0
- package/dist/shared-deps.d.ts +9 -4
- package/dist/shared-deps.js +89 -13
- package/dist/staged-runtimes.d.ts +16 -4
- package/dist/staged-runtimes.js +160 -38
- package/dist/steer-live.d.ts +19 -0
- package/dist/steer-live.js +93 -0
- package/dist/steer-status.d.ts +8 -2
- package/dist/steer-status.js +19 -5
- package/dist/storage-sweep-child.d.ts +1 -0
- package/dist/storage-sweep-child.js +76 -0
- package/dist/storage-sweep-process.d.ts +48 -0
- package/dist/storage-sweep-process.js +204 -0
- package/dist/storage-sweep-schedule.d.ts +30 -0
- package/dist/storage-sweep-schedule.js +164 -0
- package/dist/storage-sweep.d.ts +31 -6
- package/dist/storage-sweep.js +165 -39
- package/dist/storage-ui.d.ts +4 -0
- package/dist/storage-ui.js +22 -7
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +35 -0
- package/dist/store.d.ts +200 -3
- package/dist/store.js +443 -17
- package/dist/surface.js +8 -4
- package/dist/sweep-quarantine.d.ts +28 -0
- package/dist/sweep-quarantine.js +153 -0
- package/dist/task-base.d.ts +35 -0
- package/dist/task-base.js +51 -0
- package/dist/task-status.d.ts +2 -0
- package/dist/task-status.js +5 -1
- package/dist/test-temp.d.ts +6 -3
- package/dist/test-temp.js +15 -9
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.d.ts +1 -0
- package/dist/work-index.js +32 -12
- package/dist/work-summary.d.ts +2 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +57 -3
- package/dist/worktree.js +201 -23
- package/package.json +1 -1
- package/scripts/provider-canary.mjs +235 -147
package/dist/surface.js
CHANGED
|
@@ -67,9 +67,11 @@ const GUIDE_ROWS = [
|
|
|
67
67
|
// D5: the central team service (connect, the central lead verbs, conversation …) and native coding sessions are
|
|
68
68
|
// deprecated: they still run this release, with a warning, but are no longer declared here.
|
|
69
69
|
operator("chat", "talk to your lead: one conversation across every project; --say sends one message; --follow enables crew updates and --no-follow pauses them; proposals keep their approval requirements"),
|
|
70
|
-
|
|
70
|
+
// Unkeyed: --remote with a different URL replaces the project's trusted remote (recorded in the action ledger).
|
|
71
|
+
{ invocation: "repos add", synopsis: "add a project: a Git repository, in the console at once; the lead adds with its token; --remote records or replaces its trusted remote", audience: "agent", agentMayInvoke: true, mutation: "unkeyed",
|
|
71
72
|
positionals: [{ name: "path", required: false, meaning: "the repository (default: the one you are in)" }],
|
|
72
|
-
flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }
|
|
73
|
+
flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" },
|
|
74
|
+
{ name: "remote", takesValue: true, meaning: "an HTTPS or SSH URL without credentials whose main may complete landed work; replaces a different recorded one (owner or an instance operator's lead only)" }],
|
|
73
75
|
notableReasons: ["not-git", "unavailable", "refused", "locked"] },
|
|
74
76
|
{ invocation: "repos remove", synopsis: "remove a project from the console and the builder; its tasks and results stay saved", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
|
|
75
77
|
positionals: [{ name: "path", required: true, meaning: "the repository" }],
|
|
@@ -242,7 +244,9 @@ const GUIDE_ROWS = [
|
|
|
242
244
|
positionals: [{ name: "id", required: true, meaning: "the task" }],
|
|
243
245
|
flags: [jsonFlag, dbFlag, keyFlag, { name: "undo", takesValue: false, meaning: "back to filing order" }],
|
|
244
246
|
notableReasons: ["unknown-task", "claimed"] },
|
|
245
|
-
operator("task steer", "one credentialed note for the task's agent — a running build reads it when it next checks for notes, otherwise the next attempt does; steering speaks with the operator's voice"),
|
|
247
|
+
operator("task steer", "one credentialed note for the task's agent — a running Claude build gets it in seconds, another running build reads it when it next checks for notes, otherwise the next attempt does; --now pauses the running Claude build first; steering speaks with the operator's voice"),
|
|
248
|
+
operator("task pause", "stop a running Claude build's current turn and keep the build (--run <id> names the attempt): its next note continues it; never a stop"),
|
|
249
|
+
operator("task continue", "resume a paused Claude build where it stopped, without a note (--run <id> names the attempt)"),
|
|
246
250
|
{ invocation: "task assign", synopsis: "reserve a task for one worker, or return it to the shared queue", audience: "agent", agentMayInvoke: true, mutation: "keyed",
|
|
247
251
|
positionals: [{ name: "id", required: true, meaning: "the task" }],
|
|
248
252
|
flags: [jsonFlag, dbFlag, keyFlag,
|
|
@@ -449,7 +453,7 @@ const REMOTE_STEP_UP = [
|
|
|
449
453
|
const REMOTE_YES = [
|
|
450
454
|
"status", "integrations", "ready", "gaps", "grants", "sync", "audit",
|
|
451
455
|
"task add", "task ask", "task checks", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
|
|
452
|
-
"task state", "task block", "task unblock", "task next", "task steer", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
|
|
456
|
+
"task state", "task block", "task unblock", "task next", "task steer", "task pause", "task continue", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
|
|
453
457
|
"task requeue", "task review", "task repair", "task route", "task reopen", "task stop", "task resume",
|
|
454
458
|
"runner list", "coordinator list", "cap list", "cap add", "outbox list", "incident list", "incident resolve",
|
|
455
459
|
"config show", "verify show", "intake show", "intake run", "intake preview", "intake pr-comments", "template list", "template show",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const QUARANTINE_NOTE = ".toolroll-sweep.json";
|
|
2
|
+
type Journal<T> = {
|
|
3
|
+
version: 1;
|
|
4
|
+
name: string;
|
|
5
|
+
data: T;
|
|
6
|
+
deleting: boolean;
|
|
7
|
+
pid: number;
|
|
8
|
+
host: string;
|
|
9
|
+
bootId: string | null;
|
|
10
|
+
};
|
|
11
|
+
export type Quarantine<T> = {
|
|
12
|
+
path: string;
|
|
13
|
+
original: string;
|
|
14
|
+
journal: Journal<T>;
|
|
15
|
+
};
|
|
16
|
+
export declare function quarantineEntries<T>(root: string): Quarantine<T>[];
|
|
17
|
+
/** A small marker closes discovery before the atomic rename. It is never published half-written. */
|
|
18
|
+
export declare function quarantine<T>(original: string, data: T): Quarantine<T> | null;
|
|
19
|
+
/** Returns null if another sweep owns it (or won recovery). */
|
|
20
|
+
export declare function claimQuarantine<T>(entry: Quarantine<T>): Quarantine<T> | null;
|
|
21
|
+
export declare function releaseQuarantine<T>(entry: Quarantine<T>): void;
|
|
22
|
+
/** A partially deleted tree must never become discoverable again. */
|
|
23
|
+
export declare function restoreQuarantine<T>(entry: Quarantine<T>): boolean;
|
|
24
|
+
/** Keep the journal until all potentially large removals succeeded. Recovery never guesses at a half-deleted tree. */
|
|
25
|
+
export declare function deleteQuarantine<T>(entry: Quarantine<T>, remove: (path: string) => void | Promise<void>): Promise<void>;
|
|
26
|
+
/** Staged pruning is also called synchronously by update/deploy. */
|
|
27
|
+
export declare function deleteQuarantineSync<T>(entry: Quarantine<T>, remove: (path: string) => void): void;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Atomic rename before deletion. Trash is never a reusable dependency copy or staged runtime.
|
|
2
|
+
* Its journal survives partial deletion so the next sweep can recheck the original identity and retry.
|
|
3
|
+
* No database write lock spans filesystem work. A live owner keeps its trash; recovery claims a dead
|
|
4
|
+
* owner's folder by renaming it to a fresh, never-reused name, so two recoverers cannot both win. */
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { existsSync, linkSync, lstatSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { hostname } from "node:os";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
10
|
+
export const QUARANTINE_NOTE = ".toolroll-sweep.json";
|
|
11
|
+
const PREFIX = ".toolroll-trash-";
|
|
12
|
+
const freshPath = (root) => join(root, `${PREFIX}${randomBytes(16).toString("hex")}`);
|
|
13
|
+
const owner = () => ({ pid: process.pid, host: hostname(), bootId: currentBootId() });
|
|
14
|
+
function alive(one) {
|
|
15
|
+
if (one.pid === 0)
|
|
16
|
+
return false;
|
|
17
|
+
if (one.host !== hostname())
|
|
18
|
+
return true;
|
|
19
|
+
if (one.bootId !== null && provenDeadByBootChange(one))
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(one.pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return error.code !== "ESRCH";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function read(path) {
|
|
30
|
+
try {
|
|
31
|
+
const j = JSON.parse(readFileSync(join(path, QUARANTINE_NOTE), "utf8"));
|
|
32
|
+
return j.version === 1 && typeof j.name === "string" && j.name !== "." && j.name !== ".." &&
|
|
33
|
+
j.name !== "" && !/[\\/\0]/.test(j.name) && Number.isSafeInteger(j.pid) && j.pid >= 0 && typeof j.host === "string" &&
|
|
34
|
+
(j.bootId === null || typeof j.bootId === "string") && typeof j.deleting === "boolean" ? j : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function write(path, journal, exclusive = false) {
|
|
41
|
+
const note = join(path, QUARANTINE_NOTE), temp = `${note}.${randomBytes(8).toString("hex")}`;
|
|
42
|
+
writeFileSync(temp, JSON.stringify(journal), { mode: 0o600, flag: "wx" });
|
|
43
|
+
try {
|
|
44
|
+
if (exclusive)
|
|
45
|
+
linkSync(temp, note);
|
|
46
|
+
else
|
|
47
|
+
renameSync(temp, note);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
unlinkSync(temp);
|
|
52
|
+
}
|
|
53
|
+
catch { /* renamed */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function quarantineEntries(root) {
|
|
57
|
+
let names;
|
|
58
|
+
try {
|
|
59
|
+
names = readdirSync(root);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return names.filter(name => /^\.toolroll-trash-[0-9a-f]{32}$/.test(name)).flatMap(name => {
|
|
65
|
+
const path = join(root, name);
|
|
66
|
+
try {
|
|
67
|
+
if (!lstatSync(path).isDirectory())
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const journal = read(path);
|
|
74
|
+
// A crash after unlinking the journal can leave only an empty folder. No resource data remains in it.
|
|
75
|
+
if (journal === null) {
|
|
76
|
+
try {
|
|
77
|
+
if (readdirSync(path).length === 0)
|
|
78
|
+
rmdirSync(path);
|
|
79
|
+
}
|
|
80
|
+
catch { /* next pass */ }
|
|
81
|
+
}
|
|
82
|
+
return journal === null ? [] : [{ path, original: join(root, journal.name), journal }];
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** A small marker closes discovery before the atomic rename. It is never published half-written. */
|
|
86
|
+
export function quarantine(original, data) {
|
|
87
|
+
if (!lstatSync(original).isDirectory())
|
|
88
|
+
return null;
|
|
89
|
+
let journal = read(original);
|
|
90
|
+
if (journal !== null && alive(journal))
|
|
91
|
+
return null;
|
|
92
|
+
if (journal === null) {
|
|
93
|
+
journal = { version: 1, name: basename(original), data, deleting: false, ...owner() };
|
|
94
|
+
write(original, journal, true);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
journal = { ...journal, ...owner() };
|
|
98
|
+
write(original, journal);
|
|
99
|
+
}
|
|
100
|
+
const path = freshPath(dirname(original));
|
|
101
|
+
renameSync(original, path);
|
|
102
|
+
return { path, original, journal };
|
|
103
|
+
}
|
|
104
|
+
/** Returns null if another sweep owns it (or won recovery). */
|
|
105
|
+
export function claimQuarantine(entry) {
|
|
106
|
+
const seen = read(entry.path);
|
|
107
|
+
if (seen === null || alive(seen))
|
|
108
|
+
return null;
|
|
109
|
+
const journal = { ...seen, ...owner() };
|
|
110
|
+
const path = freshPath(dirname(entry.path));
|
|
111
|
+
try {
|
|
112
|
+
// Mark live before moving. Contenders already reading the old name race the rename; just one wins.
|
|
113
|
+
write(entry.path, journal);
|
|
114
|
+
renameSync(entry.path, path);
|
|
115
|
+
write(path, journal);
|
|
116
|
+
return { ...entry, path, journal };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function releaseQuarantine(entry) {
|
|
123
|
+
if (existsSync(entry.path))
|
|
124
|
+
write(entry.path, { ...entry.journal, pid: 0 });
|
|
125
|
+
}
|
|
126
|
+
/** A partially deleted tree must never become discoverable again. */
|
|
127
|
+
export function restoreQuarantine(entry) {
|
|
128
|
+
if (entry.journal.deleting || existsSync(entry.original))
|
|
129
|
+
return false;
|
|
130
|
+
renameSync(entry.path, entry.original);
|
|
131
|
+
unlinkSync(join(entry.original, QUARANTINE_NOTE));
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/** Keep the journal until all potentially large removals succeeded. Recovery never guesses at a half-deleted tree. */
|
|
135
|
+
export async function deleteQuarantine(entry, remove) {
|
|
136
|
+
entry.journal = { ...entry.journal, deleting: true };
|
|
137
|
+
write(entry.path, entry.journal);
|
|
138
|
+
for (const name of readdirSync(entry.path))
|
|
139
|
+
if (name !== QUARANTINE_NOTE)
|
|
140
|
+
await remove(join(entry.path, name));
|
|
141
|
+
unlinkSync(join(entry.path, QUARANTINE_NOTE));
|
|
142
|
+
rmdirSync(entry.path);
|
|
143
|
+
}
|
|
144
|
+
/** Staged pruning is also called synchronously by update/deploy. */
|
|
145
|
+
export function deleteQuarantineSync(entry, remove) {
|
|
146
|
+
entry.journal = { ...entry.journal, deleting: true };
|
|
147
|
+
write(entry.path, entry.journal);
|
|
148
|
+
for (const name of readdirSync(entry.path))
|
|
149
|
+
if (name !== QUARANTINE_NOTE)
|
|
150
|
+
remove(join(entry.path, name));
|
|
151
|
+
unlinkSync(join(entry.path, QUARANTINE_NOTE));
|
|
152
|
+
rmdirSync(entry.path);
|
|
153
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a task's work starts, against current main (fresh base): one view for
|
|
3
|
+
* the task page and `task show`. The base is saved run lineage — a builder
|
|
4
|
+
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
|
+
* read from Git now. Reads only.
|
|
6
|
+
*/
|
|
7
|
+
import type { Store } from "./store.js";
|
|
8
|
+
export type TaskBaseView = {
|
|
9
|
+
/** The base commit, in full. */
|
|
10
|
+
base: string;
|
|
11
|
+
branch: string;
|
|
12
|
+
/** The project's main: its checked-out branch, as a dispatch pass resolves it. Null when Git could not say. */
|
|
13
|
+
main: string | null;
|
|
14
|
+
/** Commits on main the base does not have; null when Git could not say. */
|
|
15
|
+
behind: number | null;
|
|
16
|
+
/** One short line: `Base 9d087efe · Behind main by 2 commits`. */
|
|
17
|
+
words: string;
|
|
18
|
+
};
|
|
19
|
+
/** A synchronous, read-only Git call in `cwd`: its trimmed output, or null on any failure. */
|
|
20
|
+
export type GitRead = (cwd: string, args: readonly string[]) => string | null;
|
|
21
|
+
export declare const readGit: GitRead;
|
|
22
|
+
/** The base line on its own, for surfaces that label it already: `9d087efe · Behind main by 2 commits`. */
|
|
23
|
+
export declare function baseStandingWords(view: Pick<TaskBaseView, "base" | "main" | "behind">): string;
|
|
24
|
+
/**
|
|
25
|
+
* The lease's fresh-base request for one attempt on `branch`, or nothing when the task's start is pinned elsewhere: a
|
|
26
|
+
* revision keeps the head of the attempt it revises, and a prepared candidate or coding review keeps the base its
|
|
27
|
+
* candidate was proved against.
|
|
28
|
+
*/
|
|
29
|
+
export declare function freshBaseFor(store: Store, taskId: string, branch: string, main: string): {
|
|
30
|
+
freshBase: {
|
|
31
|
+
main: string;
|
|
32
|
+
recordedBase: string | null;
|
|
33
|
+
};
|
|
34
|
+
} | Record<string, never>;
|
|
35
|
+
export declare function taskBaseOf(store: Store, taskId: string, git?: GitRead): TaskBaseView | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a task's work starts, against current main (fresh base): one view for
|
|
3
|
+
* the task page and `task show`. The base is saved run lineage — a builder
|
|
4
|
+
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
|
+
* read from Git now. Reads only.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
export const readGit = (cwd, args) => {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const commits = (count) => `${count} commit${count === 1 ? "" : "s"}`;
|
|
17
|
+
/** The base line on its own, for surfaces that label it already: `9d087efe · Behind main by 2 commits`. */
|
|
18
|
+
export function baseStandingWords(view) {
|
|
19
|
+
const short = view.base.slice(0, 8);
|
|
20
|
+
if (view.main === null || view.behind === null)
|
|
21
|
+
return short;
|
|
22
|
+
return view.behind === 0 ? `${short} · Up to date with ${view.main}` : `${short} · Behind ${view.main} by ${commits(view.behind)}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The lease's fresh-base request for one attempt on `branch`, or nothing when the task's start is pinned elsewhere: a
|
|
26
|
+
* revision keeps the head of the attempt it revises, and a prepared candidate or coding review keeps the base its
|
|
27
|
+
* candidate was proved against.
|
|
28
|
+
*/
|
|
29
|
+
export function freshBaseFor(store, taskId, branch, main) {
|
|
30
|
+
const ref = store.lookupRef(taskId);
|
|
31
|
+
if (ref === null || ref.revisionOf !== null || store.getScope(taskId)?.candidate != null || store.filedViaOf(taskId)?.startsWith("coding:") === true)
|
|
32
|
+
return {};
|
|
33
|
+
return { freshBase: { main, recordedBase: store.recordedBranchBase(ref.id, branch) } };
|
|
34
|
+
}
|
|
35
|
+
export function taskBaseOf(store, taskId, git = readGit) {
|
|
36
|
+
const ref = store.lookupRef(taskId);
|
|
37
|
+
if (ref === null || ref.repo === null)
|
|
38
|
+
return null;
|
|
39
|
+
const latest = store.runsFor(ref.id).find(one => one.role !== "reviewer" && one.branch !== null && one.baseRevision !== null);
|
|
40
|
+
if (latest === undefined || latest.branch === null || latest.baseRevision === null)
|
|
41
|
+
return null;
|
|
42
|
+
const branch = latest.branch;
|
|
43
|
+
// A move to current main newer than the latest attempt is where the next one starts.
|
|
44
|
+
const move = store.latestBranchBaseMove(ref.id, branch);
|
|
45
|
+
const base = move !== null && move.afterRun >= latest.id ? move.to : store.recordedBranchBase(ref.id, branch) ?? latest.baseRevision;
|
|
46
|
+
const main = git(ref.repo, ["symbolic-ref", "--short", "-q", "HEAD"]) || null;
|
|
47
|
+
const counted = main === null ? null : git(ref.repo, ["rev-list", "--count", `${base}..refs/heads/${main}`]);
|
|
48
|
+
const behind = counted === null || !/^\d+$/.test(counted) ? null : Number(counted);
|
|
49
|
+
const view = { base, branch, main: behind === null ? null : main, behind };
|
|
50
|
+
return { ...view, words: `Base ${baseStandingWords(view)}` };
|
|
51
|
+
}
|
package/dist/task-status.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ export type TaskStatusFacts = {
|
|
|
134
134
|
damaged: number;
|
|
135
135
|
} | null;
|
|
136
136
|
completedBy?: string | null;
|
|
137
|
+
/** Marked complete by Toolroll because its work landed: why, in a few words ("its pull request merged"). */
|
|
138
|
+
landed?: string | null;
|
|
137
139
|
action?: StatusAction | null;
|
|
138
140
|
/** Where a row's one action leads: the result (and its Checks tab), the task's pull request, Run checks. */
|
|
139
141
|
links?: {
|
package/dist/task-status.js
CHANGED
|
@@ -127,6 +127,9 @@ function sentenceOf(headline, facts) {
|
|
|
127
127
|
return "Review the change, then mark it complete.";
|
|
128
128
|
return "Built without a passing project check. Review the change, then mark it complete.";
|
|
129
129
|
case "Complete": {
|
|
130
|
+
// Landed work says why once; its merged pull request is that reason, not a second sentence.
|
|
131
|
+
if (facts.landed)
|
|
132
|
+
return `Marked complete automatically: ${facts.landed}.`;
|
|
130
133
|
const by = facts.completedBy ? `Marked complete by ${facts.completedBy}.` : "Marked complete.";
|
|
131
134
|
const pr = facts.pullRequest;
|
|
132
135
|
return pr?.state === "merged" ? `${by} ${pr.number === null ? "Its pull request" : `Pull request #${pr.number}`} merged.` : by;
|
|
@@ -488,7 +491,8 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
488
491
|
...(withResult && receipt?.proofAcceptance != null ? { accepted: true } : {}),
|
|
489
492
|
...(withResult && (assignment.readiness?.blockers.length ?? 0) > 0 ? { blockers: assignment.readiness.blockers } : {}),
|
|
490
493
|
evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
|
|
491
|
-
completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
|
|
494
|
+
completedBy: assignment.completion === null || assignment.completion.landed !== undefined ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
|
|
495
|
+
...(assignment.completion?.landed === undefined ? {} : { landed: assignment.completion.landed }),
|
|
492
496
|
action: options.action ?? null,
|
|
493
497
|
...(options.links === undefined ? {} : { links: options.links }),
|
|
494
498
|
why: options.why ?? [],
|
package/dist/test-temp.d.ts
CHANGED
|
@@ -25,13 +25,16 @@ export type RemoveOptions = {
|
|
|
25
25
|
ownerAlive?: (owner: TempOwner) => boolean;
|
|
26
26
|
/** At most this many go in one pass (a sweep after weeks can find 100,000); `more` says some were left. */
|
|
27
27
|
max?: number;
|
|
28
|
+
/** Something live work uses (storage-sweep.ts): asked right before each folder goes; true keeps it. */
|
|
29
|
+
spare?: (path: string) => boolean;
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
|
|
31
|
-
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone
|
|
33
|
+
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone, nothing
|
|
34
|
+
* live using it). Removal never holds the event loop: a worker's heartbeats keep beating while thousands go.
|
|
32
35
|
*/
|
|
33
|
-
export declare function removeStaleTestTemp(roots: readonly string[], now: Date, options?: RemoveOptions): {
|
|
36
|
+
export declare function removeStaleTestTemp(roots: readonly string[], now: Date, options?: RemoveOptions): Promise<{
|
|
34
37
|
removed: string[];
|
|
35
38
|
failed: string[];
|
|
36
39
|
more: boolean;
|
|
37
|
-
}
|
|
40
|
+
}>;
|
package/dist/test-temp.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* Every prefix the tests and scripts give tmpdir() is covered here (test-temp.test.ts reads the sources to keep it
|
|
9
9
|
* so), and Playwright's own browser profiles.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import { lstatSync, readFileSync, readdirSync
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
13
|
+
import { rm } from "node:fs/promises";
|
|
13
14
|
import { tmpdir } from "node:os";
|
|
14
15
|
import { dirname, join, resolve } from "node:path";
|
|
15
16
|
const DAY_MS = 86_400_000;
|
|
@@ -96,9 +97,10 @@ export function testTempFolders(roots, now) {
|
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
99
|
* Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
|
|
99
|
-
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone
|
|
100
|
+
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone, nothing
|
|
101
|
+
* live using it). Removal never holds the event loop: a worker's heartbeats keep beating while thousands go.
|
|
100
102
|
*/
|
|
101
|
-
export function removeStaleTestTemp(roots, now, options = {}) {
|
|
103
|
+
export async function removeStaleTestTemp(roots, now, options = {}) {
|
|
102
104
|
const removed = [];
|
|
103
105
|
const failed = [];
|
|
104
106
|
const stale = testTempFolders(roots, now).stale;
|
|
@@ -107,6 +109,7 @@ export function removeStaleTestTemp(roots, now, options = {}) {
|
|
|
107
109
|
for (const one of stale) {
|
|
108
110
|
if (removed.length + failed.length >= max)
|
|
109
111
|
return { removed, failed, more: true };
|
|
112
|
+
await nextTurn();
|
|
110
113
|
try {
|
|
111
114
|
const stat = lstatSync(one.path);
|
|
112
115
|
if (stat.isSymbolicLink() || !inside.has(resolve(dirname(one.path))))
|
|
@@ -116,7 +119,9 @@ export function removeStaleTestTemp(roots, now, options = {}) {
|
|
|
116
119
|
const owner = stat.isDirectory() ? tempOwner(one.path) : null;
|
|
117
120
|
if (owner !== null && (options.ownerAlive ?? (() => false))(owner))
|
|
118
121
|
continue;
|
|
119
|
-
|
|
122
|
+
if (options.spare?.(one.path) === true)
|
|
123
|
+
continue;
|
|
124
|
+
await removeTree(one.path);
|
|
120
125
|
removed.push(one.path);
|
|
121
126
|
}
|
|
122
127
|
catch {
|
|
@@ -125,14 +130,15 @@ export function removeStaleTestTemp(roots, now, options = {}) {
|
|
|
125
130
|
}
|
|
126
131
|
return { removed, failed, more: false };
|
|
127
132
|
}
|
|
133
|
+
const nextTurn = () => new Promise(resolve => setImmediate(resolve));
|
|
128
134
|
/** rm -r; a tree a test made read-only (a shared dependency copy, a locked fixture) is made writable first. */
|
|
129
|
-
function removeTree(path) {
|
|
135
|
+
async function removeTree(path) {
|
|
130
136
|
try {
|
|
131
|
-
|
|
137
|
+
await rm(path, { recursive: true, force: true, maxRetries: 3 });
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
catch { /* read-only inside */ }
|
|
135
141
|
if (process.platform !== "win32")
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
await new Promise(resolve => execFile("chmod", ["-R", "u+w", path], { timeout: 120_000 }, () => resolve()));
|
|
143
|
+
await rm(path, { recursive: true, force: true, maxRetries: 3 });
|
|
138
144
|
}
|
package/dist/toolroll-update.js
CHANGED
|
@@ -35,7 +35,7 @@ import { processMayBeAlive } from "./process-liveness.js";
|
|
|
35
35
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
36
36
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
37
37
|
import { NAME } from "./names.js";
|
|
38
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS } from "./store.js";
|
|
38
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS } from "./store.js";
|
|
39
39
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
40
40
|
import { markNeverIndex } from "./never-index.js";
|
|
41
41
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -398,7 +398,9 @@ const HISTORY_RULES = {
|
|
|
398
398
|
// the declared value (generation 0, no closure reason).
|
|
399
399
|
// v120: a steering note's mailing and acknowledgement, and a run's steering folder, are new columns; every row
|
|
400
400
|
// saved before them reads none.
|
|
401
|
-
|
|
401
|
+
// v121: live steering's columns and a run's attempt key read none, and no sends, the same way.
|
|
402
|
+
// v122: a project's recorded remote reads none the same way.
|
|
403
|
+
...addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS),
|
|
402
404
|
};
|
|
403
405
|
/** The tables a migration may remove whole (v115's removed features). */
|
|
404
406
|
export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
|
package/dist/work-index.d.ts
CHANGED
package/dist/work-index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { shownVerdictOf } from './proof.js';
|
|
|
16
16
|
import { isCheckLevel, requiredCheckCommandFor } from './check-levels.js';
|
|
17
17
|
import { MARKER } from './worktree.js';
|
|
18
18
|
import { withFollowUps } from './result-follow-ups.js';
|
|
19
|
+
import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
|
|
19
20
|
import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
20
21
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
21
22
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
@@ -26,6 +27,9 @@ const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is
|
|
|
26
27
|
const VIEWS = ['all', 'needs-you', 'running', 'completed'];
|
|
27
28
|
const n = (row, key) => Number(row[key] ?? 0);
|
|
28
29
|
const s = (row, key) => row[key] == null ? null : String(row[key]);
|
|
30
|
+
/** The running agent's oldest open question (agent-board.ts), from its open newest attempt only. */
|
|
31
|
+
const AGENT_ASK = `(SELECT q.id FROM agent_question q JOIN run qr ON qr.id=q.run WHERE q.task_ref=a.ref_id AND q.closed_at IS NULL
|
|
32
|
+
AND qr.outcome IS NULL AND qr.attempt_key=q.attempt_key ORDER BY q.id LIMIT 1)`;
|
|
29
33
|
/** Keep the lineage predicates identical to Store.taskFamilyProjection: an
|
|
30
34
|
* admitted child needs its parent's exact revision brief, in the same repo.
|
|
31
35
|
* All subsequent reads are scalar/indexed metadata, never artifact bytes.
|
|
@@ -88,7 +92,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
88
92
|
(SELECT id FROM run WHERE task_ref=a.ref_id AND outcome IS NULL ORDER BY id DESC LIMIT 1) unfinished,
|
|
89
93
|
CASE WHEN a.state='done' THEN (SELECT id FROM run WHERE task_ref=a.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1) END result_id,
|
|
90
94
|
(SELECT h.id FROM hold h WHERE h.task_ref=a.ref_id AND (h.until IS NULL OR h.until>$now) ORDER BY h.held_at,h.id LIMIT 1) hold_id,
|
|
91
|
-
q.decision_id,(cu.task_ref IS NOT NULL OR a.ref_id IN (SELECT value FROM json_each($workspaceCustody)))
|
|
95
|
+
q.decision_id,${AGENT_ASK} agent_ask,(cu.task_ref IS NOT NULL OR a.ref_id IN (SELECT value FROM json_each($workspaceCustody)))
|
|
92
96
|
AND a.ref_id NOT IN (SELECT value FROM json_each($settledCustody)) custody_unresolved
|
|
93
97
|
FROM admitted a LEFT JOIN lineage l ON l.ref_id=a.ref_id
|
|
94
98
|
LEFT JOIN questions q ON q.task_ref=a.ref_id LEFT JOIN custody cu ON cu.task_ref=a.ref_id
|
|
@@ -186,7 +190,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
186
190
|
AND a.action='assignment handoff checked' AND a.source='work' AND a.repo IS f.repo
|
|
187
191
|
AND length(a.outcome)=64 AND a.outcome NOT GLOB '*[^a-f0-9]*'
|
|
188
192
|
AND a.at>=f.family_updated AND a.at>=f.finished_at AND a.at>=f.approved_at AND a.at>=f.proposed_at
|
|
189
|
-
AND (a.actor GLOB 'operator:?*' OR a.actor GLOB 'coordinator:?*' OR a.actor GLOB 'lead:?*')
|
|
193
|
+
AND (a.actor GLOB 'operator:?*' OR a.actor GLOB 'coordinator:?*' OR a.actor GLOB 'lead:?*' OR a.actor='${LANDED_ACTOR}')
|
|
190
194
|
AND (cm.changed_id IS NULL OR a.id>=cm.changed_id) AND (cm.artifact_at IS NULL OR a.at>=cm.artifact_at)
|
|
191
195
|
AND NOT EXISTS(SELECT 1 FROM proof_verdict p WHERE p.run=f.result_id AND p.decided_at>a.at)
|
|
192
196
|
AND NOT EXISTS(SELECT 1 FROM proof_acceptance p WHERE p.run=f.result_id AND p.accepted_at>a.at)
|
|
@@ -203,7 +207,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
203
207
|
WHEN state='done' THEN 'result-needs-attention'
|
|
204
208
|
WHEN custody_unresolved AND live_lease IS NULL THEN 'process-needs-attention'
|
|
205
209
|
WHEN state='failed' THEN 'failed'
|
|
206
|
-
WHEN live_lease IS NOT NULL THEN CASE WHEN question_id IS NOT NULL THEN 'waiting-decision' ELSE 'running' END
|
|
210
|
+
WHEN live_lease IS NOT NULL THEN CASE WHEN question_id IS NOT NULL OR agent_ask IS NOT NULL THEN 'waiting-decision' ELSE 'running' END
|
|
207
211
|
WHEN state='running' OR unfinished IS NOT NULL THEN 'vanished-run'
|
|
208
212
|
WHEN EXISTS(SELECT 1 FROM sqlite_master WHERE type='trigger' AND name GLOB 'so_desktop_update_*') THEN 'updating'
|
|
209
213
|
WHEN hold_kind='backoff' THEN 'retry-scheduled'
|
|
@@ -239,7 +243,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
239
243
|
WHEN ${CUSTODY_WAITS} OR code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
240
244
|
${ASK_RANK} ask_rank,
|
|
241
245
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
242
|
-
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
|
|
246
|
+
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest,completed.detail checked_detail
|
|
243
247
|
FROM claimed c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
|
|
244
248
|
)`;
|
|
245
249
|
const registered = new WeakMap();
|
|
@@ -280,7 +284,11 @@ function registerValidators(store) {
|
|
|
280
284
|
const available = new Set(db.prepare(`SELECT name FROM sqlite_master WHERE type='index'
|
|
281
285
|
AND name IN ('work_open_decision','work_spawned_run','work_unsettled_custody','work_result')`)
|
|
282
286
|
.all().map(row => String(row['name'])));
|
|
283
|
-
const
|
|
287
|
+
const indexed = available.size === 4 ? PROJECTION : PROJECTION.replace(/ INDEXED BY (work_open_decision|work_spawned_run|work_unsettled_custody|work_result)\b/g, (hint, name) => available.has(name) ? hint : '');
|
|
288
|
+
// A same-schema reader whose file has not taken the Agents board's tables yet reads no agent questions.
|
|
289
|
+
const asks = db.prepare("SELECT 1 AS hit FROM sqlite_master WHERE type='table' AND name='agent_question'").get() !== undefined;
|
|
290
|
+
const projection = asks ? indexed : indexed.replace(AGENT_ASK, 'NULL')
|
|
291
|
+
.replace(`'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),`, '');
|
|
284
292
|
registered.set(db, projection);
|
|
285
293
|
return projection;
|
|
286
294
|
}
|
|
@@ -398,10 +406,11 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
398
406
|
AND f.action='checks finished' AND f.outcome LIKE q.id||':%' LIMIT 1),'open')) FROM (SELECT id,outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks requested' ORDER BY id) q),
|
|
399
407
|
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
400
408
|
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
401
|
-
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
|
|
409
|
+
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'checked_detail',checked_detail,'question_id',question_id,
|
|
402
410
|
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
|
|
403
411
|
'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
404
|
-
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'
|
|
412
|
+
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'agent_ask',page.agent_ask,
|
|
413
|
+
'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
405
414
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
|
|
406
415
|
'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
|
|
407
416
|
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
@@ -508,7 +517,7 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
508
517
|
complete: ['Complete', 'Completion is recorded for this result. Open it to inspect the saved evidence.', 'open-result', 'Open result'],
|
|
509
518
|
cancelled: ['Cancelled', 'This assignment was cancelled.', 'inspect-task', 'View assignment'],
|
|
510
519
|
running: ['Running now', '', null, ''],
|
|
511
|
-
'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : 'inspect-decisions', 'Answer question'],
|
|
520
|
+
'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : n(row, 'agent_ask') ? 'answer-agent' : 'inspect-decisions', 'Answer question'],
|
|
512
521
|
'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
|
|
513
522
|
// Its own reason replaces this below (resultHoldUpSentence).
|
|
514
523
|
'result-needs-attention': ['Needs your decision', '', 'inspect-run', 'Inspect run'],
|
|
@@ -588,11 +597,11 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
588
597
|
const read = actionCode !== null && ['inspect-task', 'inspect-run', 'inspect-stop', 'inspect-decisions', 'open-result'].includes(actionCode);
|
|
589
598
|
const primaryAction = actionCode === null ? null : { code: actionCode, label: actionLabel,
|
|
590
599
|
target: { taskId: successor ?? (code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id),
|
|
591
|
-
runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
|
|
600
|
+
runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'answer-agent' ? n(row, 'agent_ask_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
|
|
592
601
|
: actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null
|
|
593
602
|
: actionCode === 'confirm-stopped' ? process?.build ?? (n(row, 'result_id') || null) : null,
|
|
594
603
|
decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
|
|
595
|
-
access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
|
|
604
|
+
access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'answer-agent', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
|
|
596
605
|
retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
|
|
597
606
|
// The one shared status: the headline and sentence every surface uses.
|
|
598
607
|
const coded = stageOfCode(code, { needsPerson: personNeed, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
|
|
@@ -605,12 +614,15 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
605
614
|
const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' || verdict === 'verified' ? 'passed' : null;
|
|
606
615
|
const listedChecks = !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']);
|
|
607
616
|
const ready = reading.stage === 'finished' && readiness !== undefined ? readiness(row, listedChecks) : null;
|
|
617
|
+
// Toolroll marked it complete because its work landed: the reason, said once.
|
|
618
|
+
const landed = code === 'complete' ? landedReasonOf(s(row, 'checked_actor'), s(row, 'checked_detail')) : null;
|
|
608
619
|
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
|
|
609
620
|
...(process === null ? {} : { needContext: { build: process.build } }),
|
|
610
621
|
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
611
622
|
checks: ready?.checks ?? listedChecks,
|
|
612
623
|
...(ready === null ? {} : { requirements: ready.requirements ?? null, ...(ready.accepted ? { accepted: true } : {}), ...(ready.blockers === undefined ? {} : { blockers: ready.blockers }) }),
|
|
613
|
-
completedBy: code !== 'complete' ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
|
|
624
|
+
completedBy: code !== 'complete' || landed !== null ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
|
|
625
|
+
...(landed === null ? {} : { landed }),
|
|
614
626
|
...(lead === null ? {} : { lead, ...(leadName === undefined ? {} : { leadName }) }) });
|
|
615
627
|
label = shared.headline;
|
|
616
628
|
detail = shared.sentence;
|
|
@@ -619,6 +631,14 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
619
631
|
actionLabel = shared.need.action.label;
|
|
620
632
|
primaryAction.label = actionLabel;
|
|
621
633
|
}
|
|
634
|
+
// A running agent's own question (agent-board.ts): its words, and Reply on the Agents board.
|
|
635
|
+
if (actionCode === 'answer-agent' && primaryAction !== null) {
|
|
636
|
+
actionLabel = 'Reply';
|
|
637
|
+
primaryAction.label = actionLabel;
|
|
638
|
+
const asked = s(row, 'agent_ask_text');
|
|
639
|
+
if (asked !== null)
|
|
640
|
+
detail = `The agent asks: ${asked}`;
|
|
641
|
+
}
|
|
622
642
|
const views = ['all'];
|
|
623
643
|
if (need)
|
|
624
644
|
views.push('needs-you');
|
|
@@ -635,6 +655,6 @@ function itemOf(row, principal, probe, leadName, readiness) {
|
|
|
635
655
|
action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
|
|
636
656
|
ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
|
|
637
657
|
chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
|
|
638
|
-
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
|
|
658
|
+
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']), ...(landed === null ? {} : { landed }) } : null,
|
|
639
659
|
evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}) };
|
|
640
660
|
}
|
package/dist/work-summary.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export type WorkTarget = {
|
|
|
9
9
|
};
|
|
10
10
|
export type WorkAction = {
|
|
11
11
|
/** `confirm-stopped`: an approver confirms a finished build stopped (behind the password);
|
|
12
|
-
* `sign-in`: the agent's sign-in, in Settings. */
|
|
13
|
-
code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "confirm-stopped" | "sign-in";
|
|
12
|
+
* `sign-in`: the agent's sign-in, in Settings; `answer-agent`: Reply to a running agent's question on the Agents board. */
|
|
13
|
+
code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "confirm-stopped" | "sign-in" | "answer-agent";
|
|
14
14
|
label: string;
|
|
15
15
|
target: WorkTarget;
|
|
16
16
|
access: "read" | "operator-control" | "proposal-only" | "operator-handoff";
|
|
@@ -4,7 +4,9 @@ export const WORKSPACE_REVISION_KEY = 'workspace-content:v1';
|
|
|
4
4
|
const KEY = WORKSPACE_REVISION_KEY;
|
|
5
5
|
const MAX_AGE_MS = 60_000;
|
|
6
6
|
const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_delivery', 'sqlite_sequence',
|
|
7
|
-
'team_read', 'team_request', 'push_delivery', 'chat_message_ref'
|
|
7
|
+
'team_read', 'team_request', 'push_delivery', 'chat_message_ref',
|
|
8
|
+
// Who is watching an agent right now: renewed by every open page's keep-alive, never news.
|
|
9
|
+
'agent_view']);
|
|
8
10
|
const QUIET = {
|
|
9
11
|
runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
|
|
10
12
|
watch_episode: ['ticks'], provider_readiness: ['observed_at'],
|