toolroll 0.9.7 → 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 +26 -1
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment.d.ts +6 -0
- package/dist/assignment.js +18 -3
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/check-levels-ui.d.ts +5 -0
- package/dist/check-levels-ui.js +16 -2
- package/dist/cli.js +2 -0
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -3
- package/dist/dispose.js +12 -4
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +1 -1
- package/dist/invoke.js +5 -0
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/operate.js +119 -22
- package/dist/process-custody.js +8 -0
- package/dist/provider.js +16 -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/serve.js +29 -4
- package/dist/store.d.ts +36 -1
- package/dist/store.js +136 -5
- package/dist/surface.js +5 -1
- package/dist/task-status.d.ts +5 -2
- package/dist/task-status.js +10 -6
- package/dist/work-summary.js +2 -1
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +9 -8
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -67,8 +67,9 @@ import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
|
67
67
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
68
68
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, attendedRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
69
69
|
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
70
|
+
import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
70
71
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, teammateFilers, usd } from "./spend.js";
|
|
71
|
-
import {
|
|
72
|
+
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
72
73
|
import { CHECKOUT_CLEANUP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
73
74
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
74
75
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
@@ -3801,7 +3802,10 @@ function initializeStore(db, file) {
|
|
|
3801
3802
|
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
3802
3803
|
// The sign-in pause the dispatch gate last left this task waiting on (null: none), for the work index.
|
|
3803
3804
|
addColumn(db, "task_ref", "auth_wait_pause", "INTEGER REFERENCES provider_auth_pause(id)");
|
|
3805
|
+
db.exec(REVIEW_SCHEMA);
|
|
3804
3806
|
db.exec(RETENTION_SCHEMA);
|
|
3807
|
+
// 1-day evidence: an older file's retention_setting only allowed 7 days or more (no version bump: its rows carry over).
|
|
3808
|
+
widenRetentionSchema(db);
|
|
3805
3809
|
db.exec(CHECKOUT_CLEANUP_SCHEMA);
|
|
3806
3810
|
db.exec(BACKUP_SCHEMA);
|
|
3807
3811
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
@@ -9333,6 +9337,27 @@ export class Store {
|
|
|
9333
9337
|
return;
|
|
9334
9338
|
this.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action, outcome, source: "policy", detail: `${before} → ${after}` });
|
|
9335
9339
|
}
|
|
9340
|
+
/** The project's automatic review switch: its explicit setting, else on
|
|
9341
|
+
* while a hands-off mode is active on it, else off. */
|
|
9342
|
+
reviewSwitch(repo, now) {
|
|
9343
|
+
const row = this.db.prepare("SELECT enabled, changed_by, changed_at FROM review_switch WHERE repo = ?").get(repo);
|
|
9344
|
+
if (row !== undefined)
|
|
9345
|
+
return { on: Number(row["enabled"]) === 1, source: "project", changedBy: String(row["changed_by"]), changedAt: String(row["changed_at"]) };
|
|
9346
|
+
const handsOff = this.activeMode(repo, now)?.name === "hands-off";
|
|
9347
|
+
return { on: handsOff, source: handsOff ? "hands-off" : "default", changedBy: null, changedAt: null };
|
|
9348
|
+
}
|
|
9349
|
+
/** Turn a project's automatic review on or off; the policy log keeps before → after. */
|
|
9350
|
+
setReviewSwitch(repo, on, by, now) {
|
|
9351
|
+
return this.transact(() => {
|
|
9352
|
+
const before = this.reviewSwitch(repo, now);
|
|
9353
|
+
this.db.prepare(`INSERT INTO review_switch (repo, enabled, changed_by, changed_at) VALUES (?, ?, ?, ?)
|
|
9354
|
+
ON CONFLICT(repo) DO UPDATE SET enabled = excluded.enabled, changed_by = excluded.changed_by, changed_at = excluded.changed_at`)
|
|
9355
|
+
.run(repo, on ? 1 : 0, by, now.toISOString());
|
|
9356
|
+
const after = this.reviewSwitch(repo, now);
|
|
9357
|
+
this.recordPolicy(by, repo, "automatic review", reviewSwitchWords(before), reviewSwitchWords(after), now);
|
|
9358
|
+
return { before, after };
|
|
9359
|
+
});
|
|
9360
|
+
}
|
|
9336
9361
|
/** A project deleted (project-delete.ts): who, and what it held → how it went. Returns the entry's id. */
|
|
9337
9362
|
recordProjectDeleted(by, repo, held, how, now) {
|
|
9338
9363
|
this.recordPolicy(by, repo, "project deleted", held, how, now, "deleted");
|
|
@@ -16353,13 +16378,17 @@ export class Store {
|
|
|
16353
16378
|
});
|
|
16354
16379
|
}
|
|
16355
16380
|
// ---- Retention (v105) -----------------------------------------------------
|
|
16356
|
-
/** How long each kind of data is kept; null is forever (
|
|
16381
|
+
/** How long each kind of data is kept; null is forever. A kind nobody chose keeps its default (evidence 28 days, the rest forever). */
|
|
16357
16382
|
retentionPeriods() {
|
|
16358
|
-
|
|
16383
|
+
return { ...DEFAULT_PERIODS, ...this.retentionChosen() };
|
|
16384
|
+
}
|
|
16385
|
+
/** The periods someone chose: only kinds with a saved row. */
|
|
16386
|
+
retentionChosen() {
|
|
16387
|
+
const chosen = {};
|
|
16359
16388
|
for (const row of this.db.prepare("SELECT kind, days FROM retention_setting").all()) {
|
|
16360
|
-
|
|
16389
|
+
chosen[String(row["kind"])] = row["days"] === null ? null : Number(row["days"]);
|
|
16361
16390
|
}
|
|
16362
|
-
return
|
|
16391
|
+
return chosen;
|
|
16363
16392
|
}
|
|
16364
16393
|
/** Set how long one kind is kept (null: forever); the ledger keeps before → after. */
|
|
16365
16394
|
setRetentionPeriod(kind, days, by, now) {
|
|
@@ -21060,6 +21089,108 @@ export class Store {
|
|
|
21060
21089
|
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND pid IS NULL AND ((containment IS NULL AND container IS NULL) OR container_empty_at IS NOT NULL)")
|
|
21061
21090
|
.run(now.toISOString(), witness);
|
|
21062
21091
|
}
|
|
21092
|
+
/** The reconcile road for a pid-less witness a crash or a thrown spawn
|
|
21093
|
+
* left behind: once its run has finished, no live child is tracked for it
|
|
21094
|
+
* here, every other witness of the run has exited and none of the run's
|
|
21095
|
+
* process groups is alive, it settles as never started, with a ledger
|
|
21096
|
+
* entry. A sole pid-less witness has no group to prove gone and stays for
|
|
21097
|
+
* `run settle`. Native witnesses keep their OS-only road. */
|
|
21098
|
+
settleUnspawnedWitnesses(now) {
|
|
21099
|
+
const candidates = this.db.prepare(`SELECT DISTINCT p.run FROM run_process p JOIN run r ON r.id = p.run
|
|
21100
|
+
WHERE p.exited_at IS NULL AND p.pid IS NULL AND p.containment IS NULL AND p.container IS NULL
|
|
21101
|
+
AND p.host = ? AND r.outcome IS NOT NULL AND r.finished_at IS NOT NULL ORDER BY p.run`).all(hostname());
|
|
21102
|
+
let settled = 0;
|
|
21103
|
+
for (const candidate of candidates) {
|
|
21104
|
+
const runId = Number(candidate["run"]);
|
|
21105
|
+
this.recordRunProcessExits(runId, now);
|
|
21106
|
+
settled += this.transact(() => {
|
|
21107
|
+
const run = this.getRun(runId);
|
|
21108
|
+
if (run === null || run.outcome === null)
|
|
21109
|
+
return 0;
|
|
21110
|
+
if (ownedProcessCount(runOwnerTag(this, runId)) > 0)
|
|
21111
|
+
return 0;
|
|
21112
|
+
const held = this.heldSessionOf(runId);
|
|
21113
|
+
if (held !== null && held.endedAt === null)
|
|
21114
|
+
return 0;
|
|
21115
|
+
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(runId);
|
|
21116
|
+
const pidless = rows.filter(row => row["exited_at"] === null && row["pid"] === null && row["containment"] === null && row["container"] === null && row["host"] === hostname());
|
|
21117
|
+
const others = rows.filter(row => !pidless.includes(row));
|
|
21118
|
+
if (pidless.length === 0 || others.length === 0)
|
|
21119
|
+
return 0;
|
|
21120
|
+
if (others.some(row => row["exited_at"] === null))
|
|
21121
|
+
return 0;
|
|
21122
|
+
// Exit was recorded once; the group's members are asked again now.
|
|
21123
|
+
const groups = others.filter(row => row["pid"] !== null && row["host"] === hostname());
|
|
21124
|
+
if (groups.length === 0 || groups.some(row => processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt })))
|
|
21125
|
+
return 0;
|
|
21126
|
+
const ref = this.refById(run.taskRef);
|
|
21127
|
+
let count = 0;
|
|
21128
|
+
for (const row of pidless) {
|
|
21129
|
+
const changed = this.db.prepare(`UPDATE run_process SET exited_at = ?
|
|
21130
|
+
WHERE id = ? AND run = ? AND pid IS NULL AND exited_at IS NULL AND containment IS NULL AND container IS NULL`)
|
|
21131
|
+
.run(now.toISOString(), row["id"], runId);
|
|
21132
|
+
if (Number(changed.changes) !== 1)
|
|
21133
|
+
continue;
|
|
21134
|
+
count++;
|
|
21135
|
+
this.recordAction({ at: now.toISOString(), actor: "system", repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId,
|
|
21136
|
+
action: "process witness settled", outcome: "never started", source: "work",
|
|
21137
|
+
detail: `witness ${String(row["id"])}: no pid; the run finished, every other witness exited and none of its process groups is alive` });
|
|
21138
|
+
}
|
|
21139
|
+
return count;
|
|
21140
|
+
});
|
|
21141
|
+
}
|
|
21142
|
+
return settled;
|
|
21143
|
+
}
|
|
21144
|
+
/** `run settle` (an approver's last resort): every witness of a finished
|
|
21145
|
+
* run that cannot be proven either way is recorded settled under the
|
|
21146
|
+
* approver's reason, in the ledger. Refuses while anything of the run is
|
|
21147
|
+
* alive or could be checked only from another host. Never signals. */
|
|
21148
|
+
settleRunWitnessesByApprover(args, now) {
|
|
21149
|
+
return this.transact(() => {
|
|
21150
|
+
const run = this.getRun(args.runId);
|
|
21151
|
+
if (run === null)
|
|
21152
|
+
return { ok: false, reason: "unknown-run", detail: `no run #${args.runId}` };
|
|
21153
|
+
if (run.outcome === null)
|
|
21154
|
+
return { ok: false, reason: "still-running", detail: `run #${args.runId} is still open` };
|
|
21155
|
+
const alive = (detail) => ({ ok: false, reason: "alive", detail });
|
|
21156
|
+
if (ownedProcessCount(runOwnerTag(this, args.runId)) > 0)
|
|
21157
|
+
return alive(`run #${args.runId} still has an owned subprocess`);
|
|
21158
|
+
const held = this.heldSessionOf(args.runId);
|
|
21159
|
+
if (held !== null && held.endedAt === null)
|
|
21160
|
+
return alive(`run #${args.runId}'s held supervisor has not finished shutdown`);
|
|
21161
|
+
const rows = this.db.prepare("SELECT * FROM run_process WHERE run = ? ORDER BY id").all(args.runId);
|
|
21162
|
+
const open = rows.filter(row => row["exited_at"] === null);
|
|
21163
|
+
for (const row of rows) {
|
|
21164
|
+
if (row["pid"] === null)
|
|
21165
|
+
continue;
|
|
21166
|
+
if (row["host"] !== hostname()) {
|
|
21167
|
+
if (row["exited_at"] === null)
|
|
21168
|
+
return alive(`run #${args.runId}'s process ${String(row["pid"])} belongs to ${String(row["host"])}; settle it there`);
|
|
21169
|
+
continue;
|
|
21170
|
+
}
|
|
21171
|
+
if (processMayBeAlive(Number(row["pid"]), row["process_group"] === 1, { observedAt: row["observed_at"], finishedAt: run.finishedAt }))
|
|
21172
|
+
return alive(`run #${args.runId}'s process ${String(row["pid"])} is still running`);
|
|
21173
|
+
}
|
|
21174
|
+
for (const row of open) {
|
|
21175
|
+
if (row["pid"] === null && row["host"] !== hostname())
|
|
21176
|
+
return alive(`run #${args.runId} has a witness from ${String(row["host"])}; settle it there`);
|
|
21177
|
+
if (row["containment"] != null && row["container"] != null && row["container_empty_at"] == null &&
|
|
21178
|
+
containerEmptiness(String(row["containment"]), String(row["container"]), process.platform, row["container_identity"] == null ? null : String(row["container_identity"])) === "populated") {
|
|
21179
|
+
return alive(`run #${args.runId}'s ${String(row["containment"])} object ${String(row["container"])} still has members`);
|
|
21180
|
+
}
|
|
21181
|
+
}
|
|
21182
|
+
if (open.length === 0)
|
|
21183
|
+
return { ok: true, witnesses: [], repeated: true };
|
|
21184
|
+
const ids = open.map(row => Number(row["id"]));
|
|
21185
|
+
for (const id of ids)
|
|
21186
|
+
this.db.prepare("UPDATE run_process SET exited_at = ? WHERE id = ? AND run = ? AND exited_at IS NULL").run(now.toISOString(), id, args.runId);
|
|
21187
|
+
const ref = this.refById(run.taskRef);
|
|
21188
|
+
this.recordAction({ at: now.toISOString(), actor: args.by, repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId: args.runId,
|
|
21189
|
+
action: "process witness settled by approver", outcome: `witness ${ids.join(", ")}`, source: "request", detail: args.why });
|
|
21190
|
+
this.settleQuiescentStops(now);
|
|
21191
|
+
return { ok: true, witnesses: ids, repeated: false };
|
|
21192
|
+
});
|
|
21193
|
+
}
|
|
21063
21194
|
recordRunProcess(runId, pid, now, group = true, witness) {
|
|
21064
21195
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
21065
21196
|
throw new Error("a process witness needs a valid spawned PID");
|
package/dist/surface.js
CHANGED
|
@@ -265,12 +265,13 @@ export const COMMAND_GUIDE = [
|
|
|
265
265
|
operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
|
|
266
266
|
operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
|
|
267
267
|
operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
|
|
268
|
-
operator("task review", "
|
|
268
|
+
operator("task review", "show a run's automatic review: HIGH findings, suggested follow-ups, or why it was not reviewed"),
|
|
269
269
|
operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
|
|
270
270
|
operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
|
|
271
271
|
operator("task route", "which configured agents plan, build and repair this task; phase and risk changes require renewed approval"),
|
|
272
272
|
operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
|
|
273
273
|
operator("task stop", "stop ONE exact live attempt (--run <id>): durable before any process is signalled, answers 'stopping' until its own processes are established gone; work, branch, and evidence preserved; no strike, no retry"),
|
|
274
|
+
operator("run settle", "last resort for a finished run whose process witness can't be proven either way: records your --why in the ledger; refuses while any of the run's processes is alive"),
|
|
274
275
|
operator("task resume", "resume the exact stopped attempt (--run <id>): refuses until it is quiescent, lifts only that stop's hold, approves nothing — the next pass re-proves the scope and inherits the draft with fresh proof"),
|
|
275
276
|
// ---- leases (runner surface) ----
|
|
276
277
|
{ invocation: "claim", synopsis: "take one ready, approved task; answers with a lease", audience: "agent", agentMayInvoke: true, mutation: "keyed",
|
|
@@ -358,6 +359,9 @@ export const COMMAND_GUIDE = [
|
|
|
358
359
|
operator("mode set", "sign a per-repository operating mode (password ceremony; standard or hands-off, always expiring)"),
|
|
359
360
|
operator("publish merge", "say yes to ONE waiting merge — it fires only when CI is seen green on the exact commit you authorized"),
|
|
360
361
|
operator("publish refire", "recover a merge whose issued call went silent — re-enters the full merge road, never auto-retried"),
|
|
362
|
+
operatorRead("review show", "whether a project's finished builds get one automatic review"),
|
|
363
|
+
operator("review on", "one automatic review of each finished build whose check passes; only HIGH findings send it back, once"),
|
|
364
|
+
operator("review off", "finished builds go straight to the person, unreviewed"),
|
|
361
365
|
operator("mode revoke", "revoke a repository's mode — one click, every act it covered falls back to its own ceremony"),
|
|
362
366
|
operator("config set", "set phase or spend configuration"),
|
|
363
367
|
operator("config clear", "clear phase or spend configuration"),
|
package/dist/task-status.d.ts
CHANGED
|
@@ -42,7 +42,8 @@ export type TaskStatus = {
|
|
|
42
42
|
primaryAction: StatusAction | null;
|
|
43
43
|
why: string[];
|
|
44
44
|
};
|
|
45
|
-
|
|
45
|
+
/** `reviewing`: the one automatic review (build-review.ts) is reading a finished build; it reads Building, not a headline of its own. */
|
|
46
|
+
export type TaskStage = "queued" | "planning" | "needs-you" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
46
47
|
export type ChecksFact = {
|
|
47
48
|
status: "passed" | "failed" | "running" | "not-run" | "unavailable";
|
|
48
49
|
exitCode: number | null;
|
|
@@ -121,7 +122,9 @@ export declare function pullRequestFactOf(publication: {
|
|
|
121
122
|
} | null): PullRequestFact | null;
|
|
122
123
|
/** The assignment's stage. The work status, when the caller read one, names
|
|
123
124
|
* the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
|
|
124
|
-
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"
|
|
125
|
+
export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"> & {
|
|
126
|
+
review?: AssignmentSnapshot["review"];
|
|
127
|
+
}, work?: {
|
|
125
128
|
token: string;
|
|
126
129
|
views?: readonly string[];
|
|
127
130
|
} | null, planning?: boolean): {
|
package/dist/task-status.js
CHANGED
|
@@ -13,7 +13,8 @@ export function headlineOf(facts) {
|
|
|
13
13
|
case "planning": return "Planning";
|
|
14
14
|
case "needs-you": return "Needs you";
|
|
15
15
|
case "building":
|
|
16
|
-
case "checking":
|
|
16
|
+
case "checking":
|
|
17
|
+
case "reviewing": return "Building";
|
|
17
18
|
case "finished":
|
|
18
19
|
if (facts.report)
|
|
19
20
|
return "Ready for review";
|
|
@@ -36,7 +37,8 @@ function sentenceOf(headline, facts) {
|
|
|
36
37
|
case "Needs you": return facts.need === "approval" ? "Review the plan and approve it to start." : reason ?? (facts.need === "answer" ? "Answer the question so the work can continue."
|
|
37
38
|
: facts.need === "sign-in" ? "Sign in again; the task starts on its own after."
|
|
38
39
|
: "Something needs your decision before the work can continue.");
|
|
39
|
-
case "Building": return facts.stage === "checking" ? "Checks are running on the change."
|
|
40
|
+
case "Building": return facts.stage === "checking" ? "Checks are running on the change."
|
|
41
|
+
: facts.stage === "reviewing" ? "Reviewing the change before it reaches you." : reason ?? "An agent is working on it.";
|
|
40
42
|
case "Ready for review":
|
|
41
43
|
if (facts.report)
|
|
42
44
|
return "The report is ready to read. Read it, then mark it complete.";
|
|
@@ -140,7 +142,7 @@ export function taskStatusOf(facts) {
|
|
|
140
142
|
const CODE_STAGE = {
|
|
141
143
|
queued: ["queued"], ready: ["queued"], "worker-at-capacity": ["queued"], "retry-scheduled": ["queued"], updating: ["queued"],
|
|
142
144
|
"waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"], "planner-source": ["queued"],
|
|
143
|
-
running: ["building"], reviewing: ["
|
|
145
|
+
running: ["building"], reviewing: ["reviewing"], "review-pending": ["building"],
|
|
144
146
|
"signed-out": ["needs-you", "sign-in"],
|
|
145
147
|
"needs-approval": ["needs-you", "approval"],
|
|
146
148
|
"waiting-decision": ["needs-you", "answer"], "decision-queue": ["needs-you", "answer"],
|
|
@@ -154,7 +156,7 @@ const CODE_STAGE = {
|
|
|
154
156
|
* building reason is scheduler wording, and the default sentence is plainer. */
|
|
155
157
|
const QUEUED_REASONS = new Set(["worker-at-capacity", "waiting-dependency", "retry-scheduled", "updating", "planning-ready", "scouting-ready", "provider-quota"]);
|
|
156
158
|
export function plainReasonOf(stage, code, detail) {
|
|
157
|
-
if (stage === "building" || stage === "checking" || stage === "planning")
|
|
159
|
+
if (stage === "building" || stage === "checking" || stage === "reviewing" || stage === "planning")
|
|
158
160
|
return null;
|
|
159
161
|
if (stage === "queued")
|
|
160
162
|
return QUEUED_REASONS.has(code) ? detail ?? null : null;
|
|
@@ -199,7 +201,7 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
199
201
|
case "cancelled": return { stage: "stopped" };
|
|
200
202
|
case "complete": return { stage: "complete" };
|
|
201
203
|
case "ready-to-check": return { stage: "finished" };
|
|
202
|
-
case "checking": return { stage: "checking" };
|
|
204
|
+
case "checking": return { stage: assignment.review?.state === "pending" ? "reviewing" : "checking" };
|
|
203
205
|
case "working": {
|
|
204
206
|
const token = work?.token ?? "running";
|
|
205
207
|
const read = stageOfCode(token, { needsPerson: work?.views?.includes("needs-you") ?? false, planning });
|
|
@@ -325,7 +327,9 @@ export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center
|
|
|
325
327
|
* no assignment. A done task is Complete once a person marked its result;
|
|
326
328
|
* a refuted result whose project check failed is Failed. */
|
|
327
329
|
export function stageOfDispatch(d, options = {}) {
|
|
328
|
-
if (d.code === "
|
|
330
|
+
if (d.code === "reviewing")
|
|
331
|
+
return { stage: "reviewing" };
|
|
332
|
+
if (d.code === "complete" || d.code.startsWith("review-"))
|
|
329
333
|
return { stage: options.completed ? "complete" : "finished" };
|
|
330
334
|
if (d.code === "proof-refuted")
|
|
331
335
|
return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "other" };
|
package/dist/work-summary.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resultCheckLevel } from "./result-follow-ups.js";
|
|
|
6
6
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
7
7
|
import { isAlive } from "./runner.js";
|
|
8
8
|
import { taskControlOf } from "./task-control.js";
|
|
9
|
+
import { buildReviewOf } from "./review-switch.js";
|
|
9
10
|
import { reviewFactsOf, workStatusOf } from "./workspace-ui.js";
|
|
10
11
|
/** Call only after admitting the task's project. Keep the question's
|
|
11
12
|
* exact run and id; never substitute a family's newest attempt. Expiry
|
|
@@ -103,7 +104,7 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
|
|
|
103
104
|
review: reviewFactsOf(store.reviewRetryStateOf(result.id), name => {
|
|
104
105
|
const runner = store.getRunner(name)?.runner;
|
|
105
106
|
return runner !== undefined && isAlive(runner, now);
|
|
106
|
-
}),
|
|
107
|
+
}, buildReviewOf(store, result.id)?.state === "pending"),
|
|
107
108
|
...(result.outcome !== "no-change" ? {} : { recordComplete: (() => {
|
|
108
109
|
const kinds = new Set(store.artifactsFor(result.id).map(one => one.kind));
|
|
109
110
|
return kinds.has("handoff") && kinds.has("terminal-diff");
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -68,11 +68,13 @@ export type ReviewFacts = {
|
|
|
68
68
|
queuedOrigin?: "operator" | "automatic" | null;
|
|
69
69
|
/** Whether the live attempt's reviewer worker is answering. */
|
|
70
70
|
reviewerAlive: boolean;
|
|
71
|
+
/** The build's one automatic review is still pending (build-review.ts). */
|
|
72
|
+
automaticPending?: boolean;
|
|
71
73
|
};
|
|
72
74
|
/** The review facts from the store's bounded retry projection — the same
|
|
73
75
|
* reading `diagnoseTaskDispatch` makes, so every surface starts from one
|
|
74
76
|
* record. `reviewerAlive` answers for the live attempt's runner. */
|
|
75
|
-
export declare function reviewFactsOf(retry: ReviewRetryState | null, reviewerAlive: (runner: string) => boolean): ReviewFacts | null;
|
|
77
|
+
export declare function reviewFactsOf(retry: ReviewRetryState | null, reviewerAlive: (runner: string) => boolean, automaticPending?: boolean): ReviewFacts | null;
|
|
76
78
|
/** The tokens a review in flight wears — the dispatch codes, unchanged. */
|
|
77
79
|
export declare const REVIEW_TOKENS: ReadonlySet<string>;
|
|
78
80
|
/**
|
package/dist/workspace-ui.js
CHANGED
|
@@ -30,7 +30,7 @@ export function parseWorkView(raw) {
|
|
|
30
30
|
/** The review facts from the store's bounded retry projection — the same
|
|
31
31
|
* reading `diagnoseTaskDispatch` makes, so every surface starts from one
|
|
32
32
|
* record. `reviewerAlive` answers for the live attempt's runner. */
|
|
33
|
-
export function reviewFactsOf(retry, reviewerAlive) {
|
|
33
|
+
export function reviewFactsOf(retry, reviewerAlive, automaticPending = false) {
|
|
34
34
|
if (retry === null || retry.state === "unrequested")
|
|
35
35
|
return null;
|
|
36
36
|
const current = retry.live ?? (retry.state === "queued" ? null : retry.latest);
|
|
@@ -45,6 +45,7 @@ export function reviewFactsOf(retry, reviewerAlive) {
|
|
|
45
45
|
queuedBy: retry.openRequest?.requestedBy ?? null,
|
|
46
46
|
queuedOrigin: retry.openRequest?.origin ?? null,
|
|
47
47
|
reviewerAlive: retry.live !== null && reviewerAlive(retry.live.runner),
|
|
48
|
+
automaticPending,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
/** The tokens a review in flight wears — the dispatch codes, unchanged. */
|
|
@@ -59,12 +60,12 @@ const retriesLeft = (count, explicit = true) => `${count}${explicit ? " explicit
|
|
|
59
60
|
* the v50 dispatch contract; nothing here changes the lifecycle.
|
|
60
61
|
*/
|
|
61
62
|
export function reviewStatusOf(review) {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
63
|
+
// The one automatic review (build-review.ts) holds the result only while it
|
|
64
|
+
// is queued or running. A failed review never blocks: the result reads as
|
|
65
|
+
// it would unreviewed, marked "not reviewed" by the result itself.
|
|
66
|
+
if (review === null || review.automaticPending !== true || (review.state !== "queued" && review.state !== "running"))
|
|
67
|
+
return null;
|
|
68
|
+
return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label: "Open the result", kind: "open-result" } };
|
|
68
69
|
}
|
|
69
70
|
const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
70
71
|
export function evidenceProblemOf(verdict, reasons) {
|
|
@@ -123,7 +124,7 @@ export function publicationStatusOf(publication) {
|
|
|
123
124
|
* the detail).
|
|
124
125
|
*/
|
|
125
126
|
export function resultStatusOf(result, publication = null) {
|
|
126
|
-
return storedResultStatusOf(result, publication);
|
|
127
|
+
return (result?.role === "builder" ? reviewStatusOf(result.review ?? null) : null) ?? storedResultStatusOf(result, publication);
|
|
127
128
|
}
|
|
128
129
|
/** What the machine itself recorded about a result, in a clause. */
|
|
129
130
|
function machineVerdictWords(result) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|