toolroll 0.9.5 → 0.9.6

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.
@@ -1,12 +1,15 @@
1
1
  /** A compact projection of saved work, never a percentage or an ETA. The
2
- * words come from the same assignment record the console and the CLI show:
3
- * a finished build is Ready once its saved result can be inspected, and
4
- * Complete once the lead or a person has marked that exact result. No model
5
- * reviewer is waited for or announced. */
6
- import { manualReviewOnly } from "./proof.js";
2
+ * headline, sentence and detail rows come from the shared task status
3
+ * (task-status.ts), read from the same assignment record the console and the
4
+ * CLI show: Ready for review once the saved result can be inspected, Complete
5
+ * once the lead or a person has marked that exact result. Slack, Discord and
6
+ * Teams send this same card. */
7
7
  import { assignmentOf } from "./assignment.js";
8
8
  import { chatResultHref, chatControlHref } from "./chat-controls.js";
9
9
  import { phoneText, projectLabel } from "./telegram-status.js";
10
+ import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, taskStatusOf } from "./task-status.js";
11
+ import { manualReviewOnly } from "./proof.js";
12
+ import { failedCheckExit } from "./workspace-ui.js";
10
13
  /** The saved assignment for this exact result, or null when the run is not
11
14
  * the family's current result (an older attempt keeps its own plain words). */
12
15
  function assignmentFor(store, run, taskId, project, now, root) {
@@ -19,111 +22,70 @@ function assignmentFor(store, run, taskId, project, now, root) {
19
22
  }
20
23
  }
