toolroll 0.9.6 → 0.9.8
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 +64 -1
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +2 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +80 -16
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +28 -24
- package/dist/browser-workspace.d.ts +117 -2
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +11 -1
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +26 -0
- package/dist/check-levels-ui.js +84 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -0
- package/dist/demo-chat.js +4 -1
- package/dist/dispatch.js +9 -3
- package/dist/dispose.d.ts +12 -3
- package/dist/dispose.js +19 -5
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/invoke.js +5 -0
- package/dist/lead-context.js +6 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +711 -98
- package/dist/process-custody.js +8 -0
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/provider.js +16 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1038 -145
- package/dist/store.d.ts +112 -4
- package/dist/store.js +447 -27
- package/dist/surface.js +36 -9
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +13 -3
- package/dist/task-status.js +38 -16
- package/dist/team-cli.js +3 -0
- package/dist/telegram-status.js +2 -2
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +39 -6
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +4 -1
- package/dist/workspace-ui.d.ts +6 -1
- package/dist/workspace-ui.js +14 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.9.8 — 2026-10-01
|
|
4
|
+
|
|
5
|
+
- **One automatic review per build.** When a build's checks pass (or it
|
|
6
|
+
finishes, with checks Off) in a project with review on, one read-only
|
|
7
|
+
reviewer reads the scope, acceptance and diff and grades each finding HIGH,
|
|
8
|
+
MEDIUM or LOW. Any HIGH sends the task back once, with the findings as its
|
|
9
|
+
note; a HIGH on that revision comes to you. MEDIUM and LOW never block: they
|
|
10
|
+
show on the result as suggested follow-ups. A review that fails or times
|
|
11
|
+
out never blocks: the result reaches you marked Not reviewed. Turn it on or
|
|
12
|
+
off per project in Settings or with `toolroll review on|off --repo <path>`;
|
|
13
|
+
it's on by default under a hands-off mode. Release candidate checks are
|
|
14
|
+
never reviewed.
|
|
15
|
+
- **Evidence is kept 1, 7, 14 or 28 days, or forever.** Settings →
|
|
16
|
+
Retention and `toolroll retention set evidence 1d` take any of them. Until
|
|
17
|
+
someone chooses, evidence is kept 28 days: **after upgrading, the daily
|
|
18
|
+
sweep starts removing finished tasks' evidence older than 28 days.** A
|
|
19
|
+
period chosen before stays as it was; one the page no longer offers shows
|
|
20
|
+
as "30 days (custom)".
|
|
21
|
+
Nothing a task still needs is removed: unfinished tasks, results Ready for
|
|
22
|
+
review, anything on hold, live runs and release candidates.
|
|
23
|
+
- **A run whose process never started no longer blocks updates.** A spawn
|
|
24
|
+
that fails now settles its own record, and Toolroll settles a finished run's
|
|
25
|
+
leftover record once its processes are provably gone, noting it in the
|
|
26
|
+
ledger. For the rare record that can't be proven either way, an approver
|
|
27
|
+
can run `toolroll run settle <run> --why "<reason>"`; it refuses while
|
|
28
|
+
anything of the run is alive.
|
|
29
|
+
|
|
30
|
+
## 0.9.7 — 2026-10-01
|
|
31
|
+
|
|
32
|
+
- **Onboarding without a password or a form.** `toolroll up` prints three
|
|
33
|
+
lines and opens the console already signed in, through a one-time link that
|
|
34
|
+
works once, for ten minutes, from this computer only. The lead turns on by
|
|
35
|
+
itself with the agent that's signed in (Claude Code, else Codex); the old
|
|
36
|
+
setup form moved to Settings → Lead → Advanced. With no agent signed in,
|
|
37
|
+
Chat shows the command to install and sign in, and checks again on its own.
|
|
38
|
+
A short "how it works", a live Plan → Ready timeline on your first task, a
|
|
39
|
+
card for pairing your phone, and a "wrong host" page that says what to run.
|
|
40
|
+
- **The task page is one conversation.** The status stays on top; below it
|
|
41
|
+
the plan, the agent's notes and results, questions and your replies, in
|
|
42
|
+
order, ending in a box to message the agent (Build, Plan only, or Just
|
|
43
|
+
answer). Details (scope, attempts, audit) moved to a side panel, or a sheet
|
|
44
|
+
on phones. The Chat home shows who is working now and four counts, and
|
|
45
|
+
Catch up and the Inbox have tabs: Needs you, Ready, Running, All.
|
|
46
|
+
- **Checks: quick while building, easy to turn off.** Each project has a
|
|
47
|
+
check level: Quick (the default for new projects), Full or Off, and a task
|
|
48
|
+
can override it ("skip the tests"). With checks Off a result is still
|
|
49
|
+
Ready for review, and says no check ran. Any result can run its checks again or file a task to add tests.
|
|
50
|
+
- **A flow gallery, and flows as files.** Flows → New is a gallery of
|
|
51
|
+
templates grouped by what they're for, with new ones for developers
|
|
52
|
+
(Overnight bug bash, Flaky test hunter, Release notes writer, Docs follow
|
|
53
|
+
the code, Error to fix, PR second opinion and more). Export any flow as a
|
|
54
|
+
`.toolroll-flow.json` file and import one from a file or a link, with a
|
|
55
|
+
preview; secrets never leave, and imported triggers start switched off.
|
|
56
|
+
- **Several builds at once on one project.** The service builds up to two
|
|
57
|
+
tasks of a project at a time (Settings → Projects, `toolroll project
|
|
58
|
+
concurrency`), within the machine's limit. Stopping the service mid-build
|
|
59
|
+
saves the work and resumes it next time.
|
|
60
|
+
- **The lead's housekeeping stays quiet.** An agent acting as your lead gets
|
|
61
|
+
its own credential (`toolroll lead token`); its work pings nobody unless it
|
|
62
|
+
hands something to you. Your own actions never ping you, a replaced task
|
|
63
|
+
reads "Replaced by", and a whole project can be muted.
|
|
64
|
+
- **A faster release check.** Flow journeys run in parallel groups, browser
|
|
65
|
+
journeys are rebalanced, and a version-only bump no longer forces every
|
|
66
|
+
test.
|
|
4
67
|
|
|
5
68
|
## 0.9.6 — 2026-10-01
|
|
6
69
|
|
package/dist/actor.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Actor = {
|
|
2
|
+
account: string;
|
|
3
|
+
lead: boolean;
|
|
4
|
+
};
|
|
5
|
+
/** Run `fn` with this actor (or an empty slot a verified sign-in may fill). */
|
|
6
|
+
export declare function withActor<T>(actor: Actor | null, fn: () => T): T;
|
|
7
|
+
export declare function currentActor(): Actor | null;
|
|
8
|
+
/** A person verified their password inside this command: their acts are now theirs. A lead keeps its own label. */
|
|
9
|
+
export declare function claimActor(account: string): void;
|
|
10
|
+
/** How the ledger names an actor. */
|
|
11
|
+
export declare function actorLabel(actor: Actor): string;
|
|
12
|
+
export declare function mintLeadToken(): {
|
|
13
|
+
id: string;
|
|
14
|
+
token: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function parseLeadToken(presented: string): {
|
|
18
|
+
id: string;
|
|
19
|
+
secret: string;
|
|
20
|
+
} | null;
|
|
21
|
+
export declare function leadSecretMatches(secret: string, keptHash: string): boolean;
|
package/dist/actor.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is acting right now, for pings that follow responsibility.
|
|
3
|
+
*
|
|
4
|
+
* A lead is an agent acting for one person under a lead token (`toolroll lead
|
|
5
|
+
* token`): it signs in as that person, and its acts are recorded as "lead for
|
|
6
|
+
* <owner>". A person acting for themselves is recorded by name. Work whose
|
|
7
|
+
* actor is the lead pings nobody, and nobody is pinged about their own act.
|
|
8
|
+
*
|
|
9
|
+
* The actor is held for one command or one console request, never for a
|
|
10
|
+
* long-running service: a worker's facts are nobody's act.
|
|
11
|
+
*/
|
|
12
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
14
|
+
/** The slot is mutable so a command can name its person after it has verified their password. */
|
|
15
|
+
const slot = new AsyncLocalStorage();
|
|
16
|
+
/** Run `fn` with this actor (or an empty slot a verified sign-in may fill). */
|
|
17
|
+
export function withActor(actor, fn) {
|
|
18
|
+
return slot.run({ actor }, fn);
|
|
19
|
+
}
|
|
20
|
+
export function currentActor() {
|
|
21
|
+
return slot.getStore()?.actor ?? null;
|
|
22
|
+
}
|
|
23
|
+
/** A person verified their password inside this command: their acts are now theirs. A lead keeps its own label. */
|
|
24
|
+
export function claimActor(account) {
|
|
25
|
+
const held = slot.getStore();
|
|
26
|
+
if (held !== undefined && held.actor === null)
|
|
27
|
+
held.actor = { account, lead: false };
|
|
28
|
+
}
|
|
29
|
+
/** How the ledger names an actor. */
|
|
30
|
+
export function actorLabel(actor) {
|
|
31
|
+
return actor.lead ? `lead for ${actor.account}` : actor.account;
|
|
32
|
+
}
|
|
33
|
+
// ---- the lead token ---------------------------------------------------------
|
|
34
|
+
/** `lt_<id>_<secret>`: shown once, only its hash kept. */
|
|
35
|
+
const SHAPE = /^lt_([a-f0-9]{12})_([A-Za-z0-9_-]{43})$/;
|
|
36
|
+
export function mintLeadToken() {
|
|
37
|
+
const id = randomBytes(6).toString("hex"), secret = randomBytes(32).toString("base64url");
|
|
38
|
+
return { id, token: `lt_${id}_${secret}`, hash: hashLeadSecret(secret) };
|
|
39
|
+
}
|
|
40
|
+
const hashLeadSecret = (secret) => createHash("sha256").update(secret, "utf8").digest("hex");
|
|
41
|
+
export function parseLeadToken(presented) {
|
|
42
|
+
const match = SHAPE.exec(presented.trim());
|
|
43
|
+
return match === null ? null : { id: match[1], secret: match[2] };
|
|
44
|
+
}
|
|
45
|
+
export function leadSecretMatches(secret, keptHash) {
|
|
46
|
+
const a = Buffer.from(hashLeadSecret(secret), "hex"), b = Buffer.from(keptHash, "hex");
|
|
47
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
48
|
+
}
|
|
@@ -117,6 +117,7 @@ export declare function assignmentForCoordinator(store: Store, token: string, op
|
|
|
117
117
|
actor: string;
|
|
118
118
|
at: string;
|
|
119
119
|
digest: string;
|
|
120
|
+
lead?: true;
|
|
120
121
|
} | null;
|
|
121
122
|
result: {
|
|
122
123
|
digest: string;
|
|
@@ -139,6 +140,7 @@ export declare function assignmentForCoordinator(store: Store, token: string, op
|
|
|
139
140
|
prUrl: string | null;
|
|
140
141
|
remoteState: string | null;
|
|
141
142
|
} | null;
|
|
143
|
+
review: import("./review-switch.js").BuildReviewView | null;
|
|
142
144
|
deployment: {
|
|
143
145
|
status: "not-recorded";
|
|
144
146
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Assignment reads and delivery acknowledgments. These adapters never start
|
|
2
2
|
* work, answer a decision, accept proof, or change an approval. */
|
|
3
|
+
import { buildReviewLines } from "./review-switch.js";
|
|
3
4
|
import { readFileSync, statSync } from "node:fs";
|
|
4
5
|
import { authenticateCoordinator } from "./coordinator.js";
|
|
5
6
|
import { assignmentOf, assignmentUpdates, claimAssignment, checkAssignment } from "./assignment.js";
|
|
@@ -113,6 +114,7 @@ function assignmentLines(assignment) {
|
|
|
113
114
|
...(receipt.agentReport === null ? [] : [`Agent report: ${receipt.agentReport}`]),
|
|
114
115
|
`Receipt: ${receipt.digest}`,
|
|
115
116
|
]),
|
|
117
|
+
...buildReviewLines(assignment.review ?? null).map(line => line.trimStart().replace(/^review: /, "Review: ")),
|
|
116
118
|
...(assignment.savedContext === undefined ? [] : [
|
|
117
119
|
...(assignment.savedContext.goal === null ? [] : [`Goal: ${assignment.savedContext.goal}`]),
|
|
118
120
|
...(assignment.savedContext.outOfScope === null ? [] : [`Out of scope: ${assignment.savedContext.outOfScope}`]),
|
package/dist/assignment-ui.js
CHANGED
|
@@ -67,7 +67,9 @@ function savedMaterialNotice(detail, assignment) {
|
|
|
67
67
|
export function assignmentCardOf(assignment, options = {}) {
|
|
68
68
|
const receipt = assignment.receipt;
|
|
69
69
|
const links = receipt === null ? {} : { result: options.resultHref ?? chatResultHref(receipt.taskId, receipt.runId), checks: chatResultHref(receipt.taskId, receipt.runId, 'checks'),
|
|
70
|
-
pullRequest: `/t/${encodeURIComponent(assignment.rootId)}#merge
|
|
70
|
+
pullRequest: `/t/${encodeURIComponent(assignment.rootId)}#merge`,
|
|
71
|
+
// Run checks lives with the result's checks; the row's action leads there.
|
|
72
|
+
...(receipt.completionKind === 'research-report' ? {} : { runChecks: `${chatResultHref(receipt.taskId, receipt.runId, 'checks')}#follow-ups` }) };
|
|
71
73
|
const presentation = assignmentPresentationOf(assignment, { ...options, links });
|
|
72
74
|
const { status, diagnostics, taskStatus } = presentation;
|
|
73
75
|
const href = assignment.primaryAction?.code === 'open-result' && options.resultHref !== undefined ? options.resultHref : assignmentActionHref(assignment);
|
package/dist/assignment.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Store, type ProofVerdictRow, type ProofAcceptanceRow } from "./store.js";
|
|
2
2
|
import { type WorkAction, type WorkSummaryAccess } from "./work-summary.js";
|
|
3
3
|
import { type VerifiedApprover } from "./principal.js";
|
|
4
|
+
import { type CheckLevel } from "./check-levels.js";
|
|
5
|
+
import { type BuildReviewView } from "./review-switch.js";
|
|
4
6
|
export type AssignmentAccess = WorkSummaryAccess;
|
|
5
7
|
export type AssignmentOwner = {
|
|
6
8
|
kind: "coordinator" | "lead";
|
|
@@ -13,6 +15,10 @@ export type AssignmentChecks = {
|
|
|
13
15
|
command: string | null;
|
|
14
16
|
logArtifactId: number | null;
|
|
15
17
|
detail: string;
|
|
18
|
+
/** Which check this is (check-levels.ts): quick, full, or off; null before levels. */
|
|
19
|
+
level?: CheckLevel | null;
|
|
20
|
+
/** A follow-up check waiting or running on this commit. */
|
|
21
|
+
running?: "quick" | "full" | null;
|
|
16
22
|
};
|
|
17
23
|
export type AssignmentReceipt = {
|
|
18
24
|
digest: string;
|
|
@@ -70,10 +76,12 @@ export type AssignmentSnapshot = {
|
|
|
70
76
|
problem: string | null;
|
|
71
77
|
}[];
|
|
72
78
|
};
|
|
79
|
+
/** `lead`: marked complete by the person's lead (`toolroll lead token`), shown as "by the lead". */
|
|
73
80
|
completion: {
|
|
74
81
|
actor: string;
|
|
75
82
|
at: string;
|
|
76
83
|
digest: string;
|
|
84
|
+
lead?: true;
|
|
77
85
|
} | null;
|
|
78
86
|
handoff: {
|
|
79
87
|
kind: "result" | "decision" | "attention";
|
|
@@ -85,6 +93,9 @@ export type AssignmentSnapshot = {
|
|
|
85
93
|
prUrl: string | null;
|
|
86
94
|
remoteState: string | null;
|
|
87
95
|
} | null;
|
|
96
|
+
/** The result's one automatic review, when its project had review on:
|
|
97
|
+
* pending, HIGH findings, suggested follow-ups (MEDIUM/LOW), or not reviewed. */
|
|
98
|
+
review: BuildReviewView | null;
|
|
88
99
|
deployment: {
|
|
89
100
|
status: "not-recorded";
|
|
90
101
|
};
|
|
@@ -112,6 +123,7 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
|
|
|
112
123
|
actor: string;
|
|
113
124
|
at: string;
|
|
114
125
|
digest: string;
|
|
126
|
+
lead?: true;
|
|
115
127
|
} | null;
|
|
116
128
|
result: {
|
|
117
129
|
digest: string;
|
|
@@ -134,6 +146,7 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
|
|
|
134
146
|
prUrl: string | null;
|
|
135
147
|
remoteState: string | null;
|
|
136
148
|
} | null;
|
|
149
|
+
review: BuildReviewView | null;
|
|
137
150
|
deployment: {
|
|
138
151
|
status: "not-recorded";
|
|
139
152
|
};
|
|
@@ -189,6 +202,7 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
|
|
|
189
202
|
actor: string;
|
|
190
203
|
at: string;
|
|
191
204
|
digest: string;
|
|
205
|
+
lead?: true;
|
|
192
206
|
} | null;
|
|
193
207
|
result: {
|
|
194
208
|
digest: string;
|
|
@@ -211,6 +225,7 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
|
|
|
211
225
|
prUrl: string | null;
|
|
212
226
|
remoteState: string | null;
|
|
213
227
|
} | null;
|
|
228
|
+
review: BuildReviewView | null;
|
|
214
229
|
deployment: {
|
|
215
230
|
status: "not-recorded";
|
|
216
231
|
};
|
package/dist/assignment.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* approval or proof state. Lead ownership and exact receipt acknowledgments
|
|
3
3
|
* are append-only actions; handoffs use the existing durable outbox. */
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { currentActor, withActor } from "./actor.js";
|
|
6
|
+
import { replacedWords } from "./task-status.js";
|
|
5
7
|
import { COMPLETION_ACTION, familyChangedFiles } from "./result-completion.js";
|
|
6
8
|
import { homedir } from "node:os";
|
|
7
9
|
import { readSchemaVersion } from "./store.js";
|
|
@@ -12,6 +14,9 @@ import { verificationEvidence } from "./verification-evidence.js";
|
|
|
12
14
|
import { reproveApprover } from "./principal.js";
|
|
13
15
|
import { noteAssignmentStatus } from "./assignment-status.js";
|
|
14
16
|
import { historicalAssessmentReason } from "./assignment-presentation.js";
|
|
17
|
+
import { runCheckLevel } from "./check-levels.js";
|
|
18
|
+
import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
|
|
19
|
+
import { buildReviewOf, findingWords } from "./review-switch.js";
|
|
15
20
|
/** Status-first handoff for routine reads. Fetch get_assignment only when
|
|
16
21
|
* inspecting its exact evidence; do not repeat a full proof in every poll. */
|
|
17
22
|
export function assignmentBrief(assignment) {
|
|
@@ -25,9 +30,23 @@ export function assignmentBrief(assignment) {
|
|
|
25
30
|
base: receipt.base, head: receipt.head, completionKind: receipt.completionKind,
|
|
26
31
|
checks: receipt.checks, proofAcceptance: receipt.proofAcceptance, verdict: receipt.proof?.verdict ?? null,
|
|
27
32
|
criteria: { passed: receipt.proof?.matrix.filter(row => row.state === "pass").length ?? 0, total: receipt.proof?.matrix.length ?? 0 }, evidence: receipt.evidence },
|
|
28
|
-
publication: assignment.publication, deployment: assignment.deployment };
|
|
33
|
+
publication: assignment.publication, review: assignment.review, deployment: assignment.deployment };
|
|
29
34
|
}
|
|
30
35
|
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
36
|
+
/** A lookup added with lead-quiet, read safely: a deploy proves completion with this code over the INSTALLED runtime's
|
|
37
|
+
* store, before this version's migration, so the method (or its table) may not exist yet. Absent reads as none. */
|
|
38
|
+
function olderStoreSafe(read, absent) {
|
|
39
|
+
try {
|
|
40
|
+
return read();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof TypeError || /no such (table|column)/.test(String(error)))
|
|
44
|
+
return absent;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** A check as the receipt digest seals it: presentation-only fields (check-levels.ts) left out. */
|
|
49
|
+
const sealedChecks = ({ level: _level, running: _running, ...proof }) => proof;
|
|
31
50
|
const OWNER_ACTION = "assignment claimed";
|
|
32
51
|
const CHECK_ACTION = COMPLETION_ACTION;
|
|
33
52
|
const actorOf = (owner) => `${owner.kind}:${owner.id}`;
|
|
@@ -58,8 +77,24 @@ function ownerOf(store, rootId, repo) {
|
|
|
58
77
|
active: credential !== undefined && credential["revoked_at"] === null && repo !== null && Array.isArray(repos) && repos.includes(repo) };
|
|
59
78
|
}
|
|
60
79
|
// Report the recorded check outcome, never turn a model verdict into a check.
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
// Follow-up checks on the same commit (Run checks) upgrade a pass and show a failure.
|
|
81
|
+
function nativeChecks(store, root, runId, now) {
|
|
82
|
+
const own = buildChecks(store, root, runId);
|
|
83
|
+
const run = store.getRun(runId);
|
|
84
|
+
const followUps = followUpChecksOf(store, runId, now, root);
|
|
85
|
+
if (followUps.length === 0)
|
|
86
|
+
return own;
|
|
87
|
+
const read = withFollowUps({ status: own.status, level: own.level ?? null, exitCode: own.exitCode, head: run?.headRevision ?? null }, followUps);
|
|
88
|
+
const latest = [...followUps].reverse().find(one => one.state === "passed" || one.state === "failed");
|
|
89
|
+
const changed = read.status !== own.status || read.level !== (own.level ?? null) || read.exitCode !== own.exitCode;
|
|
90
|
+
const words = read.level === "quick" ? "Quick checks" : "Checks";
|
|
91
|
+
return { ...own, running: read.running,
|
|
92
|
+
...(changed ? { status: read.status, level: read.level, exitCode: read.exitCode, logArtifactId: latest?.logArtifactId ?? own.logArtifactId,
|
|
93
|
+
detail: read.status === "passed" ? `${words} passed.` : `${words} failed (exit ${read.exitCode}).` } : {}) };
|
|
94
|
+
}
|
|
95
|
+
function buildChecks(store, root, runId) {
|
|
96
|
+
const level = runCheckLevel(store, runId);
|
|
97
|
+
const unavailable = (detail) => ({ status: "unavailable", exitCode: null, command: null, logArtifactId: null, detail, level });
|
|
63
98
|
if (root === undefined)
|
|
64
99
|
return unavailable("Saved checks are unavailable.");
|
|
65
100
|
try {
|
|
@@ -68,11 +103,14 @@ function nativeChecks(store, root, runId) {
|
|
|
68
103
|
return unavailable(gate.problem);
|
|
69
104
|
const receipt = gate.bytes === null ? null : JSON.parse(gate.bytes);
|
|
70
105
|
if (receipt === null)
|
|
71
|
-
return { ...unavailable("No machine check is recorded."), status: "not-run" };
|
|
106
|
+
return { ...unavailable(level === "off" ? "Checks were off for this build." : "No machine check is recorded."), status: "not-run" };
|
|
72
107
|
const ran = receipt.result.ran === true, exitCode = ran ? receipt.result.exitCode : null;
|
|
108
|
+
// The sealed receipt names the grant that ran: the quick one, or the full one.
|
|
109
|
+
const sealedLevel = typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:") ? "quick" : "full";
|
|
110
|
+
const words = sealedLevel === "quick" ? "Quick checks" : "Checks";
|
|
73
111
|
return { status: ran ? exitCode === 0 ? "passed" : "failed" : "not-run", exitCode,
|
|
74
|
-
command: receipt.command.command, logArtifactId: receipt.log.artifactId,
|
|
75
|
-
detail: ran ? exitCode === 0 ?
|
|
112
|
+
command: receipt.command.command, logArtifactId: receipt.log.artifactId, level: level === null ? null : sealedLevel,
|
|
113
|
+
detail: ran ? exitCode === 0 ? `${words} passed.` : `${words} failed (exit ${exitCode}).` : `${words} did not finish.` };
|
|
76
114
|
}
|
|
77
115
|
catch {
|
|
78
116
|
return unavailable("Saved checks could not be read.");
|
|
@@ -112,7 +150,7 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
112
150
|
const reports = artifacts.filter(a => a.kind === "report");
|
|
113
151
|
const unavailable = family.versions.flatMap(version => store.runsFor(version.refId).flatMap(run => store.artifactsFor(run.id).filter(artifact => root === undefined || !readVerifiedArtifact(root, artifact).ok)
|
|
114
152
|
.map(artifact => `Saved ${artifact.kind} #${artifact.id} (run ${run.id}) is unavailable or changed.`)));
|
|
115
|
-
const checks = result === null ? null : nativeChecks(store, root, result.id);
|
|
153
|
+
const checks = result === null ? null : nativeChecks(store, root, result.id, now);
|
|
116
154
|
const finishedBuild = result?.role === "builder" && (result.outcome === "built" || result.outcome === "no-change") &&
|
|
117
155
|
result.finishedAt !== null && /^[a-f0-9]{40}$/.test(result.headRevision ?? "");
|
|
118
156
|
// Finished work returns to its lead or user. Strict terms and previous model
|
|
@@ -133,7 +171,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
133
171
|
...artifacts.filter(a => a.kind !== "check-log" && (a.truncated || a.captureStatus === "failed")).map(a => `Saved ${a.kind} #${a.id} is ${a.captureStatus === "failed" ? "a failed capture" : "incomplete"}.`)],
|
|
134
172
|
agentReport: result.handoff, evidence: "recorded",
|
|
135
173
|
};
|
|
136
|
-
|
|
174
|
+
// The digest seals the proof, not how it is shown: the check's `level` (read from its sealed command) and `running`
|
|
175
|
+
// (a follow-up in flight) stay out of it, so a result completed before check levels keeps its completion, and a
|
|
176
|
+
// follow-up check that is still running never unseals one.
|
|
177
|
+
const sealedBody = receiptBody === null ? null : { ...receiptBody, checks: sealedChecks(receiptBody.checks) };
|
|
178
|
+
const receipt = receiptBody === null ? null : { ...receiptBody, digest: digest({ receipt: sealedBody,
|
|
137
179
|
scope: scope === null ? null : { digest: scope.digest, approved: approvalOf(scope).approved, termsProblem: scope.termsProblem ?? null },
|
|
138
180
|
versions: family.versions.map(v => ({ id: v.id, state: v.state })) }) };
|
|
139
181
|
let completion = null;
|
|
@@ -145,7 +187,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
145
187
|
// The exact completed result, scope, family and custody fences apply to
|
|
146
188
|
// every deliverable. Human acceptance remains its own recorded authority;
|
|
147
189
|
// it never changes a machine verdict or supplies a missing report.
|
|
148
|
-
|
|
190
|
+
// Kept out of the sealed receipt (sealedChecks): the review never changes the result's digest.
|
|
191
|
+
const review = result === null ? null : olderStoreSafe(() => buildReviewOf(store, result.id), null);
|
|
192
|
+
// A result under its one automatic review has not reached the person yet.
|
|
193
|
+
const reviewing = review?.state === "pending" && current.state === "done";
|
|
194
|
+
const ready = !reviewing && current.state === "done" && result !== null && completionKind !== null &&
|
|
149
195
|
scope !== null && scope.termsProblem == null && approvalOf(scope).approved && scope.digest === result.scopeDigest &&
|
|
150
196
|
store.activeHolds(current.refId, now).length === 0 && store.applicableStopFor(result.id) === null &&
|
|
151
197
|
processProblem === null &&
|
|
@@ -154,8 +200,10 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
154
200
|
state = "needs-decision";
|
|
155
201
|
else if (current.state === "cancelled") {
|
|
156
202
|
state = "cancelled";
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
const successor = olderStoreSafe(() => store.replacementOf(current.id) ?? store.replacementOf(family.root.id), null);
|
|
204
|
+
detail = successor === null ? "This assignment was cancelled." : `${replacedWords(successor)}.`;
|
|
205
|
+
primaryAction = successor === null ? { code: "inspect-task", label: "View assignment", target: { taskId: current.id, runId: null, decisionId: null }, access: "read", retry: "read-again" }
|
|
206
|
+
: { code: "inspect-task", label: `Open ${successor}`, target: { taskId: successor, runId: null, decisionId: null }, access: "read", retry: "read-again" };
|
|
159
207
|
}
|
|
160
208
|
else if (!live && processProblem !== null && result !== null) {
|
|
161
209
|
state = "needs-decision";
|
|
@@ -170,6 +218,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
170
218
|
detail = question.decision.question;
|
|
171
219
|
primaryAction = workDecisionAction(question.taskId, question.decision, access.principal);
|
|
172
220
|
}
|
|
221
|
+
else if (reviewing && result !== null) {
|
|
222
|
+
state = "checking";
|
|
223
|
+
detail = "An automatic review is reading this result before it reaches you.";
|
|
224
|
+
primaryAction = { code: "inspect-run", label: "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
|
|
225
|
+
}
|
|
173
226
|
else if (ready && receipt !== null) {
|
|
174
227
|
state = "ready-to-check";
|
|
175
228
|
detail = completionKind === "research-report" ? "The research report is ready for the lead to read."
|
|
@@ -182,10 +235,12 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
182
235
|
// membership changes and ownership transfer cannot revoke past completion.
|
|
183
236
|
// The exact result digest still fences changes to work, scope and history.
|
|
184
237
|
if (checked !== undefined) {
|
|
185
|
-
|
|
238
|
+
const rootRef = store.lookupRef(family.root.id);
|
|
239
|
+
const byLead = rootRef !== null && olderStoreSafe(() => store.taskActs(rootRef.id), []).filter(one => one.act === "completed").at(-1)?.lead === true;
|
|
240
|
+
completion = { actor: String(checked["actor"]), at: String(checked["at"]), digest: receipt.digest, ...(byLead ? { lead: true } : {}) };
|
|
186
241
|
state = "complete";
|
|
187
242
|
primaryAction = { ...primaryAction, label: completionKind === "research-report" ? "Read report" : "Open result" };
|
|
188
|
-
const label = completion.actor.startsWith("operator:") ? completion.actor.slice(9) : owner && completion.actor === actorOf(owner) ? owner.label : "the previous lead";
|
|
243
|
+
const label = byLead ? "the lead" : completion.actor.startsWith("operator:") ? completion.actor.slice(9) : owner && completion.actor === actorOf(owner) ? owner.label : "the previous lead";
|
|
189
244
|
detail = completionKind === "research-report" ? `Research report checked by ${label}. No deployment is implied.`
|
|
190
245
|
: `Handled by ${label}. ${receipt.checks.detail} Publication and deployment are separate.`;
|
|
191
246
|
}
|
|
@@ -218,6 +273,11 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
218
273
|
attention.push(...(proof?.reasons ?? []).filter(reason => !historicalAssessmentReason(reason)));
|
|
219
274
|
attention.push(...receipt.caveats.filter(reason => !proof?.reasons.includes(reason)));
|
|
220
275
|
}
|
|
276
|
+
if (review !== null && review.sentBackAs === null) {
|
|
277
|
+
if (review.state === "not-reviewed")
|
|
278
|
+
attention.push(`Not reviewed: ${review.reason ?? "the automatic review did not finish"}.`);
|
|
279
|
+
attention.push(...review.high.map(one => `Review: ${findingWords(one)}`));
|
|
280
|
+
}
|
|
221
281
|
if (state !== "complete")
|
|
222
282
|
completion = null;
|
|
223
283
|
if (current.state !== "cancelled")
|
|
@@ -247,7 +307,7 @@ export function assignmentOf(store, taskId, now, access, root) {
|
|
|
247
307
|
return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
|
|
248
308
|
state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
|
|
249
309
|
publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
|
|
250
|
-
deployment: { status: "not-recorded" } };
|
|
310
|
+
review, deployment: { status: "not-recorded" } };
|
|
251
311
|
}
|
|
252
312
|
function admittedOwner(store, taskId, owner, now, root) {
|
|
253
313
|
if (owner.kind !== "coordinator")
|
|
@@ -316,6 +376,9 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
|
|
|
316
376
|
return { ok: false, reason: "approval-rules", message: problem };
|
|
317
377
|
store.recordAction({ at: now.toISOString(), actor, repo: current.repo,
|
|
318
378
|
taskId: current.rootId, runId: receipt.runId, action: CHECK_ACTION, outcome: receiptDigest, source: "work" });
|
|
379
|
+
const ref = store.lookupRef(current.rootId);
|
|
380
|
+
if (ref !== null)
|
|
381
|
+
store.noteTaskAct(ref.id, "completed", now);
|
|
319
382
|
store.bumpWake();
|
|
320
383
|
}
|
|
321
384
|
const assignment = assignmentOf(store, current.rootId, now, access, root);
|
|
@@ -335,7 +398,8 @@ export function checkAssignment(store, taskId, receiptDigest, owner, now, root =
|
|
|
335
398
|
/** The signed-in user can mark the same exact receipt handled without taking
|
|
336
399
|
* lead ownership. This records review, never check success or new authority. */
|
|
337
400
|
export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now, root = evidenceRoot(homedir())) {
|
|
338
|
-
|
|
401
|
+
// Completing is this person's act (or their lead's): it never pings them.
|
|
402
|
+
return withActor(currentActor() ?? { account: who.name, lead: false }, () => store.transact(() => {
|
|
339
403
|
if (!reproveApprover(store, who).ok)
|
|
340
404
|
return { ok: false, reason: "unauthenticated", message: "Sign in again before marking this result complete." };
|
|
341
405
|
const access = { principal: "operator", repos: who.repos };
|
|
@@ -343,7 +407,7 @@ export function checkAssignmentAsOperator(store, taskId, receiptDigest, who, now
|
|
|
343
407
|
if (current === null || !store.accountCanAccess(who.name, current.repo))
|
|
344
408
|
return { ok: false, reason: "not-found", message: "No assignment is available in your projects." };
|
|
345
409
|
return acknowledgeCurrent(store, current, receiptDigest, `operator:${who.name}`, now, root, access);
|
|
346
|
-
});
|
|
410
|
+
}));
|
|
347
411
|
}
|
|
348
412
|
function noteAssignmentHandoff(store, assignment, now) {
|
|
349
413
|
if (!assignment.owner?.active || assignment.handoff === null || assignment.handoff.acknowledged)
|