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.
Files changed (102) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/dist/actor.d.ts +21 -0
  3. package/dist/actor.js +48 -0
  4. package/dist/assignment-adapters.d.ts +2 -0
  5. package/dist/assignment-adapters.js +2 -0
  6. package/dist/assignment-ui.js +3 -1
  7. package/dist/assignment.d.ts +15 -0
  8. package/dist/assignment.js +80 -16
  9. package/dist/browser/workspace.css +2 -2
  10. package/dist/browser/workspace.js +28 -24
  11. package/dist/browser-workspace.d.ts +117 -2
  12. package/dist/build-review.d.ts +38 -0
  13. package/dist/build-review.js +341 -0
  14. package/dist/builder.d.ts +1 -0
  15. package/dist/builder.js +33 -13
  16. package/dist/chat-actions.js +2 -1
  17. package/dist/chat-delivery.js +2 -0
  18. package/dist/chat-quiet.js +11 -1
  19. package/dist/chat-task-actions.d.ts +13 -0
  20. package/dist/chat-task-actions.js +19 -1
  21. package/dist/check-levels-ui.d.ts +26 -0
  22. package/dist/check-levels-ui.js +84 -0
  23. package/dist/check-levels.d.ts +67 -0
  24. package/dist/check-levels.js +171 -0
  25. package/dist/claim.d.ts +22 -0
  26. package/dist/claim.js +43 -0
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +6 -0
  29. package/dist/demo-chat.js +4 -1
  30. package/dist/dispatch.js +9 -3
  31. package/dist/dispose.d.ts +12 -3
  32. package/dist/dispose.js +19 -5
  33. package/dist/exec.d.ts +5 -0
  34. package/dist/exec.js +30 -4
  35. package/dist/export.js +3 -2
  36. package/dist/first-run.d.ts +34 -0
  37. package/dist/first-run.js +37 -1
  38. package/dist/flow-gallery-ui.d.ts +30 -0
  39. package/dist/flow-gallery-ui.js +85 -0
  40. package/dist/flow-gallery.d.ts +111 -0
  41. package/dist/flow-gallery.js +445 -0
  42. package/dist/flow-share.d.ts +102 -0
  43. package/dist/flow-share.js +614 -0
  44. package/dist/flow-starters.d.ts +2 -0
  45. package/dist/flow-starters.js +1 -1
  46. package/dist/flow-steps.js +7 -0
  47. package/dist/flow-triggers.js +2 -0
  48. package/dist/flows-cli.d.ts +56 -0
  49. package/dist/flows-cli.js +109 -3
  50. package/dist/flows-ui.d.ts +15 -1
  51. package/dist/flows-ui.js +31 -6
  52. package/dist/guides.js +27 -2
  53. package/dist/invoke.js +5 -0
  54. package/dist/lead-context.js +6 -2
  55. package/dist/mate-doors.d.ts +0 -15
  56. package/dist/mate-doors.js +5 -1
  57. package/dist/mate-tools.js +9 -4
  58. package/dist/models-ui.js +1 -1
  59. package/dist/modes.d.ts +3 -1
  60. package/dist/modes.js +3 -1
  61. package/dist/operate.d.ts +4 -1
  62. package/dist/operate.js +711 -98
  63. package/dist/process-custody.js +8 -0
  64. package/dist/project-concurrency.d.ts +62 -0
  65. package/dist/project-concurrency.js +116 -0
  66. package/dist/project-delete-ui.d.ts +7 -0
  67. package/dist/project-delete-ui.js +19 -2
  68. package/dist/project-delete.js +4 -0
  69. package/dist/provider.js +16 -0
  70. package/dist/pull-request-flow.d.ts +9 -0
  71. package/dist/pull-request-flow.js +21 -3
  72. package/dist/result-follow-ups.d.ts +101 -0
  73. package/dist/result-follow-ups.js +333 -0
  74. package/dist/retention-ui.d.ts +3 -1
  75. package/dist/retention-ui.js +4 -4
  76. package/dist/retention.d.ts +13 -6
  77. package/dist/retention.js +50 -12
  78. package/dist/review-switch.d.ts +41 -0
  79. package/dist/review-switch.js +88 -0
  80. package/dist/reviewer.d.ts +24 -2
  81. package/dist/reviewer.js +49 -1
  82. package/dist/scope.js +11 -0
  83. package/dist/serve.d.ts +211 -2
  84. package/dist/serve.js +1038 -145
  85. package/dist/store.d.ts +112 -4
  86. package/dist/store.js +447 -27
  87. package/dist/surface.js +36 -9
  88. package/dist/tailnet.d.ts +11 -0
  89. package/dist/tailnet.js +47 -0
  90. package/dist/task-outcome-cli.js +2 -2
  91. package/dist/task-status.d.ts +13 -3
  92. package/dist/task-status.js +38 -16
  93. package/dist/team-cli.js +3 -0
  94. package/dist/telegram-status.js +2 -2
  95. package/dist/verification-evidence.js +21 -1
  96. package/dist/webhooks.js +5 -0
  97. package/dist/work-index.js +39 -6
  98. package/dist/work-summary.d.ts +0 -4
  99. package/dist/work-summary.js +4 -1
  100. package/dist/workspace-ui.d.ts +6 -1
  101. package/dist/workspace-ui.js +14 -11
  102. package/package.json +1 -1