21
24
  export function telegramProgressCard(store, run, taskId, project, now = new Date(), root) {
22
- const proof = store.proofVerdictFor(run.id);
23
- const rows = proof?.matrix ?? [];
24
- const direct = rows.length > 0 && rows.every(row => row.assessment !== undefined);
25
- const passed = rows.filter(row => row.state === "pass").length;
26
- const human = manualReviewOnly(proof);
27
- const accepted = store.proofAcceptance(run.id) !== null;
28
25
  const built = run.finishedAt !== null && (run.outcome === "built" || run.outcome === "no-change");
29
26
  const assignment = built ? assignmentFor(store, run, taskId, project, now, root) : null;
30
- const checksFailed = assignment?.receipt?.checks.status === "failed";
31
- let status = "Working";
32
- let icon = "⏳";
33
- let next = "";
34
- let build = built ? "✓ Build saved" : "● Build in progress";
35
- let checks = rows.length ? `${passed === rows.length ? "✓" : "○"} Checks · ${passed}/${rows.length} requirements` : "○ Checks pending";
36
- if (direct)
37
- checks = proof?.machineVerdict === "verified" ? "✓ Checks passed" : proof?.machineVerdict === "attested" ? "○ Checks not configured" : "! Checks need attention";
38
- let label = built ? "Open result" : "Open task";
39
- if (run.outcome === null && run.phase !== null && run.phase !== "agent-running") {
40
- status = "Checking the result";
41
- build = "✓ Draft prepared";
42
- checks = "● Checks in progress";
43
- }
44
- if (built) {
45
- status = run.committed === false ? "Finished without a commit" : "Result saved";
46
- if (proof === null)
47
- checks = "○ Checks not recorded";
48
- const recorded = assignment?.receipt?.checks;
49
- if (recorded !== undefined && recorded.status !== "unavailable") {
50
- checks = recorded.status === "passed" ? "✓ Checks passed"
51
- : recorded.status === "failed" ? `! Checks failed${recorded.exitCode === null ? "" : ` (exit ${recorded.exitCode})`}`
52
- : "○ Checks did not run";
53
- }
54
- if (assignment?.state === "ready-to-check") {
55
- status = "Ready";
56
- icon = checksFailed ? "⚠️" : "✅";
57
- next = checksFailed ? "Next: inspect the failed check, then mark it complete or request changes." : "Next: open the result, then mark it complete or request changes.";
58
- }
59
- else if (assignment?.state === "complete") {
60
- status = "Complete";
61
- icon = "✅";
62
- next = phoneText(assignment.detail, 200);
63
- }
64
- else if (assignment?.state === "needs-decision") {
65
- status = "Needs your decision";
66
- icon = "💬";
67
- next = phoneText(assignment.detail, 200);
68
- }
69
- else if (assignment?.state === "cancelled") {
70
- status = "Cancelled";
71
- icon = "⏹";
72
- }
73
- else if (assignment !== null) {
74
- next = phoneText(assignment.detail, 200);
75
- }
76
- else if (run.committed !== false) {
77
- next = "Open the task for its current status.";
78
- }
79
- }
80
- else if (run.outcome !== null) {
81
- status = run.outcome === "parked" ? "Decision needed" : run.outcome === "interrupted" || run.reason === "interrupted" ? "Stopped" : "Build needs attention";
82
- icon = run.outcome === "parked" ? "💬" : "⏸";
83
- build = "! Build unfinished";
84
- next = "Next: inspect the blocker.";
85
- label = "Open task";
86
- }
27
+ const publication = store.publicationForRun(run.id);
28
+ const pullRequest = publication === null ? undefined : pullRequestFactOf(publication);
87
29
  const stop = store.stopOf(run.id);
88
- if (run.outcome === null && stop !== null && stop.resumedAt === null) {
89
- status = stop.settledAt === null ? "Stopping" : "Stopped";
90
- icon = "⏸";
91
- build = stop.settledAt === null ? "● Build stopping" : "! Build stopped";
92
- next = stop.settledAt === null ? "Waiting for the running process to stop." : "Next: inspect the saved work, then resume.";
93
- }
94
30
  const holds = store.activeHolds(run.taskRef, now);
95
31
  const operatorHold = holds.find(hold => hold.ownerKind === "operator");
96
32
  const accessBlocked = run.reason === "retryable-infra" && /^could not re-read (?:the branch|HEAD) in /i.test(run.handoff ?? "");
97
- if (accessBlocked) {
98
- status = "Worker needs project access";
99
- icon = "⏸";
100
- next = "Next: restore the worker’s folder access, then resume.";
101
- label = "Open task";
33
+ // The one shared status: the same headline, sentence and rows as the console.
34
+ let facts;
35
+ if (assignment !== null && assignment.state !== "working" && assignment.state !== "checking") {
36
+ facts = assignmentStatusFacts(assignment, pullRequest === undefined ? {} : { pullRequest });
102
37
  }
103
- if (operatorHold) {
104
- icon = "⏸";
105
- if (!accessBlocked) {
106
- status = run.outcome === null ? "Working · Next attempt paused" : "Paused";
107
- next = phoneText(operatorHold.reason, 150);
108
- }
109
- label = "Open task";
38
+ else if (built) {
39
+ // A saved result is Ready even in the moment before its worker lets go of
40
+ // the task. Without its receipt yet, the stored machine verdict speaks.
41
+ const recorded = assignment?.receipt?.checks ?? null;
42
+ const proof = store.proofVerdictFor(run.id);
43
+ const machine = proof?.machineVerdict ?? proof?.verdict ?? null;
44
+ const exit = failedCheckExit(proof?.reasons ?? []);
45
+ const checks = recorded !== null ? { status: recorded.status, exitCode: recorded.exitCode, head: run.headRevision }
46
+ : exit !== null ? { status: "failed", exitCode: exit, head: run.headRevision }
47
+ : machine === "verified" ? { status: "passed", exitCode: null, head: run.headRevision }
48
+ : machine === "attested" ? { status: "not-run", exitCode: null, head: null } : null;
49
+ facts = { stage: "finished", report: run.role === "scout", checks, requirements: requirementsOf(proof?.matrix),
50
+ ...(pullRequest === undefined ? {} : { pullRequest }) };
110
51
  }
111
- else if (accessBlocked && holds.some(hold => hold.ownerKind === "backoff")) {
112
- next = "The worker will retry after a short pause. Check its folder access.";
52
+ else if (accessBlocked) {
53
+ facts = { stage: "needs-you", need: "other", reason: holds.some(hold => hold.ownerKind === "backoff")
54
+ ? "The worker can't read the project folder. It retries after a short pause; check its folder access."
55
+ : "The worker can't read the project folder. Restore its access, then resume." };
113
56
  }
57
+ else if (run.outcome === null && stop !== null && stop.resumedAt === null) {
58
+ facts = { stage: "stopped", reason: stop.settledAt === null ? "Stopping. Waiting for the running process to end." : "Paused by a person. Inspect the saved work, then resume." };
59
+ }
60
+ else if (operatorHold) {
61
+ facts = { stage: "stopped", reason: `On hold: ${phoneText(operatorHold.reason, 150)}` };
62
+ }
63
+ else if (run.outcome === null) {
64
+ facts = { stage: run.phase !== null && run.phase !== "agent-running" ? "checking" : run.role === "planner" ? "planning" : "building" };
65
+ }
66
+ else if (run.outcome === "parked") {
67
+ facts = { stage: "needs-you", need: "answer" };
68
+ }
69
+ else if (run.outcome === "interrupted" || run.reason === "interrupted") {
70
+ facts = { stage: "stopped", reason: "The attempt was interrupted. Inspect the saved work, then resume." };
71
+ }
72
+ else {
73
+ facts = { stage: "failed", reason: "The build stopped before it finished. Inspect the blocker, then retry." };
74
+ }
75
+ const status = taskStatusOf(facts);
114
76
  const checkProgress = store.checkProgress(run.id);
115
- if (checkProgress !== null)
116
- checks = checkProgress.line;
117
- const publication = store.publicationForRun(run.id);
118
- const delivery = publication?.remoteState === "MERGED" ? "Merged · Installation not confirmed" : publication?.remoteState === "CLOSED" ? "Pull request closed without merging" : publication?.state === "opened" ? "Pull request open · Not merged" : publication?.state === "pushed" ? "Branch pushed · Pull request pending" : publication?.state === "intended" ? "Publication queued" : publication?.state === "failed" ? "Publication failed · Local result preserved" : built ? "Saved locally · Not published" : null;
119
- const acceptance = accepted ? (human ? "Accepted by a person · Recorded checks unchanged" : "Accepted with an exception · Recorded checks unchanged") : null;
77
+ const progress = status.headline === "Building" && checkProgress !== null ? [`● ${checkProgress.line}`] : [];
120
78
  const title = phoneText(store.getTask(taskId)?.title ?? taskId, 88);
121
- const heading = `${icon} ${status}`;
122
- const text = [title, heading, "", build, checks,
123
- ...(next ? ["", next] : []),
124
- ...(acceptance === null ? [] : [acceptance]),
125
- ...(delivery === null ? [] : ["", delivery]), "", `${projectLabel(project)} · #${run.id}`].join("\n");
79
+ const heading = `${headlineEmoji(status.headline)} ${status.headline}`;
80
+ const next = phoneText(status.sentence, 200);
81
+ // A person's acceptance is its own recorded decision; it never changes a check.
82
+ const acceptance = store.proofAcceptance(run.id) === null ? [] : [manualReviewOnly(store.proofVerdictFor(run.id))
83
+ ? "Accepted by a person · Recorded checks unchanged" : "Accepted with an exception · Recorded checks unchanged"];
84
+ const rows = [...statusDetailLines(status), ...progress, ...acceptance];
85
+ const text = [title, heading, next, ...(rows.length === 0 ? [] : ["", ...rows]), "", `${projectLabel(project)} · #${run.id}`].join("\n");
86
+ const failedChecks = status.details.some(one => one.key === "checks" && (one.mark === "failed" || one.mark === "note"));
87
+ const recovery = operatorHold !== undefined || accessBlocked || (!built && run.outcome !== null);
126
88
  return { text, next, entities: [{ type: "bold", offset: 0, length: title.length }, { type: "bold", offset: title.length + 1, length: heading.length }],
127
- link: { label, path: operatorHold || accessBlocked || (!built && run.outcome !== null)
128
- ? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id, checksFailed ? "checks" : "summary") : chatControlHref("task", taskId) } };
89
+ link: { label: built ? "Open result" : "Open task", path: recovery
90
+ ? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id, failedChecks ? "checks" : "summary") : chatControlHref("task", taskId) } };
129
91
  }
