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
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
/** Private lead catch-up, separate from human notification delivery. These are
|
|
2
2
|
* observed assignment transitions, not a promise to capture every intermediate
|
|
3
|
-
* state between scans. Recording or reading them never dispatches agent work.
|
|
3
|
+
* state between scans. Recording or reading them never dispatches agent work.
|
|
4
|
+
* One stream serves every lead: a coordinator's or team lead's events are
|
|
5
|
+
* recorded under its owner, and the built-in lead's crew updates under its
|
|
6
|
+
* follow grant (`follow:<grant>`, lead-follow.ts); each reads its own through
|
|
7
|
+
* the delivery inbox (assignment-delivery.ts). */
|
|
4
8
|
import { createHash } from "node:crypto";
|
|
5
9
|
import { assignmentOf } from "./assignment.js";
|
|
6
10
|
import { publicChatText } from "./chat-display.js";
|
|
11
|
+
import { sweepIfMoved } from "./sweep-gate.js";
|
|
7
12
|
export const ASSIGNMENT_STATUS_ACTION = "assignment status observed";
|
|
13
|
+
/** A reader's first sight of a task that is not news: the state later events are compared to, never delivered. */
|
|
14
|
+
export const ASSIGNMENT_STATUS_BASELINE = "assignment status baseline";
|
|
8
15
|
export const ASSIGNMENT_STATUS_SOURCE = "work";
|
|
16
|
+
/** The states that wake a lead: a result to check, a question, completion, or cancellation. */
|
|
17
|
+
export const LEAD_WAKE_STATES = ["ready-to-check", "needs-decision", "complete", "cancelled"];
|
|
18
|
+
/** The built-in lead's events, one stream per follow grant. */
|
|
19
|
+
export const followActor = (grant) => `follow:${grant}`;
|
|
9
20
|
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
10
21
|
const text = publicChatText;
|
|
11
22
|
/** No saved artifact contents, credential bytes, or polling timestamps. Exact
|
|
@@ -51,7 +62,7 @@ export function parseAssignmentStatusEvent(value) {
|
|
|
51
62
|
!Array.isArray(a["attention"]) || a["attention"].length > 8 || !a["attention"].every(one => string(one, 500)))
|
|
52
63
|
return null;
|
|
53
64
|
const owner = a["owner"], action = a["primaryAction"], result = a["result"], completion = a["completion"], publication = a["publication"];
|
|
54
|
-
if (!object(owner) || !keys(owner, ["kind", "id", "label"]) || !["coordinator", "lead"].includes(String(owner["kind"])) || !string(owner["id"], 128) || !string(owner["label"], 100))
|
|
65
|
+
if (owner !== null && (!object(owner) || !keys(owner, ["kind", "id", "label"]) || !["coordinator", "lead"].includes(String(owner["kind"])) || !string(owner["id"], 128) || !string(owner["label"], 100)))
|
|
55
66
|
return null;
|
|
56
67
|
if (action !== null && (!object(action) || !keys(action, ["code", "label", "target"]) || !string(action["code"], 80) || !string(action["label"], 160) ||
|
|
57
68
|
!object(action["target"]) || !keys(action["target"], ["taskId", "runId", "decisionId"]) || !string(action["target"]["taskId"], 64) || !id(action["target"]["runId"]) || !id(action["target"]["decisionId"])))
|
|
@@ -103,50 +114,70 @@ export function noteAssignmentStatus(store, snapshot, now) {
|
|
|
103
114
|
if (!Array.isArray(repos) || !repos.includes(snapshot.repo))
|
|
104
115
|
return null;
|
|
105
116
|
}
|
|
106
|
-
|
|
107
|
-
if (assignment.owner === null)
|
|
117
|
+
if (assignmentStatusBrief(snapshot).owner === null)
|
|
108
118
|
return null;
|
|
109
|
-
|
|
110
|
-
const previous = store.handle.prepare("SELECT id,outcome FROM action_ledger WHERE task_id=? AND action=? AND source=? ORDER BY id DESC LIMIT 1")
|
|
111
|
-
.get(snapshot.rootId, ASSIGNMENT_STATUS_ACTION, ASSIGNMENT_STATUS_SOURCE);
|
|
112
|
-
if (previous !== undefined) {
|
|
113
|
-
try {
|
|
114
|
-
if (JSON.parse(String(previous["outcome"]))?.stateDigest === stateDigest)
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
catch { /* Preserve an unreadable older event; append the current observation. */ }
|
|
118
|
-
}
|
|
119
|
-
const digest = hash({ rootRef: ref.id, owner: owner.id, previous: previous === undefined ? null : Number(previous["id"]), stateDigest });
|
|
120
|
-
const event = { version: 1, digest, stateDigest, assignment: { ...assignment, owner: assignment.owner } };
|
|
121
|
-
return store.recordAction({ at: now.toISOString(), actor: `${owner.kind}:${owner.id}`, repo: snapshot.repo,
|
|
122
|
-
taskId: snapshot.rootId, runId: assignment.runId, action: ASSIGNMENT_STATUS_ACTION,
|
|
123
|
-
outcome: JSON.stringify(event), source: ASSIGNMENT_STATUS_SOURCE });
|
|
119
|
+
return appendStatusEvent(store, `${owner.kind}:${owner.id}`, owner.id, ref.id, snapshot, now);
|
|
124
120
|
});
|
|
125
121
|
}
|
|
122
|
+
/** The built-in lead's crew update (lead-follow.ts): the same event, under its follow grant. The
|
|
123
|
+
* caller has already proved the grant and its projects; the root must still be a placed task. */
|
|
124
|
+
export function noteFollowStatus(store, grant, snapshot, now, action = ASSIGNMENT_STATUS_ACTION) {
|
|
125
|
+
if (snapshot.repo === null)
|
|
126
|
+
return null;
|
|
127
|
+
return store.transact(() => {
|
|
128
|
+
const ref = store.lookupRef(snapshot.rootId);
|
|
129
|
+
if (!ref || ref.repo !== snapshot.repo || ref.revisionOf !== null)
|
|
130
|
+
return null;
|
|
131
|
+
return appendStatusEvent(store, followActor(grant), followActor(grant), ref.id, snapshot, now, action);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** Each reader's latest event or baseline, not a permanent digest set, deduplicates its retries, so Working → Needs
|
|
135
|
+
* decision → Working is retained. One reader's events never suppress or duplicate another's. */
|
|
136
|
+
function appendStatusEvent(store, actor, ownerId, rootRef, snapshot, now, action = ASSIGNMENT_STATUS_ACTION) {
|
|
137
|
+
const assignment = assignmentStatusBrief(snapshot);
|
|
138
|
+
const stateDigest = hash(assignment);
|
|
139
|
+
const previous = store.handle.prepare("SELECT id,outcome FROM action_ledger WHERE task_id=? AND action IN (?,?) AND source=? AND actor=? ORDER BY id DESC LIMIT 1")
|
|
140
|
+
.get(snapshot.rootId, ASSIGNMENT_STATUS_ACTION, ASSIGNMENT_STATUS_BASELINE, ASSIGNMENT_STATUS_SOURCE, actor);
|
|
141
|
+
if (previous !== undefined) {
|
|
142
|
+
try {
|
|
143
|
+
if (JSON.parse(String(previous["outcome"]))?.stateDigest === stateDigest)
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
catch { /* Preserve an unreadable older event; append the current observation. */ }
|
|
147
|
+
}
|
|
148
|
+
const digest = hash({ rootRef, owner: ownerId, previous: previous === undefined ? null : Number(previous["id"]), stateDigest });
|
|
149
|
+
const event = { version: 1, digest, stateDigest, assignment };
|
|
150
|
+
return store.recordAction({ at: now.toISOString(), actor, repo: snapshot.repo,
|
|
151
|
+
taskId: snapshot.rootId, runId: assignment.runId, action,
|
|
152
|
+
outcome: JSON.stringify(event), source: ASSIGNMENT_STATUS_SOURCE });
|
|
153
|
+
}
|
|
126
154
|
/** At most fifty roots per pass; the cursor survives process restarts. A full
|
|
127
|
-
* pass cycles back, so changes below the cursor are eventually observed too.
|
|
155
|
+
* pass cycles back, so changes below the cursor are eventually observed too.
|
|
156
|
+
* A pass is skipped when nothing moved since the cycle began (sweep-gate.ts). */
|
|
128
157
|
export function syncAssignmentStatuses(store, now, repos, root) {
|
|
129
158
|
if (repos.length === 0)
|
|
130
159
|
return;
|
|
131
160
|
const allowed = [...new Set(repos)].sort();
|
|
132
161
|
const key = `assignment-status:${hash(allowed)}`;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(SELECT 1 FROM
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
sweepIfMoved(store, key, now, () => {
|
|
163
|
+
const rows = store.handle.prepare(`SELECT t.id,t.external_id FROM task_ref t
|
|
164
|
+
WHERE t.backend='built-in' AND t.revision_of IS NULL
|
|
165
|
+
AND t.repo IN (SELECT value FROM json_each(?)) AND t.id>?
|
|
166
|
+
AND (t.coordinator_cid IS NOT NULL OR EXISTS(SELECT 1 FROM team_task_owner o WHERE o.task_ref=t.id) OR EXISTS
|
|
167
|
+
(SELECT 1 FROM action_ledger a WHERE a.task_id=t.external_id AND a.action='assignment claimed' AND a.source='work'))
|
|
168
|
+
ORDER BY t.id LIMIT 50`).all(JSON.stringify(allowed), store.serviceCursor(key));
|
|
169
|
+
// Release the writer between families. Each observation and its durable
|
|
170
|
+
// position still commit together; an interrupted sweep resumes after the
|
|
171
|
+
// last committed family instead of holding every heartbeat behind a batch.
|
|
172
|
+
if (rows.length === 0)
|
|
173
|
+
store.setServiceCursor(key, 0, now);
|
|
174
|
+
for (const [index, row] of rows.entries()) {
|
|
175
|
+
store.transact(() => {
|
|
176
|
+
const snapshot = assignmentOf(store, String(row['external_id']), now, { principal: 'coordinator', repos: allowed }, root);
|
|
177
|
+
if (snapshot !== null)
|
|
178
|
+
noteAssignmentStatus(store, snapshot, now);
|
|
179
|
+
store.setServiceCursor(key, index === rows.length - 1 && rows.length < 50 ? 0 : Number(row['id']), now);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
152
183
|
}
|
package/dist/assignment.d.ts
CHANGED
|
@@ -86,11 +86,13 @@ export type AssignmentSnapshot = {
|
|
|
86
86
|
}[];
|
|
87
87
|
};
|
|
88
88
|
/** `lead`: marked complete by the person's lead (`toolroll lead token`), shown as "by the lead". */
|
|
89
|
+
/** `landed`: Toolroll marked it complete because its work is already in main, and why ("its pull request merged"). */
|
|
89
90
|
completion: {
|
|
90
91
|
actor: string;
|
|
91
92
|
at: string;
|
|
92
93
|
digest: string;
|
|
93
94
|
lead?: true;
|
|
95
|
+
landed?: string;
|
|
94
96
|
} | null;
|
|
95
97
|
handoff: {
|
|
96
98
|
kind: "result" | "decision" | "attention";
|
|
@@ -154,6 +156,7 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
|
|
|
154
156
|
at: string;
|
|
155
157
|
digest: string;
|
|
156
158
|
lead?: true;
|
|
159
|
+
landed?: string;
|
|
157
160
|
} | null;
|
|
158
161
|
result: {
|
|
159
162
|
digest: string;
|
|
@@ -233,9 +236,13 @@ export declare function acceptAndCompleteAsOperator(store: Store, taskId: string
|
|
|
233
236
|
receiptDigest: string;
|
|
234
237
|
note: string | null;
|
|
235
238
|
}, who: VerifiedApprover, now: Date, root?: string, finish?: (digest: string) => Finished): Finished;
|
|
236
|
-
/** The ordinary worker reconciles at most 50 owned roots per pass
|
|
237
|
-
*
|
|
239
|
+
/** The ordinary worker reconciles at most 50 owned roots per pass, and skips a
|
|
240
|
+
* pass when nothing moved (sweep-gate.ts). A lost wakeup is recovered after
|
|
241
|
+
* restart; immutable notification keys dedupe it. */
|
|
238
242
|
export declare function syncAssignmentHandoffs(store: Store, now: Date, repos: readonly string[], root?: string): void;
|
|
243
|
+
/** The history of assignment handoffs sent to people (the `assignment-handoff` notifications), read by cursor. It is
|
|
244
|
+
* not a delivery path: nothing is saved, replayed or acknowledged here. Leads receive their updates through the
|
|
245
|
+
* assignment inbox (assignment-delivery.ts), the one road results take to a lead. */
|
|
239
246
|
export declare function assignmentUpdates(store: Store, now: Date, access: AssignmentAccess, query: {
|
|
240
247
|
after: number;
|
|
241
248
|
limit: number;
|
|
@@ -268,6 +275,7 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
|
|
|
268
275
|
at: string;
|
|
269
276
|
digest: string;
|
|
270
277
|
lead?: true;
|
|
278
|
+
landed?: string;
|
|
271
279
|
} | null;
|
|
272
280
|
result: {
|
|
273
281
|
digest: string;
|
package/dist/assignment.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { currentActor, withActor } from "./actor.js";
|
|
6
6
|
import { replacedWords } from "./task-status.js";
|
|
7
|
-
import { COMPLETION_ACTION, familyChangedFiles } from "./result-completion.js";
|
|
7
|
+
import { COMPLETER_ACTOR, COMPLETION_ACTION, familyChangedFiles, landedReasonOf } from "./result-completion.js";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { readSchemaVersion, stopFactOf } from "./store.js";
|
|
10
10
|
import { approvalOf } from "./scope.js";
|
|
11
11
|
import { openWorkDecisionOf, taskWorkSummaryOf, workDecisionAction } from "./work-summary.js";
|
|
12
|
-
import { evidenceRoot, readVerifiedArtifact, readVerifiedReport } from "./evidence.js";
|
|
12
|
+
import { evidenceRoot, readVerifiedArtifact, readVerifiedReport, verifyArtifact } from "./evidence.js";
|
|
13
|
+
import { sweepIfMoved } from "./sweep-gate.js";
|
|
13
14
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
14
15
|
import { reproveApprover } from "./principal.js";
|
|
15
16
|
import { noteAssignmentStatus } from "./assignment-status.js";
|
|
@@ -163,7 +164,7 @@ export function assignmentOf(store, taskId, now, access, root, memo) {
|
|
|
163
164
|
});
|
|
164
165
|
const artifacts = result === null ? [] : store.artifactsFor(result.id);
|
|
165
166
|
const reports = artifacts.filter(a => a.kind === "report");
|
|
166
|
-
const unavailable = family.versions.flatMap(version => store.runsFor(version.refId).flatMap(run => store.artifactsFor(run.id).filter(artifact => root === undefined || !
|
|
167
|
+
const unavailable = family.versions.flatMap(version => store.runsFor(version.refId).flatMap(run => store.artifactsFor(run.id).filter(artifact => root === undefined || !verifyArtifact(root, artifact).ok)
|
|
167
168
|
.map(artifact => `Saved ${artifact.kind} #${artifact.id} (run ${run.id}) is unavailable or changed.`)));
|
|
168
169
|
const ownChecks = result === null ? null : assignmentChecksForRun(store, root, result.id, now);
|
|
169
170
|
// Checks Off: a passing full release check that contains this commit covers it, read now from the existing receipts.
|
|
@@ -262,20 +263,22 @@ export function assignmentOf(store, taskId, now, access, root, memo) {
|
|
|
262
263
|
: completionKind === "accepted-exception" ? "An operator accepted this result with its recorded limitations. The lead can inspect that decision; the recorded checks are unchanged."
|
|
263
264
|
: receipt.checks.detail;
|
|
264
265
|
primaryAction = { code: "open-result", label: completionKind === "research-report" ? "Read report" : "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
|
|
265
|
-
const checked = store.handle.prepare("SELECT actor,at FROM action_ledger WHERE task_id = ? AND run_id = ? AND action = ? AND outcome = ? AND source = 'work' ORDER BY id DESC")
|
|
266
|
-
.all(family.root.id, result.id, CHECK_ACTION, receipt.digest).find(row =>
|
|
266
|
+
const checked = store.handle.prepare("SELECT actor,at,detail FROM action_ledger WHERE task_id = ? AND run_id = ? AND action = ? AND outcome = ? AND source = 'work' ORDER BY id DESC")
|
|
267
|
+
.all(family.root.id, result.id, CHECK_ACTION, receipt.digest).find(row => COMPLETER_ACTOR.test(String(row["actor"])));
|
|
267
268
|
// This ledger fact was authorized when written. Credential rotation,
|
|
268
269
|
// membership changes and ownership transfer cannot revoke past completion.
|
|
269
270
|
// The exact result digest still fences changes to work, scope and history.
|
|
270
271
|
if (checked !== undefined) {
|
|
271
272
|
const rootRef = store.lookupRef(family.root.id);
|
|
272
273
|
const byLead = rootRef !== null && olderStoreSafe(() => store.taskActs(rootRef.id), []).filter(one => one.act === "completed").at(-1)?.lead === true;
|
|
273
|
-
|
|
274
|
+
const landed = landedReasonOf(String(checked["actor"]), checked["detail"] == null ? null : String(checked["detail"]));
|
|
275
|
+
completion = { actor: String(checked["actor"]), at: String(checked["at"]), digest: receipt.digest, ...(byLead && landed === null ? { lead: true } : {}), ...(landed === null ? {} : { landed }) };
|
|
274
276
|
state = "complete";
|
|
275
277
|
primaryAction = { ...primaryAction, label: completionKind === "research-report" ? "Read report" : "Open result" };
|
|
276
278
|
const label = byLead ? "the lead" : completion.actor.startsWith("operator:") ? completion.actor.slice(9) : owner && completion.actor === actorOf(owner) ? owner.label : "the previous lead";
|
|
277
|
-
detail =
|
|
278
|
-
: `
|
|
279
|
+
detail = landed !== null ? `Marked complete automatically: ${landed}. ${receipt.checks.detail}`
|
|
280
|
+
: completionKind === "research-report" ? `Research report checked by ${label}. No deployment is implied.`
|
|
281
|
+
: `Handled by ${label}. ${receipt.checks.detail} Publication and deployment are separate.`;
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
else if (work.status.rank === 0 || (!live && (current.state === "done" || current.state === "failed" || work.status.views.includes("needs-you")))) {
|
|
@@ -418,7 +421,7 @@ export function assignmentEvidenceIntact(store, root, receipt) {
|
|
|
418
421
|
try {
|
|
419
422
|
const family = store.taskFamilyOf(receipt.taskId, null, true);
|
|
420
423
|
if (family === null || family.problem !== null || family.root.id !== receipt.rootId ||
|
|
421
|
-
!family.versions.every(version => store.runsFor(version.refId).every(run => store.artifactsFor(run.id).every(artifact =>
|
|
424
|
+
!family.versions.every(version => store.runsFor(version.refId).every(run => store.artifactsFor(run.id).every(artifact => verifyArtifact(root, artifact).ok))))
|
|
422
425
|
return false;
|
|
423
426
|
if (receipt.completionKind === "research-report") {
|
|
424
427
|
const report = readVerifiedReport(store, root, store.lookupRef(receipt.taskId).id);
|
|
@@ -567,13 +570,14 @@ function noteAssignmentHandoff(store, assignment, now) {
|
|
|
567
570
|
kind: "assignment-handoff", source: { taskRef: ref.id }, subject: assignment.state === "cancelled" ? "Assignment cancelled" : assignment.state === "ready-to-check" ? "Ready" : "Assignment needs a decision",
|
|
568
571
|
body: assignment.detail, link: `/t/${encodeURIComponent(assignment.rootId)}` }, now);
|
|
569
572
|
}
|
|
570
|
-
/** The ordinary worker reconciles at most 50 owned roots per pass
|
|
571
|
-
*
|
|
573
|
+
/** The ordinary worker reconciles at most 50 owned roots per pass, and skips a
|
|
574
|
+
* pass when nothing moved (sweep-gate.ts). A lost wakeup is recovered after
|
|
575
|
+
* restart; immutable notification keys dedupe it. */
|
|
572
576
|
export function syncAssignmentHandoffs(store, now, repos, root) {
|
|
573
577
|
if (repos.length === 0)
|
|
574
578
|
return;
|
|
575
|
-
|
|
576
|
-
|
|
579
|
+
const key = `assignment-handoff:${digest([...new Set(repos)].sort())}`;
|
|
580
|
+
sweepIfMoved(store, key, now, () => store.transact(() => {
|
|
577
581
|
const cursor = store.serviceCursor(key);
|
|
578
582
|
const rows = store.handle.prepare(`SELECT t.id,t.external_id FROM task_ref t WHERE t.backend = 'built-in' AND t.revision_of IS NULL
|
|
579
583
|
AND t.repo IN (SELECT value FROM json_each(?)) AND t.id > ?
|
|
@@ -587,8 +591,11 @@ export function syncAssignmentHandoffs(store, now, repos, root) {
|
|
|
587
591
|
}
|
|
588
592
|
}
|
|
589
593
|
store.setServiceCursor(key, rows.length < 50 ? 0 : Number(rows.at(-1)["id"]), now);
|
|
590
|
-
});
|
|
594
|
+
}));
|
|
591
595
|
}
|
|
596
|
+
/** The history of assignment handoffs sent to people (the `assignment-handoff` notifications), read by cursor. It is
|
|
597
|
+
* not a delivery path: nothing is saved, replayed or acknowledged here. Leads receive their updates through the
|
|
598
|
+
* assignment inbox (assignment-delivery.ts), the one road results take to a lead. */
|
|
592
599
|
export function assignmentUpdates(store, now, access, query, root) {
|
|
593
600
|
const after = Number.isSafeInteger(query.after) && query.after >= 0 ? query.after : 0;
|
|
594
601
|
const limit = Math.max(1, Math.min(100, Math.floor(query.limit) || 50));
|
package/dist/bin.js
CHANGED
|
@@ -17,6 +17,15 @@ if (major < 22 || (major === 22 && minor < 13)) {
|
|
|
17
17
|
process.stderr.write(`Toolroll needs Node 22.13 or newer (this is ${versions.node}) — it uses node:sqlite.\n`);
|
|
18
18
|
process.exit(2);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// --version and the top-level usage answer here, before the CLI and its command families load.
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const { quickAnswer } = await import("./cli-fast.js");
|
|
23
|
+
const quick = quickAnswer(argv);
|
|
24
|
+
if (quick !== null) {
|
|
25
|
+
console.log(quick);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const { runCliProcess } = await import("./cli.js");
|
|
29
|
+
await runCliProcess(argv);
|
|
30
|
+
}
|
|
22
31
|
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line, prefixed, with nothing that can end the block or start a new rule.
|
|
3
|
+
*
|
|
4
|
+
* Scope text is written by whoever filed the task. A goal containing a newline
|
|
5
|
+
* and a bullet would otherwise read to the agent as another rule in the list,
|
|
6
|
+
* which is how "add a guard" becomes "add a guard, and ignore the rules below".
|
|
7
|
+
* The same quoting wraps a steering note the agent reads from its mailbox.
|
|
8
|
+
*/
|
|
9
|
+
export declare function fence(text: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line, prefixed, with nothing that can end the block or start a new rule.
|
|
3
|
+
*
|
|
4
|
+
* Scope text is written by whoever filed the task. A goal containing a newline
|
|
5
|
+
* and a bullet would otherwise read to the agent as another rule in the list,
|
|
6
|
+
* which is how "add a guard" becomes "add a guard, and ignore the rules below".
|
|
7
|
+
* The same quoting wraps a steering note the agent reads from its mailbox.
|
|
8
|
+
*/
|
|
9
|
+
export function fence(text) {
|
|
10
|
+
// Newlines and other control characters collapse to a space: they are the
|
|
11
|
+
// only way a value can stop being one line and start looking like a new
|
|
12
|
+
// rule. The visible text is otherwise left exactly as written — mangling
|
|
13
|
+
// somebody's scope to defend against it would be its own kind of wrong.
|
|
14
|
+
return `| ${text
|
|
15
|
+
// C0/C1 plus the Unicode line and paragraph separators: everything
|
|
16
|
+
// that could end this physical line (Codex free-text review, finding 5).
|
|
17
|
+
.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]+/g, " ")
|
|
18
|
+
// A quoted protocol-shaped name is broken VISIBLY, so untrusted text
|
|
19
|
+
// can never collide with the real nonce-bearing filename that follows.
|
|
20
|
+
.replace(/STANDING-ORDERS-/g, "NIGHTORDERS[quoted]-")
|
|
21
|
+
.trim()}`;
|
|
22
|
+
}
|