package/dist/surface.js CHANGED
@@ -135,8 +135,27 @@ export const COMMAND_GUIDE = [
135
135
  flags: [jsonFlag, dbFlag, keyFlag,
136
136
  { name: "id", takesValue: true, meaning: "choose the task id" },
137
137
  { name: "repo", takesValue: true, meaning: "which repository the task belongs to" },
138
- { name: "report", takesValue: false, meaning: "a scout task: the deliverable is a report, never a branch" }],
139
- notableReasons: ["duplicate", "usage"] },
138
+ { name: "report", takesValue: false, meaning: "a scout task: the deliverable is a report, never a branch" },
139
+ { name: "replaces", takesValue: true, meaning: "the task this one replaces: it is cancelled and reads \"Replaced by <id>\"" },
140
+ { name: "token", takesValue: true, meaning: "a lead token: filed as the lead, so it pings nobody" },
141
+ { name: "checks", takesValue: true, meaning: "quick|full|off: this task's checks, overriding the project's level (\"skip the tests\" is off)" }],
142
+ notableReasons: ["duplicate", "usage", "unknown-task", "unauthenticated"] },
143
+ { invocation: "task ask", synopsis: "hand a task to a person: one message saying why, and the task is theirs", audience: "agent", agentMayInvoke: true, mutation: "unkeyed",
144
+ positionals: [{ name: "id", required: true, meaning: "the task" }],
145
+ flags: [jsonFlag, dbFlag,
146
+ { name: "person", takesValue: true, meaning: "who to ask (someone who can see the task's project)" },
147
+ { name: "why", takesValue: true, meaning: "what they need to do, one line" },
148
+ { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }],
149
+ notableReasons: ["unknown-task", "unknown-person", "unplaced", "unauthenticated"] },
150
+ { invocation: "task checks", synopsis: "run checks on the task's latest result, on its exact commit, now; a pass upgrades its status and a failure stays visible",
151
+ audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
152
+ positionals: [{ name: "task", required: true, meaning: "any version of the task" }],
153
+ flags: [jsonFlag, dbFlag, { name: "level", takesValue: true, meaning: "quick (default) or full" }],
154
+ notableReasons: ["no-result", "no-check", "checks-failed"] },
155
+ { invocation: "task add-tests", synopsis: "file a small task to write tests for the task's latest change; it waits for approval like any task",
156
+ audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
157
+ positionals: [{ name: "task", required: true, meaning: "any version of the task" }], flags: [jsonFlag, dbFlag],
158
+ notableReasons: ["no-result"] },
140
159
  { invocation: "task list", synopsis: "a page of saved task status", audience: "agent", agentMayInvoke: true, mutation: "none",
141
160
  flags: [jsonFlag, dbFlag, { name: "state", takesValue: true, meaning: "queued|running|done|failed|cancelled" },
142
161
  { name: "view", takesValue: true, meaning: "all|needs-you|running|completed" },
@@ -168,8 +187,9 @@ export const COMMAND_GUIDE = [
168
187
  { invocation: "task merge", synopsis: "merge the task's pull request once its checks pass — the console's Merge, behind the approver's password; the project's method (squash by default), branch deleted, recorded in the ledger",
169
188
  audience: "operator", agentMayInvoke: false, mutation: "identity-idempotent",
170
189
  positionals: [{ name: "task", required: true, meaning: "any version of the task" }],
171
- flags: [jsonFlag, dbFlag, { name: "as", takesValue: true, meaning: "the approver merging" }, { name: "token", takesValue: true, meaning: "their password" }],
172
- notableReasons: ["usage", "not-an-approver"] },
190
+ flags: [jsonFlag, dbFlag, { name: "as", takesValue: true, meaning: "the approver merging" }, { name: "token", takesValue: true, meaning: "their password" },
191
+ { name: "anyway", takesValue: false, meaning: "merge a Quick or Off result without waiting for its full check" }],
192
+ notableReasons: ["usage", "not-an-approver", "full-checks"] },
173
193
  ...["show", "updates", "claim", "check", "brief", "inbox", "ack"].map(action => ({
174
194
  invocation: `assignment ${action}`,
175
195
  synopsis: action === "show" ? "read the root, current work, owner and exact receipt"
@@ -193,7 +213,8 @@ export const COMMAND_GUIDE = [
193
213
  ...(action === "ack" ? [{ name: "batch", takesValue: true, meaning: "exact batch ID returned by assignment inbox" }] : []),
194
214
  ],
195
215
  })),
196
- operator("task state", "set a task's state by hand — an operator correction, not a workflow step"),
216
+ operator("task state", "set a task's state by hand — an operator correction, not a workflow step (--replaced-by <id> with cancelled)"),
217
+ operator("lead token", "mint the lead's credential behind your password (--revoke ends it): its own work pings nobody"),
197
218
  { invocation: "task block", synopsis: "make one task wait for another", audience: "agent", agentMayInvoke: true, mutation: "keyed",
198
219
  positionals: [{ name: "id", required: true, meaning: "the waiting task" }],
199
220
  flags: [jsonFlag, dbFlag, keyFlag, { name: "on", takesValue: true, meaning: "the blocker's id" }],
@@ -244,12 +265,13 @@ export const COMMAND_GUIDE = [
244
265
  operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
245
266
  operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
246
267
  operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
247
- operator("task review", "retired compatibility command; inspect the saved result and explicitly complete or revise it"),
268
+ operator("task review", "show a run's automatic review: HIGH findings, suggested follow-ups, or why it was not reviewed"),
248
269
  operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
249
270
  operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
250
271
  operator("task route", "which configured agents plan, build and repair this task; phase and risk changes require renewed approval"),
251
272
  operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
252
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"),
253
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"),
254
276
  // ---- leases (runner surface) ----
255
277
  { invocation: "claim", synopsis: "take one ready, approved task; answers with a lease", audience: "agent", agentMayInvoke: true, mutation: "keyed",
@@ -289,7 +311,7 @@ export const COMMAND_GUIDE = [
289
311
  positionals: [{ name: "id", required: true, meaning: "the routine" }], flags: [jsonFlag, dbFlag] },
290
312
  { invocation: "config show", synopsis: "phase and spend configuration", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag, repoFlag] },
291
313
  { invocation: "setup show", synopsis: "installation setup", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
292
- { invocation: "verify show", synopsis: "the repository's approved verification command, if one exists", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag, repoFlag] },
314
+ { invocation: "verify show", synopsis: "the repository's check level, its approved full and quick commands, and a suggested quick command", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag, repoFlag] },
293
315
  { invocation: "intake show", synopsis: "intake configuration", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
294
316
  { invocation: "providers", synopsis: "which agent providers this binary can drive, and their transports; --report records this machine's readiness under its runner", audience: "agent", agentMayInvoke: true, mutation: "none",
295
317
  flags: [jsonFlag,
@@ -309,6 +331,7 @@ export const COMMAND_GUIDE = [
309
331
  operator("runner retire", "retire a worker"),
310
332
  operator("runner bind", "replace which repositories a worker may build in"),
311
333
  operator("runner capacity", "set how many tasks a worker runs at once; running work carries on, the new number applies at the next claim"),
334
+ operator("project concurrency", "set how many of a project's tasks build at once (default 2, never past the worker's capacity); running work carries on"),
312
335
  operator("coordinator mint", "mint the MCP filing credential — repo-bound, rate-limited, token shown once"),
313
336
  operator("coordinator revoke", "revoke an MCP filing credential"),
314
337
  operator("mcp", "serve the MCP gateway over stdio — the coordinator credential is the only key it accepts"),
@@ -336,12 +359,16 @@ export const COMMAND_GUIDE = [
336
359
  operator("mode set", "sign a per-repository operating mode (password ceremony; standard or hands-off, always expiring)"),
337
360
  operator("publish merge", "say yes to ONE waiting merge — it fires only when CI is seen green on the exact commit you authorized"),
338
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"),
339
365
  operator("mode revoke", "revoke a repository's mode — one click, every act it covered falls back to its own ceremony"),
340
366
  operator("config set", "set phase or spend configuration"),
341
367
  operator("config clear", "clear phase or spend configuration"),
342
368
  operator("setup clear", "clear installation setup"),
343
- operator("verify set", "approve the one shell command the plane re-runs after every future commit; --self-heal binds one approved-setup replay when a required project executable is missing"),
344
- operator("verify clear", "revoke a repository's approved verification command"),
369
+ operator("verify set", "approve the one shell command the plane re-runs after every future commit (--quick \"<cmd>\" approves the fast check Quick runs); --self-heal binds one approved-setup replay when a required project executable is missing"),
370
+ operator("verify clear", "revoke a repository's approved verification command; --level quick revokes only the quick command"),
371
+ operator("verify level", "set a project's checks after each build: quick (the approved quick command), full, or off — an approver's act, in the ledger"),
345
372
  operator("intake grant", "grant an intake source"),
346
373
  operator("intake run", "run intake now"),
347
374
  operator("intake preview", "preview what intake would file"),
@@ -0,0 +1,11 @@
1
+ type Runner = (file: string, args: readonly string[], options: {
2
+ timeoutMs: number;
3
+ }) => Promise<{
4
+ code: number | null;
5
+ stdout: string;
6
+ notFound?: boolean;
7
+ }>;
8
+ /** The full MagicDNS name first (machine.tailnet.ts.net), then its short form (machine). Empty when Tailscale isn't running. */
9
+ export declare function tailnetNamesOf(statusJson: string): string[];
10
+ export declare function readTailnetNames(runner?: Runner, platform?: NodeJS.Platform): Promise<string[]>;
11
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * This computer's own names on its tailnet, when Tailscale is up: the console
3
+ * answers to them on its port without an --allow-host, and the phone card
4
+ * names the address to type. Read with `tailscale status --json`, which
5
+ * spends nothing and changes nothing; any failure means no tailnet.
6
+ */
7
+ import { existsSync } from "node:fs";
8
+ import { run } from "./exec.js";
9
+ /** The Mac app ships its command line inside the app bundle. */
10
+ const MAC_APP_CLI = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
11
+ const NAME = /^[a-z0-9]([a-z0-9-]{0,62})(\.[a-z0-9]([a-z0-9-]{0,62}))*$/;
12
+ /** The full MagicDNS name first (machine.tailnet.ts.net), then its short form (machine). Empty when Tailscale isn't running. */
13
+ export function tailnetNamesOf(statusJson) {
14
+ let status;
15
+ try {
16
+ status = JSON.parse(statusJson);
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ if (typeof status !== "object" || status === null)
22
+ return [];
23
+ const body = status;
24
+ if (body.BackendState !== "Running" || typeof body.Self?.DNSName !== "string")
25
+ return [];
26
+ const full = body.Self.DNSName.replace(/\.$/, "").toLowerCase();
27
+ if (!NAME.test(full) || full.length > 253)
28
+ return [];
29
+ const short = full.split(".")[0];
30
+ return short === full ? [full] : [full, short];
31
+ }
32
+ export async function readTailnetNames(runner = run, platform = process.platform) {
33
+ const candidates = ["tailscale", ...(platform === "darwin" && existsSync(MAC_APP_CLI) ? [MAC_APP_CLI] : [])];
34
+ for (const file of candidates) {
35
+ try {
36
+ const answer = await runner(file, ["status", "--json"], { timeoutMs: 3_000 });
37
+ if (answer.notFound === true)
38
+ continue;
39
+ if (answer.code === 0)
40
+ return tailnetNamesOf(answer.stdout);
41
+ }
42
+ catch {
43
+ // No Tailscale here: no tailnet names.
44
+ }
45
+ }
46
+ return [];
47
+ }
@@ -126,7 +126,7 @@ export async function runTaskMergeCommand(positional, flags, context) {
126
126
  return result.ok ? 0 : result.reason === 'usage' ? 2 : 3;
127
127
  };
128
128
  const fail = (reason, message) => emit({ ok: false, reason, message });
129
- const allowed = new Set(['json', 'db', 'as', 'token']);
129
+ const allowed = new Set(['json', 'db', 'as', 'token', 'anyway']);
130
130
  for (const key of flags.keys())
131
131
  if (!allowed.has(key))
132
132
  return fail('usage', `--${key} is not a task merge option.`);
@@ -147,7 +147,7 @@ export async function runTaskMergeCommand(positional, flags, context) {
147
147
  const view = newestPullRequestOf(store, family.versions.map(one => one.id));
148
148
  if (view === null)
149
149
  return fail('no-pr', 'This task has no pull request. Open one with task complete --pull-request.');
150
- const merged = await mergeAsPerson(store, { runId: view.runId, name: acting.name, password: acting.token, ...(context.exec === undefined ? {} : { exec: context.exec }), clock: context.clock });
150
+ const merged = await mergeAsPerson(store, { runId: view.runId, name: acting.name, password: acting.token, ...(flags.get('anyway') === true ? { anyway: true } : {}), ...(context.exec === undefined ? {} : { exec: context.exec }), clock: context.clock });
151
151
  if (!merged.ok)
152
152
  return emit({ ...merged, reason: merged.reason === 'password' ? 'not-an-approver' : merged.reason, pullRequest: view });
153
153
  const after = newestPullRequestOf(store, family.versions.map(one => one.id));
@@ -11,6 +11,8 @@
11
11
  * output) is an amber note on its own row with one action; the exact
12
12
  * technical reason stays one tap away in `why`. */
13
13
  import type { AssignmentSnapshot } from "./assignment.js";
14
+ /** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
15
+ export declare const replacedWords: (successor: string) => string;
14
16
  export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
15
17
  export type Headline = (typeof HEADLINES)[number];
16
18
  export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
@@ -40,11 +42,16 @@ export type TaskStatus = {
40
42
  primaryAction: StatusAction | null;
41
43
  why: string[];
42
44
  };
43
- export type TaskStage = "queued" | "planning" | "needs-you" | "building" | "checking" | "finished" | "complete" | "failed" | "stopped";
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";
44
47
  export type ChecksFact = {
45
48
  status: "passed" | "failed" | "running" | "not-run" | "unavailable";
46
49
  exitCode: number | null;
47
50
  head: string | null;
51
+ /** The check level that ran (check-levels.ts). Off never reads Ready; a quick pass says so. */
52
+ level?: "quick" | "full" | "off" | null;
53
+ /** A follow-up check (Run checks) waiting or running on this commit. */
54
+ running?: "quick" | "full" | null;
48
55
  };
49
56
  export type PullRequestFact = {
50
57
  state: "none" | "opening" | "open" | "merged" | "closed" | "failed";
@@ -79,11 +86,12 @@ export type TaskStatusFacts = {
79
86
  } | null;
80
87
  completedBy?: string | null;
81
88
  action?: StatusAction | null;
82
- /** Where a row's one action leads: the result (and its Checks tab), the task's pull request. */
89
+ /** Where a row's one action leads: the result (and its Checks tab), the task's pull request, Run checks. */
83
90
  links?: {
84
91
  result?: string | null;
85
92
  checks?: string | null;
86
93
  pullRequest?: string | null;
94
+ runChecks?: string | null;
87
95
  };
88
96
  /** Exact technical reasons, shown only on request. */
89
97
  why?: readonly string[];
@@ -114,7 +122,9 @@ export declare function pullRequestFactOf(publication: {
114
122
  } | null): PullRequestFact | null;
115
123
  /** The assignment's stage. The work status, when the caller read one, names
116
124
  * the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
117
- export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction">, work?: {
125
+ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "state" | "primaryAction"> & {
126
+ review?: AssignmentSnapshot["review"];
127
+ }, work?: {
118
128
  token: string;
119
129
  views?: readonly string[];
120
130
  } | null, planning?: boolean): {
@@ -1,3 +1,6 @@
1
+ /** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
2
+ export const replacedWords = (successor) => `Replaced by ${successor}`;
3
+ const REPLACED = /^Replaced by \S+/;
1
4
  export const HEADLINES = ["Queued", "Planning", "Needs you", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
2
5
  export const HEADLINE_TONE = {
3
6
  Queued: "neutral", Planning: "live", "Needs you": "attention", Building: "live",
@@ -10,8 +13,15 @@ export function headlineOf(facts) {
10
13
  case "planning": return "Planning";
11
14
  case "needs-you": return "Needs you";
12
15
  case "building":
13
- case "checking": return "Building";
14
- case "finished": return !facts.report && facts.checks?.status === "failed" ? "Failed" : "Ready for review";
16
+ case "checking":
17
+ case "reviewing": return "Building";
18
+ case "finished":
19
+ if (facts.report)
20
+ return "Ready for review";
21
+ if (facts.checks?.status === "failed")
22
+ return "Failed";
23
+ // Off: ready like any build; the sentence and the Checks row say no check ran.
24
+ return "Ready for review";
15
25
  case "complete": return "Complete";
16
26
  case "failed": return "Failed";
17
27
  case "stopped": return "Stopped";
@@ -27,12 +37,15 @@ function sentenceOf(headline, facts) {
27
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."
28
38
  : facts.need === "sign-in" ? "Sign in again; the task starts on its own after."
29
39
  : "Something needs your decision before the work can continue.");
30
- case "Building": return facts.stage === "checking" ? "Checks are running on the change." : reason ?? "An agent is working on it.";
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.";
31
42
  case "Ready for review":
32
43
  if (facts.report)
33
44
  return "The report is ready to read. Read it, then mark it complete.";
45
+ if (facts.checks?.level === "off" && facts.checks.status !== "passed")
46
+ return "Checks are off for this project. Review the change, then mark it complete.";
34
47
  if (facts.checks?.status === "passed")
35
- return `Checks passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
48
+ return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
36
49
  // Never claim a check that isn't known to have passed.
37
50
  if (facts.checks == null)
38
51
  return "Review the change, then mark it complete.";
@@ -44,7 +57,7 @@ function sentenceOf(headline, facts) {
44
57
  }
45
58
  case "Failed":
46
59
  if (facts.checks?.status === "failed")
47
- return `Checks failed${sha === null ? "" : ` on ${sha}`}. See what broke, then retry or ask for changes.`;
60
+ return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. See what broke, then retry or ask for changes.`;
48
61
  return reason ?? "The last attempt stopped before it finished. Review it, then retry.";
49
62
  case "Stopped": return reason ?? "Stopped by a person. The work so far is kept.";
50
63
  }
@@ -58,14 +71,20 @@ function detailsOf(headline, facts) {
58
71
  if (checks != null && !facts.report) {
59
72
  const sha = short(checks.head);
60
73
  const checksHref = facts.links?.checks ?? facts.links?.result ?? null;
61
- if (checks.status === "passed")
62
- row("checks", "Checks", `Passed${sha === null ? "" : ` on ${sha}`}`, "ok", { href: checksHref });
74
+ const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
75
+ const quick = checks.level === "quick";
76
+ if (checks.running != null)
77
+ row("checks", "Checks", `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
78
+ else if (checks.status === "passed")
79
+ row("checks", "Checks", `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
63
80
  else if (checks.status === "failed")
64
- row("checks", "Checks", `Failed${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
81
+ row("checks", "Checks", `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
65
82
  else if (checks.status === "running")
66
83
  row("checks", "Checks", "Running", "running");
84
+ else if (checks.level === "off")
85
+ row("checks", "Checks", "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
67
86
  else if (checks.status === "not-run")
68
- row("checks", "Checks", "Didn't run", "none");
87
+ row("checks", "Checks", "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
69
88
  else
70
89
  row("checks", "Checks", "Couldn't be read", "note", { action: { label: "Open the result", href: facts.links?.result ?? null } });
71
90
  }
@@ -123,7 +142,7 @@ export function taskStatusOf(facts) {
123
142
  const CODE_STAGE = {
124
143
  queued: ["queued"], ready: ["queued"], "worker-at-capacity": ["queued"], "retry-scheduled": ["queued"], updating: ["queued"],
125
144
  "waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"], "planner-source": ["queued"],
126
- running: ["building"], reviewing: ["building"], "review-pending": ["building"],
145
+ running: ["building"], reviewing: ["reviewing"], "review-pending": ["building"],
127
146
  "signed-out": ["needs-you", "sign-in"],
128
147
  "needs-approval": ["needs-you", "approval"],
129
148
  "waiting-decision": ["needs-you", "answer"], "decision-queue": ["needs-you", "answer"],
@@ -137,7 +156,7 @@ const CODE_STAGE = {
137
156
  * building reason is scheduler wording, and the default sentence is plainer. */
138
157
  const QUEUED_REASONS = new Set(["worker-at-capacity", "waiting-dependency", "retry-scheduled", "updating", "planning-ready", "scouting-ready", "provider-quota"]);
139
158
  export function plainReasonOf(stage, code, detail) {
140
- if (stage === "building" || stage === "checking" || stage === "planning")
159
+ if (stage === "building" || stage === "checking" || stage === "reviewing" || stage === "planning")
141
160
  return null;
142
161
  if (stage === "queued")
143
162
  return QUEUED_REASONS.has(code) ? detail ?? null : null;
@@ -182,7 +201,7 @@ export function assignmentStageOf(assignment, work, planning = false) {
182
201
  case "cancelled": return { stage: "stopped" };
183
202
  case "complete": return { stage: "complete" };
184
203
  case "ready-to-check": return { stage: "finished" };
185
- case "checking": return { stage: "checking" };
204
+ case "checking": return { stage: assignment.review?.state === "pending" ? "reviewing" : "checking" };
186
205
  case "working": {
187
206
  const token = work?.token ?? "running";
188
207
  const read = stageOfCode(token, { needsPerson: work?.views?.includes("needs-you") ?? false, planning });
@@ -231,14 +250,15 @@ export function assignmentStatusFacts(assignment, options = {}) {
231
250
  const withResult = finished || stage === "failed" || (stage === "needs-you" && receipt !== null);
232
251
  const report = receipt?.completionKind === "research-report";
233
252
  const checks = receipt === null || !withResult ? null
234
- : { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head };
253
+ : { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
254
+ ...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }) };
235
255
  const publication = assignment.publication;
236
256
  const pullRequest = options.pullRequest !== undefined ? options.pullRequest
237
257
  : publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
238
258
  : pullRequestFactOf(publication);
239
259
  // Ready, Complete and Failed speak for themselves; every other stage keeps its recorded reason.
240
260
  const reason = stage === "needs-you" || stage === "stopped" || stage === "queued" || (stage === "failed" && checks?.status !== "failed")
241
- ? stage === "stopped" && assignment.state === "cancelled" ? "Cancelled. Nothing else will run." : plainReasonOf(stage, options.work?.token ?? "", options.work?.detail ?? assignment.detail)
261
+ ? stage === "stopped" && assignment.state === "cancelled" ? REPLACED.test(assignment.detail) ? assignment.detail : "Cancelled. Nothing else will run." : plainReasonOf(stage, options.work?.token ?? "", options.work?.detail ?? assignment.detail)
242
262
  : null;
243
263
  return {
244
264
  stage, ...(need === undefined ? {} : { need }), reason, report,
@@ -246,7 +266,7 @@ export function assignmentStatusFacts(assignment, options = {}) {
246
266
  pullRequest: withResult ? pullRequest : null,
247
267
  requirements: withResult ? requirementsOf(receipt?.proof?.matrix) : null,
248
268
  evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
249
- completedBy: assignment.completion === null ? null : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
269
+ completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
250
270
  action: options.action ?? null,
251
271
  ...(options.links === undefined ? {} : { links: options.links }),
252
272
  why: options.why ?? [],
@@ -307,7 +327,9 @@ export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center
307
327
  * no assignment. A done task is Complete once a person marked its result;
308
328
  * a refuted result whose project check failed is Failed. */
309
329
  export function stageOfDispatch(d, options = {}) {
310
- if (d.code === "complete" || d.code.startsWith("review-") || d.code === "reviewing")
330
+ if (d.code === "reviewing")
331
+ return { stage: "reviewing" };
332
+ if (d.code === "complete" || d.code.startsWith("review-"))
311
333
  return { stage: options.completed ? "complete" : "finished" };
312
334
  if (d.code === "proof-refuted")
313
335
  return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "other" };
package/dist/team-cli.js CHANGED
@@ -305,6 +305,9 @@ function render(reply, listing) {
305
305
  /** null means preserve the existing local command path. Nothing here opens a database. */
306
306
  export async function maybeRunTeamCommand(argv, write, options = {}) {
307
307
  const command = argv[0];
308
+ // `lead token` is local: the credential a lead agent acts with on this computer.
309
+ if (command === 'lead' && argv[1] === 'token')
310
+ return null;
308
311
  if (!['connect', 'lead', 'conversation', 'chat', 'brief'].includes(command ?? '') && !argv.includes('--profile'))
309
312
  return null;
310
313
  const json = argv.includes('--json');
@@ -65,7 +65,7 @@ function groupOf(d) {
65
65
  if (d.condition === "running")
66
66
  return "Working";
67
67
  if (d.code === "cancelled")
68
- return "Cancelled";
68
+ return d.summary.startsWith("Replaced by ") ? "Replaced" : "Cancelled";
69
69
  if (d.code === "complete")
70
70
  return "Finished";
71
71
  if (d.code === "review-pending" || d.action === null)
@@ -105,7 +105,7 @@ export function phoneStatus(store, repos, now, focused = null) {
105
105
  lines.push("Newest 60 tasks only — older work may still need attention.", "");
106
106
  if (snapshot.tasks.length === 0)
107
107
  lines.push("No tasks are recorded in these projects.");
108
- for (const group of ["Needs attention", "Working", "Waiting / next up", "Finished", "Cancelled"]) {
108
+ for (const group of ["Needs attention", "Working", "Waiting / next up", "Finished", "Replaced", "Cancelled"]) {
109
109
  const rows = snapshot.tasks.filter(t => t.dispatch !== null && t.dispatch !== undefined && groupOf(t.dispatch) === group);
110
110
  if (rows.length === 0)
111
111
  continue;
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { adjudicate } from "./proof.js";
8
8
  import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
9
9
  import { readVerifiedArtifact, storeEvidence, scanForSecrets, redactSecretLines } from "./evidence.js";
10
+ import { liveQuickCommand, quickVerifyKey, runCheckLevel } from "./check-levels.js";
10
11
  export const VERIFICATION_RECEIPT_CAPTURE = "machine verification receipt v1";
11
12
  export const REVIEW_GATE_NAME = "REVIEW-VERIFICATION.json";
12
13
  const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
@@ -36,11 +37,15 @@ export function verificationEvidence(store, root, runId) {
36
37
  if (!source)
37
38
  return fail("The verification source run is missing.");
38
39
  const repo = store.refById(source.taskRef)?.repo;
39
- const command = repo ? store.liveVerifyCommand(repo) : null;
40
40
  const artifacts = store.artifactsFor(runId);
41
41
  const receipts = artifacts.filter(isVerificationReceipt);
42
42
  const machine = store.proofVerdictFor(runId);
43
43
  const verified = (machine?.machineVerdict ?? machine?.verdict) === "verified";
44
+ // A run whose checks were Off recorded no check; that is not a gap.
45
+ if (!receipts.length && !verified && runCheckLevel(store, runId) === "off")
46
+ return { ok: true, bytes: null, digest: hash(null) };
47
+ // A Quick run is bound to the project's approved quick command, a Full one to the full command.
48
+ const command = repo ? sealedCommandKey(root, receipts) === quickVerifyKey(repo) ? liveQuickCommand(store, repo) : store.liveVerifyCommand(repo) : null;
44
49
  if (!command && !receipts.length && !verified)
45
50
  return { ok: true, bytes: null, digest: hash(null) };
46
51
  if (!command || command.approvedAt > source.startedAt)
@@ -129,6 +134,21 @@ export function verificationEvidence(store, root, runId) {
129
134
  const bytes = JSON.stringify(receipt);
130
135
  return { ok: true, bytes, digest: hash({ receipt, receipts }) };
131
136
  }
137
+ /** The grant key a run's single sealed receipt names, read without trusting it further. */
138
+ function sealedCommandKey(root, receipts) {
139
+ if (receipts.length !== 1)
140
+ return null;
141
+ const sealed = readVerifiedArtifact(root, receipts[0]);
142
+ if (!sealed.ok)
143
+ return null;
144
+ try {
145
+ const repo = JSON.parse(sealed.content.toString("utf8")).command?.repo;
146
+ return typeof repo === "string" ? repo : null;
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
132
152
  /** Only a sealed failed check can start unattended diagnosis. Lost custody,
133
153
  * changed approvals/trees and incomplete evidence still need attention; they
134
154
  * must not become another code-writing attempt. Keep the log out of the small
package/dist/webhooks.js CHANGED
@@ -267,6 +267,11 @@ export async function webhookPass(store, options) {
267
267
  return report;
268
268
  const claimed = store.claimDeliveries(owner, DELIVERY_CLAIM_MS, clock());
269
269
  for (const row of claimed) {
270
+ // The lead's own work posts to no channel; the console keeps it.
271
+ if (store.leadQuiet(row)) {
272
+ store.finalizeDelivery(row.id, owner, { ok: true, receipt: "skipped:quiet" }, clock());
273
+ continue;
274
+ }
270
275
  const link = linkFor(options.consoleUrl, row);
271
276
  const outcomes = await Promise.all(options.targets.map(target => postWebhook(target, row, link, options.fetcher ?? fetch)));
272
277
  const failed = outcomes.filter(one => !one.ok);
@@ -4,7 +4,9 @@ import { createHash } from 'node:crypto';
4
4
  import { readProjectAccess } from './project-access.js';
5
5
  import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
6
6
  import { scopeTermsProblem } from './store.js';
7
- import { plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
7
+ import { plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
8
+ import { isCheckLevel } from './check-levels.js';
9
+ import { withFollowUps } from './result-follow-ups.js';
8
10
  export const WORK_INDEX_PAGE_LIMIT = 40;
9
11
  export const WORK_INDEX_MAX_LIMIT = 100;
10
12
  const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
@@ -297,11 +299,15 @@ export function workIndexPage(store, now, access, options = {}) {
297
299
  'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
298
300
  'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
299
301
  'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
302
+ 'check_level',(SELECT outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks used' ORDER BY id DESC LIMIT 1),
303
+ 'check_follows',(SELECT json_group_array(q.outcome||' '||COALESCE((SELECT substr(f.outcome,instr(f.outcome,':')+1) FROM action_ledger f WHERE f.repo IS page.repo AND f.run_id=page_result_id
304
+ AND f.action='checks finished' AND f.outcome LIKE q.id||':%' LIMIT 1),'open')) FROM (SELECT id,outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks requested' ORDER BY id) q),
300
305
  'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
301
306
  'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'family_running',family_running,'rank',rank,'sort_at',sort_at,
302
307
  'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
303
308
  'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
304
- 'question_run',(SELECT run FROM decision WHERE id=page.question_id),
309
+ 'question_run',(SELECT run FROM decision WHERE id=page.question_id),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
310
+ 'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
305
311
  'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
306
312
  .all({ ...parameters(now, access, options), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
307
313
  const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
@@ -320,6 +326,27 @@ export function workCountsByProject(store, now, access) {
320
326
  }
321
327
  /** Labels stay navigation, never an authorization to mutate. Full task
322
328
  * opening re-proves process custody, receipt bytes and approval terms. */
329
+ /** The list's Checks: the build's own result, its level, and follow-up checks on the same commit (result-follow-ups.ts). */
330
+ function listChecksOf(own, level, follows) {
331
+ let done = [];
332
+ try {
333
+ // The row's JSON already holds the list; an older reader may hand over its text.
334
+ done = (typeof follows === 'string' ? JSON.parse(follows) : Array.isArray(follows) ? follows : []).flatMap(one => {
335
+ const [ask, said] = String(one).split(' ');
336
+ if (ask !== 'quick' && ask !== 'full')
337
+ return [];
338
+ return [{ level: ask, state: said === 'passed' || said === 'failed' ? said : said === 'open' ? 'waiting' : 'not-run', head: '', exitCode: null }];
339
+ });
340
+ }
341
+ catch {
342
+ done = [];
343
+ }
344
+ const ran = isCheckLevel(level) ? level : null;
345
+ const read = withFollowUps({ status: own ?? 'not-run', level: ran, exitCode: null, head: null }, done);
346
+ if (own === null && read.status === 'not-run' && ran !== 'off' && read.running === null)
347
+ return null;
348
+ return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
349
+ }
323
350
  function itemOf(row, principal) {
324
351
  const code = String(row['code']), id = String(row['id']);
325
352
  const need = n(row, 'needs') === 1, running = n(row, 'family_running') === 1;
@@ -368,6 +395,12 @@ function itemOf(row, principal) {
368
395
  }
369
396
  if (code === 'running' && n(row, 'version_count') > 1)
370
397
  label = 'Revising';
398
+ // Replaced, never "Cancelled": the successor is one tap away.
399
+ const successor = code === 'cancelled' ? s(row, 'replaced_by') : null;
400
+ if (successor !== null) {
401
+ detail = `${replacedWords(successor)}.`;
402
+ actionLabel = `Open ${successor}`;
403
+ }
371
404
  if (code === 'terminal-dependency' && s(row, 'dependency_id') !== null)
372
405
  detail = `${s(row, 'dependency_id')} ${s(row, 'dependency_state') === 'cancelled' ? 'was cancelled' : 'failed'} before it finished.`;
373
406
  if (code === 'result-needs-attention' && n(row, 'custody_unresolved'))
@@ -382,7 +415,7 @@ function itemOf(row, principal) {
382
415
  }
383
416
  const read = actionCode !== null && ['inspect-task', 'inspect-run', 'inspect-stop', 'inspect-decisions', 'open-result'].includes(actionCode);
384
417
  const primaryAction = actionCode === null ? null : { code: actionCode, label: actionLabel,
385
- target: { taskId: code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id,
418
+ target: { taskId: successor ?? (code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id),
386
419
  runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
387
420
  : actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null : null,
388
421
  decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
@@ -392,11 +425,11 @@ function itemOf(row, principal) {
392
425
  const reading = stageOfCode(code, { needsPerson: need, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
393
426
  const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
394
427
  const finished = reading.stage === 'finished' || reading.stage === 'complete';
428
+ const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' || verdict === 'verified' ? 'passed' : null;
395
429
  const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }),
396
430
  reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
397
- checks: !finished ? null : checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? { status: 'failed', exitCode: null, head: null }
398
- : checkStatus === 'passed' || verdict === 'verified' ? { status: 'passed', exitCode: null, head: null } : null,
399
- completedBy: code === 'complete' ? String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, '') : null });
431
+ checks: !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']),
432
+ completedBy: code !== 'complete' ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, '') });
400
433
  label = shared.headline;
401
434
  detail = shared.sentence;
402
435
  const views = ['all'];
@@ -1,7 +1,3 @@
1
- /** Read-only work context shared by console, CLI, and agent adapters.
2
- * State, decisions, approval, process custody and proof still belong to
3
- * their existing owners. Action hints are navigation/proposal guidance,
4
- * never credentials or permission to bypass an owning operation. */
5
1
  import { type DispatchAction } from "./dispatch.js";
6
2
  import type { Store } from "./store.js";
7
3
  import { type DisplayStatus, type WorkFacts, type WorkStatus } from "./workspace-ui.js";
@@ -2,9 +2,11 @@
2
2
  * State, decisions, approval, process custody and proof still belong to
3
3
  * their existing owners. Action hints are navigation/proposal guidance,
4
4
  * never credentials or permission to bypass an owning operation. */
5
+ import { resultCheckLevel } from "./result-follow-ups.js";
5
6
  import { diagnoseTaskDispatch } from "./dispatch.js";
6
7
  import { isAlive } from "./runner.js";
7
8
  import { taskControlOf } from "./task-control.js";
9
+ import { buildReviewOf } from "./review-switch.js";
8
10
  import { reviewFactsOf, workStatusOf } from "./workspace-ui.js";
9
11
  /** Call only after admitting the task's project. Keep the question's
10
12
  * exact run and id; never substitute a family's newest attempt. Expiry
@@ -98,10 +100,11 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
98
100
  result: result === null ? null : {
99
101
  runId: result.id, role: result.role, outcome: result.outcome,
100
102
  verdict: proof?.verdict ?? null, reasons: proof?.reasons ?? [], accepted: store.proofAcceptance(result.id) !== null,
103
+ checkLevel: resultCheckLevel(store, result.id, now),
101
104
  review: reviewFactsOf(store.reviewRetryStateOf(result.id), name => {
102
105
  const runner = store.getRunner(name)?.runner;
103
106
  return runner !== undefined && isAlive(runner, now);
104
- }),
107
+ }, buildReviewOf(store, result.id)?.state === "pending"),
105
108
  ...(result.outcome !== "no-change" ? {} : { recordComplete: (() => {
106
109
  const kinds = new Set(store.artifactsFor(result.id).map(one => one.kind));
107
110
  return kinds.has("handoff") && kinds.has("terminal-diff");