@@ -3,6 +3,8 @@ import { manualReviewOnly } from "./proof.js";
3
3
  /** Read-only phone views over the same dispatch and proof records as chat.
4
4
  * No provider calls, repository access, new workflow state, or inferred success. */
5
5
  import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
6
+ import { COMPLETION_ACTION } from "./result-completion.js";
7
+ import { plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
6
8
  import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
7
9
  export function phoneCommand(text) {
8
10
  // Slash forms everywhere; the bare words too, because Slack and Discord
@@ -50,6 +52,15 @@ export const phoneText = plain;
50
52
  export function projectLabel(repo) {
51
53
  return plain(repo.split(/[\\/]/).filter(Boolean).pop() ?? "project", 48);
52
54
  }
55
+ /** The shared headline (task-status.ts) for a dispatch diagnosis on the phone. */
56
+ function headlineFor(store, id, d) {
57
+ const completed = d.code === "complete" && store.handle.prepare("SELECT 1 FROM action_ledger WHERE task_id = ? AND action = ? AND source = 'work' LIMIT 1")
58
+ .get(store.taskFamilyOf(id, null, true)?.root.id ?? id, COMPLETION_ACTION) !== undefined;
59
+ const read = stageOfDispatch(d, { completed });
60
+ const finished = read.stage === "finished" || read.stage === "complete" || read.stage === "failed";
61
+ return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }), reason: finished ? null : plainReasonOf(read.stage, d.code, d.detail),
62
+ checks: read.stage === "failed" && d.code === "proof-refuted" ? { status: "failed", exitCode: null, head: null } : null });
63
+ }
53
64
  function groupOf(d) {
54
65
  if (d.condition === "running")
55
66
  return "Working";
@@ -100,7 +111,7 @@ export function phoneStatus(store, repos, now, focused = null) {
100
111
  continue;
101
112
  lines.push(`${group} · ${rows.length}${snapshot.tasksSaturated ? " in this snapshot" : ""}`);
102
113
  for (const task of rows.slice(0, 2)) {
103
- lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${plain(task.dispatch.summary, 80)}`);
114
+ lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch).headline}`);
104
115
  }
105
116
  if (rows.length > 2)
106
117
  lines.push(` +${rows.length - 2} more in the console`);
@@ -131,7 +142,7 @@ export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
131
142
  if (seen.has(id) || !words.every(word => `${id} ${row.title}`.toLowerCase().includes(word)))
132
143
  continue;
133
144
  seen.add(id);
134
- choices.push({ id, title: plain(row.title, 64), label: plain(row.dispatch.summary, 48), group: groupOf(row.dispatch) });
145
+ choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch).headline, group: groupOf(row.dispatch) });
135
146
  if (choices.length >= limit)
136
147
  break;
137
148
  }
@@ -214,10 +225,11 @@ export function phoneTaskView(store, repos, id, now) {
214
225
  const d = diagnoseTaskDispatch(store, id, now);
215
226
  if (d === null)
216
227
  return { text: "This task's status is unavailable. Open it in the console before retrying.", link: null, run: null };
217
- const lines = [plain(task.title, 140), `${plain(id, 64)} · ${projectLabel(ref.repo)}`, `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC`, "", plain(d.summary, 160)];
228
+ const shared = headlineFor(store, id, d);
229
+ const lines = [plain(task.title, 140), `${plain(id, 64)} · ${projectLabel(ref.repo)}`, `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC`, "", shared.headline];
218
230
  const blocker = d.blockerTaskId === null ? null : store.lookupRef(d.blockerTaskId);
219
231
  const hiddenDependency = blocker !== null && (blocker.repo === null || !repos.includes(blocker.repo));
220
- lines.push(hiddenDependency ? "A required task outside this phone view has not finished. Open the dependency in the console." : plain(d.detail, 650));
232
+ lines.push(hiddenDependency ? "A required task outside this phone view has not finished. Open the dependency in the console." : plain(shared.sentence, 650));
221
233
  if (d.nextAt !== null)
222
234
  lines.push(`Earliest recorded wake: ${plain(d.nextAt, 40)} (a connected worker is still required).`);
223
235
  const runs = store.runsFor(ref.id);
@@ -247,8 +259,9 @@ export function phoneTaskView(store, repos, id, now) {
247
259
  if (accepted && !manualReviewOnly(proof))
248
260
  lines.push("An operator accepted this result; that does not change its recorded checks.");
249
261
  const publication = store.publicationForRun(result.id);
250
- const delivery = publication?.remoteState === "MERGED" ? "Merge observed on GitHub" : publication?.remoteState === "CLOSED" ? "Pull request closed, not merged" : publication?.state === "opened" ? `Pull request #${publication.prNumber ?? "?"} opened; not recorded as merged` : publication?.state === "pushed" ? "Branch pushed; pull request not yet recorded" : publication?.state === "intended" ? "Publication queued; not yet confirmed" : publication?.state === "failed" ? "Publication failed; the local result is preserved" : result.role === "scout" ? "Report saved locally" : "Result saved locally; no publication recorded";
251
- lines.push(`Delivery: ${delivery}.`);
262
+ const delivery = publication?.remoteState === "MERGED" ? `#${publication.prNumber ?? "?"} merged` : publication?.remoteState === "CLOSED" ? `#${publication.prNumber ?? "?"} closed without merging` : publication?.state === "opened" ? `#${publication.prNumber ?? "?"} open` : publication?.state === "pushed" || publication?.state === "intended" ? "Opening…" : publication?.state === "failed" ? "Couldn't open; the commit is safe locally" : null;
263
+ if (delivery !== null)
264
+ lines.push(`Pull request: ${delivery}.`);
252
265
  }
253
266
  if (result === undefined)
254
267
  link = taskLinkFor(id, d, null);
@@ -4,6 +4,7 @@ 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
8
  export const WORK_INDEX_PAGE_LIMIT = 40;
8
9
  export const WORK_INDEX_MAX_LIMIT = 100;
9
10
  const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
@@ -194,9 +195,9 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
194
195
  ELSE 'queued' END code
195
196
  FROM checked f
196
197
  ), ranked AS MATERIALIZED (
197
- SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','signed-out','planning-ready','scouting-ready','queued') needs,
198
+ SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') needs,
198
199
  CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
199
- WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','signed-out','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
200
+ WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
200
201
  CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
201
202
  completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
202
203
  FROM classified c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
@@ -293,7 +294,11 @@ export function workIndexPage(store, now, access, options = {}) {
293
294
  UNION ALL SELECT 1,0,0,0,0,json_object('root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
294
295
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
295
296
  'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),
296
- 'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),'code',code,'needs',needs,'family_running',family_running,'rank',rank,'sort_at',sort_at,
297
+ 'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
298
+ 'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
299
+ 'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
300
+ 'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
301
+ '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,
297
302
  'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
298
303
  '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,
299
304
  'question_run',(SELECT run FROM decision WHERE id=page.question_id),
@@ -326,15 +331,15 @@ function itemOf(row, principal) {
326
331
  'ready-to-check': ['Ready', 'The saved result is ready to review.', 'open-result', 'Open result'],
327
332
  complete: ['Complete', 'Completion is recorded for this result. Open it to inspect the saved evidence.', 'open-result', 'Open result'],
328
333
  cancelled: ['Cancelled', 'This assignment was cancelled.', 'inspect-task', 'View assignment'],
329
- running: ['Running now', 'A worker owns a current live claim.', null, ''],
334
+ running: ['Running now', '', null, ''],
330
335
  'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : 'inspect-decisions', 'Answer question'],
331
336
  'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
332
337
  'result-needs-attention': ['Needs your decision', 'Inspect the saved result and resolve its remaining execution or scope issue.', 'inspect-run', 'Inspect run'],
333
338
  'process-needs-attention': ['Needs your decision', 'A process exit is not recorded. Open the attempt to check whether its work has stopped.', 'inspect-run', 'Inspect run'],
334
339
  failed: ['Needs your decision', 'The last attempt stopped; review its incident before retrying.', 'retry-task', 'Review and retry'],
335
340
  'vanished-run': ['Needs your decision', 'An unfinished attempt has no current live claim.', 'reconcile-run', 'Check the unfinished attempt'],
336
- stopping: ['Needs your decision', 'The stop is waiting for recorded settlement.', 'inspect-stop', 'View stop details'],
337
- stopped: ['Needs your decision', 'The stopped attempt is preserved.', 'resume-run', 'Review pause'],
341
+ stopping: ['Needs your decision', 'Stopping. The work so far is kept.', 'inspect-stop', 'View stop details'],
342
+ stopped: ['Needs your decision', 'Paused by a person. The work so far is kept.', 'resume-run', 'Review pause'],
338
343
  held: ['Needs your decision', s(row, 'hold_reason') ?? 'This task is on hold.', s(row, 'hold_kind') === 'operator' ? 'unhold' : 'inspect-hold', 'Review hold'],
339
344
  'waiting-incident': ['Needs your decision', 'An unresolved incident holds the next attempt.', 'retry-task', 'Review and retry'],
340
345
  'terminal-dependency': ['Needs your decision', 'A required task did not finish.', 'repair-dependency', 'Review required task'],
@@ -353,13 +358,13 @@ function itemOf(row, principal) {
353
358
  updating: ['Waiting for app update', 'New work resumes after the update.', null, ''],
354
359
  'planning-ready': ['Planner ready', 'A connected worker can draft the plan.', null, ''],
355
360
  'scouting-ready': ['Scout ready', 'A connected worker can produce the report.', null, ''],
356
- queued: ['Ready to run', 'A connected worker can claim this task.', null, ''],
361
+ queued: ['Queued', '', null, ''],
357
362
  };
358
363
  let [label, detail, actionCode, actionLabel] = words[code];
359
364
  if (code === 'signed-out') {
360
365
  const [provider = '', authMode = 'subscription'] = (s(row, 'signed_out') ?? '').split(':');
361
366
  label = signInReason({ provider: provider, authMode: authMode === 'api-key' ? 'api-key' : 'subscription' });
362
- detail = `It starts again on its own once ${providerName(provider)} works.`;
367
+ detail = `${label}. It starts again on its own once ${providerName(provider)} works.`;
363
368
  }
364
369
  if (code === 'running' && n(row, 'version_count') > 1)
365
370
  label = 'Revising';
@@ -383,6 +388,17 @@ function itemOf(row, principal) {
383
388
  decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
384
389
  access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
385
390
  retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
391
+ // The one shared status: the headline and sentence every surface uses.
392
+ const reading = stageOfCode(code, { needsPerson: need, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
393
+ const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
394
+ const finished = reading.stage === 'finished' || reading.stage === 'complete';
395
+ const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }),
396
+ 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 });
400
+ label = shared.headline;
401
+ detail = shared.sentence;
386
402
  const views = ['all'];
387
403
  if (need)
388
404
  views.push('needs-you');
@@ -395,7 +411,7 @@ function itemOf(row, principal) {
395
411
  versionCount: n(row, 'version_count'), earlierActiveCount: n(row, 'earlier_active'), familyProblem: n(row, 'broken') ? FAMILY_PROBLEM : null,
396
412
  liveRunId: n(row, 'live_run') || null, unfinishedRunId: n(row, 'unfinished') || null, resultRunId: n(row, 'result_id') || null, resultTaskId: n(row, 'result_id') ? id : null,
397
413
  resultOutcome: s(row, 'result_outcome'), publicationUrl: s(row, 'publication_url'),
398
- status: { token: `assignment-${assignmentState}`, label, detail, tone: code === 'complete' ? 'done' : code === 'ready-to-check' ? 'ready' : need ? 'attention' : running ? 'live' : 'muted',
414
+ status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
399
415
  action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
400
416
  primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
401
417
  evidence: 'recorded' };
@@ -182,6 +182,13 @@ export type WorkStatus = DisplayStatus & {
182
182
  * concrete act — never every queued task indiscriminately. */
183
183
  export declare function needsPerson(dispatch: DispatchDiagnosis | null): boolean;
184
184
  export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null): string;
185
+ /** A finished result's display status in the shared headline's words: a
186
+ * failed project check is Failed, a result missing its record needs a
187
+ * person, every other saved result is Ready for review. Token and action stay. */
188
+ export declare function resultHeadlineOf<T extends DisplayStatus>(status: T): T;
189
+ /** The one shared headline (task-status.ts) over the work status below: the
190
+ * token, views, rank, action and diagnostics stay; the words and tone are
191
+ * the eight headlines'. */
185
192
  export declare function workStatusOf(facts: WorkFacts, resultDisplay?: DisplayStatus): WorkStatus;
186
193
  /** The counts each view tab wears — from the same rows the page lists. */
187
194
  export declare function workCounts(rows: readonly {
@@ -17,10 +17,11 @@
17
17
  * claimed without one.
18
18
  */
19
19
  import { GOAL_ASSESSMENT_PENDING, manualReviewOnly } from "./proof.js";
20
+ import { plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
20
21
  export const WORK_VIEWS = [
21
22
  { key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
22
23
  { key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
23
- { key: "running", label: "Running", hint: "Attempts a builder owns right now.", empty: "Nothing is building right now. Approved tasks start when a builder with capacity is connected." },
24
+ { key: "running", label: "Building", hint: "Attempts a builder owns right now.", empty: "Nothing is building right now. Approved tasks start when a builder with capacity is connected." },
24
25
  { key: "completed", label: "Complete", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "No tasks have been marked complete in this view." },
25
26
  ];
26
27
  export function parseWorkView(raw) {
@@ -302,7 +303,35 @@ const DISPATCH_ACTION_LABELS = {
302
303
  export function dispatchActionLabel(dispatch) {
303
304
  return dispatch?.action == null ? "View task details" : DISPATCH_ACTION_LABELS[dispatch.action];
304
305
  }
306
+ /** Result tokens a done task can wear, as the shared stage they mean. A
307
+ * result that is missing its record waits on a person; a failed check fails. */
308
+ const RESULT_NEEDS_YOU = new Set(["no-build-record", "verification-needed", "evidence-mismatch", "record-incomplete", "evidence-damaged"]);
309
+ /** Result tokens that only follow a passing project check. */
310
+ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed", "pr-closed"]);
311
+ /** A finished result's display status in the shared headline's words: a
312
+ * failed project check is Failed, a result missing its record needs a
313
+ * person, every other saved result is Ready for review. Token and action stay. */
314
+ export function resultHeadlineOf(status) {
315
+ const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
316
+ : status.token === "checks-failed" ? { stage: "failed" } : { stage: "finished" };
317
+ const shared = taskStatusOf({ ...read, reason: status.detail, checks: read.stage === "failed" ? { status: "failed", exitCode: null, head: null }
318
+ : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null } : null });
319
+ return { ...status, label: shared.headline, tone: workToneOf(shared.headline) };
320
+ }
321
+ /** The one shared headline (task-status.ts) over the work status below: the
322
+ * token, views, rank, action and diagnostics stay; the words and tone are
323
+ * the eight headlines'. */
305
324
  export function workStatusOf(facts, resultDisplay) {
325
+ const status = workStatusWordsOf(facts, resultDisplay);
326
+ const done = facts.state === "done" && status.token !== "waiting-decision";
327
+ const read = done ? RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" } : stageOfCode(status.token, { needsPerson: false })
328
+ : stageOfCode(status.token, { needsPerson: status.views.includes("needs-you"), planning: facts.dispatch?.code === "running" && facts.dispatch.role === "planner", operatorHold: facts.dispatch?.action === "unhold" });
329
+ const stage = done && read.stage !== "needs-you" && read.stage !== "failed" ? "finished" : read.stage;
330
+ const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), reason: plainReasonOf(stage, status.token, status.detail),
331
+ report: facts.result?.role === "scout", checks: stage === "finished" && VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null } : null });
332
+ return { ...status, label: shared.headline, tone: workToneOf(shared.headline), detail: stage === "finished" ? status.detail : shared.sentence };
333
+ }
334
+ function workStatusWordsOf(facts, resultDisplay) {
306
335
  const dispatch = facts.dispatch;
307
336
  const result = facts.state === "done" ? resultDisplay ?? resultStatusOf(facts.result, facts.publication) : null;
308
337
  const running = facts.liveRunId !== null || (dispatch?.condition === "running" && !REVIEW_TOKENS.has(dispatch.code));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
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"