toolroll 0.9.5 → 0.9.7

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 (92) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/actor.d.ts +21 -0
  3. package/dist/actor.js +48 -0
  4. package/dist/assignment-adapters.d.ts +1 -0
  5. package/dist/assignment-presentation.d.ts +13 -2
  6. package/dist/assignment-presentation.js +22 -9
  7. package/dist/assignment-ui.d.ts +10 -15
  8. package/dist/assignment-ui.js +19 -18
  9. package/dist/assignment.d.ts +9 -0
  10. package/dist/assignment.js +62 -13
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +29 -24
  13. package/dist/browser-workspace.d.ts +120 -12
  14. package/dist/builder.d.ts +1 -0
  15. package/dist/builder.js +33 -13
  16. package/dist/chat-acceptance.js +2 -2
  17. package/dist/chat-actions.js +2 -1
  18. package/dist/chat-delivery.js +2 -0
  19. package/dist/chat-quiet.js +30 -19
  20. package/dist/chat-task-actions.d.ts +13 -0
  21. package/dist/chat-task-actions.js +19 -1
  22. package/dist/check-levels-ui.d.ts +21 -0
  23. package/dist/check-levels-ui.js +70 -0
  24. package/dist/check-levels.d.ts +67 -0
  25. package/dist/check-levels.js +171 -0
  26. package/dist/claim.d.ts +22 -0
  27. package/dist/claim.js +43 -0
  28. package/dist/cli.d.ts +1 -1
  29. package/dist/cli.js +4 -0
  30. package/dist/demo-chat.d.ts +0 -5
  31. package/dist/demo-chat.js +14 -3
  32. package/dist/dispatch.js +7 -2
  33. package/dist/dispose.d.ts +8 -0
  34. package/dist/dispose.js +7 -1
  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/lead-context.js +6 -2
  54. package/dist/lead-status.d.ts +7 -0
  55. package/dist/lead-status.js +11 -2
  56. package/dist/mate-doors.d.ts +0 -15
  57. package/dist/mate-doors.js +5 -1
  58. package/dist/mate-tools.js +9 -4
  59. package/dist/models-ui.js +1 -1
  60. package/dist/operate.d.ts +4 -1
  61. package/dist/operate.js +601 -85
  62. package/dist/project-concurrency.d.ts +62 -0
  63. package/dist/project-concurrency.js +116 -0
  64. package/dist/project-delete-ui.d.ts +7 -0
  65. package/dist/project-delete-ui.js +19 -2
  66. package/dist/project-delete.js +4 -0
  67. package/dist/pull-request-flow.d.ts +9 -0
  68. package/dist/pull-request-flow.js +21 -3
  69. package/dist/result-follow-ups.d.ts +101 -0
  70. package/dist/result-follow-ups.js +333 -0
  71. package/dist/scope.js +11 -0
  72. package/dist/serve.d.ts +211 -2
  73. package/dist/serve.js +1070 -204
  74. package/dist/store.d.ts +76 -3
  75. package/dist/store.js +311 -22
  76. package/dist/surface.js +31 -8
  77. package/dist/tailnet.d.ts +11 -0
  78. package/dist/tailnet.js +47 -0
  79. package/dist/task-outcome-cli.js +2 -2
  80. package/dist/task-status.d.ts +186 -0
  81. package/dist/task-status.js +335 -0
  82. package/dist/team-cli.js +3 -0
  83. package/dist/telegram-progress.js +62 -100
  84. package/dist/telegram-status.js +21 -8
  85. package/dist/verification-evidence.js +21 -1
  86. package/dist/webhooks.js +5 -0
  87. package/dist/work-index.js +60 -11
  88. package/dist/work-summary.d.ts +0 -4
  89. package/dist/work-summary.js +2 -0
  90. package/dist/workspace-ui.d.ts +10 -0
  91. package/dist/workspace-ui.js +33 -2
  92. package/package.json +1 -1
@@ -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,11 +52,20 @@ 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";
56
67
  if (d.code === "cancelled")
57
- return "Cancelled";
68
+ return d.summary.startsWith("Replaced by ") ? "Replaced" : "Cancelled";
58
69
  if (d.code === "complete")
59
70
  return "Finished";
60
71
  if (d.code === "review-pending" || d.action === null)
@@ -94,13 +105,13 @@ export function phoneStatus(store, repos, now, focused = null) {
94
105
  lines.push("Newest 60 tasks only — older work may still need attention.", "");
95
106
  if (snapshot.tasks.length === 0)
96
107
  lines.push("No tasks are recorded in these projects.");
97
- 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"]) {
98
109
  const rows = snapshot.tasks.filter(t => t.dispatch !== null && t.dispatch !== undefined && groupOf(t.dispatch) === group);
99
110
  if (rows.length === 0)
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);
@@ -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,6 +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, replacedWords, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
8
+ import { isCheckLevel } from './check-levels.js';
9
+ import { withFollowUps } from './result-follow-ups.js';
7
10
  export const WORK_INDEX_PAGE_LIMIT = 40;
8
11
  export const WORK_INDEX_MAX_LIMIT = 100;
9
12
  const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
@@ -194,9 +197,9 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
194
197
  ELSE 'queued' END code
195
198
  FROM checked f
196
199
  ), 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,
200
+ SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') needs,
198
201
  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,
202
+ WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
200
203
  CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
201
204
  completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
202
205
  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,10 +296,18 @@ export function workIndexPage(store, now, access, options = {}) {
293
296
  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
297
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
295
298
  '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,
299
+ 'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
300
+ 'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
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),
305
+ 'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
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,
297
307
  'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
298
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,
299
- '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),
300
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`)
301
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() });
302
313
  const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
@@ -315,6 +326,27 @@ export function workCountsByProject(store, now, access) {
315
326
  }
316
327
  /** Labels stay navigation, never an authorization to mutate. Full task
317
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
+ }
318
350
  function itemOf(row, principal) {
319
351
  const code = String(row['code']), id = String(row['id']);
320
352
  const need = n(row, 'needs') === 1, running = n(row, 'family_running') === 1;
@@ -326,15 +358,15 @@ function itemOf(row, principal) {
326
358
  'ready-to-check': ['Ready', 'The saved result is ready to review.', 'open-result', 'Open result'],
327
359
  complete: ['Complete', 'Completion is recorded for this result. Open it to inspect the saved evidence.', 'open-result', 'Open result'],
328
360
  cancelled: ['Cancelled', 'This assignment was cancelled.', 'inspect-task', 'View assignment'],
329
- running: ['Running now', 'A worker owns a current live claim.', null, ''],
361
+ running: ['Running now', '', null, ''],
330
362
  'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : 'inspect-decisions', 'Answer question'],
331
363
  'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
332
364
  'result-needs-attention': ['Needs your decision', 'Inspect the saved result and resolve its remaining execution or scope issue.', 'inspect-run', 'Inspect run'],
333
365
  '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
366
  failed: ['Needs your decision', 'The last attempt stopped; review its incident before retrying.', 'retry-task', 'Review and retry'],
335
367
  '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'],
368
+ stopping: ['Needs your decision', 'Stopping. The work so far is kept.', 'inspect-stop', 'View stop details'],
369
+ stopped: ['Needs your decision', 'Paused by a person. The work so far is kept.', 'resume-run', 'Review pause'],
338
370
  held: ['Needs your decision', s(row, 'hold_reason') ?? 'This task is on hold.', s(row, 'hold_kind') === 'operator' ? 'unhold' : 'inspect-hold', 'Review hold'],
339
371
  'waiting-incident': ['Needs your decision', 'An unresolved incident holds the next attempt.', 'retry-task', 'Review and retry'],
340
372
  'terminal-dependency': ['Needs your decision', 'A required task did not finish.', 'repair-dependency', 'Review required task'],
@@ -353,16 +385,22 @@ function itemOf(row, principal) {
353
385
  updating: ['Waiting for app update', 'New work resumes after the update.', null, ''],
354
386
  'planning-ready': ['Planner ready', 'A connected worker can draft the plan.', null, ''],
355
387
  '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, ''],
388
+ queued: ['Queued', '', null, ''],
357
389
  };
358
390
  let [label, detail, actionCode, actionLabel] = words[code];
359
391
  if (code === 'signed-out') {
360
392
  const [provider = '', authMode = 'subscription'] = (s(row, 'signed_out') ?? '').split(':');
361
393
  label = signInReason({ provider: provider, authMode: authMode === 'api-key' ? 'api-key' : 'subscription' });
362
- detail = `It starts again on its own once ${providerName(provider)} works.`;
394
+ detail = `${label}. It starts again on its own once ${providerName(provider)} works.`;
363
395
  }
364
396
  if (code === 'running' && n(row, 'version_count') > 1)
365
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
+ }
366
404
  if (code === 'terminal-dependency' && s(row, 'dependency_id') !== null)
367
405
  detail = `${s(row, 'dependency_id')} ${s(row, 'dependency_state') === 'cancelled' ? 'was cancelled' : 'failed'} before it finished.`;
368
406
  if (code === 'result-needs-attention' && n(row, 'custody_unresolved'))
@@ -377,12 +415,23 @@ function itemOf(row, principal) {
377
415
  }
378
416
  const read = actionCode !== null && ['inspect-task', 'inspect-run', 'inspect-stop', 'inspect-decisions', 'open-result'].includes(actionCode);
379
417
  const primaryAction = actionCode === null ? null : { code: actionCode, label: actionLabel,
380
- 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),
381
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
382
420
  : actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null : null,
383
421
  decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
384
422
  access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
385
423
  retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
424
+ // The one shared status: the headline and sentence every surface uses.
425
+ const reading = stageOfCode(code, { needsPerson: need, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
426
+ const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
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;
429
+ const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }),
430
+ reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
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):/, '') });
433
+ label = shared.headline;
434
+ detail = shared.sentence;
386
435
  const views = ['all'];
387
436
  if (need)
388
437
  views.push('needs-you');
@@ -395,7 +444,7 @@ function itemOf(row, principal) {
395
444
  versionCount: n(row, 'version_count'), earlierActiveCount: n(row, 'earlier_active'), familyProblem: n(row, 'broken') ? FAMILY_PROBLEM : null,
396
445
  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
446
  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',
447
+ status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
399
448
  action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
400
449
  primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
401
450
  evidence: 'recorded' };
@@ -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,6 +2,7 @@
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";
@@ -98,6 +99,7 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
98
99
  result: result === null ? null : {
99
100
  runId: result.id, role: result.role, outcome: result.outcome,
100
101
  verdict: proof?.verdict ?? null, reasons: proof?.reasons ?? [], accepted: store.proofAcceptance(result.id) !== null,
102
+ checkLevel: resultCheckLevel(store, result.id, now),
101
103
  review: reviewFactsOf(store.reviewRetryStateOf(result.id), name => {
102
104
  const runner = store.getRunner(name)?.runner;
103
105
  return runner !== undefined && isAlive(runner, now);
@@ -43,6 +43,9 @@ export type ResultFacts = {
43
43
  /** A no-change conclusion's two presence facts (handoff + sealed diff);
44
44
  * undefined when the caller did not read them. */
45
45
  recordComplete?: boolean;
46
+ /** The check level this result now stands at (check-levels.ts): Off reads
47
+ * Ready for review, saying no check ran, until a follow-up check passes. */
48
+ checkLevel?: "quick" | "full" | "off" | null;
46
49
  /** This exact run's independent review (v50 retry projection), read
47
50
  * per run so an older selected result keeps its own words; undefined
48
51
  * when the caller did not read it, null when never requested. */
@@ -182,6 +185,13 @@ export type WorkStatus = DisplayStatus & {
182
185
  * concrete act — never every queued task indiscriminately. */
183
186
  export declare function needsPerson(dispatch: DispatchDiagnosis | null): boolean;
184
187
  export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null): string;
188
+ /** A finished result's display status in the shared headline's words: a
189
+ * failed project check is Failed, a result missing its record needs a
190
+ * person, every other saved result is Ready for review. Token and action stay. */
191
+ export declare function resultHeadlineOf<T extends DisplayStatus>(status: T): T;
192
+ /** The one shared headline (task-status.ts) over the work status below: the
193
+ * token, views, rank, action and diagnostics stay; the words and tone are
194
+ * the eight headlines'. */
185
195
  export declare function workStatusOf(facts: WorkFacts, resultDisplay?: DisplayStatus): WorkStatus;
186
196
  /** The counts each view tab wears — from the same rows the page lists. */
187
197
  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,37 @@ 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 level = facts.result?.checkLevel ?? null;
331
+ const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), reason: plainReasonOf(stage, status.token, status.detail),
332
+ report: facts.result?.role === "scout", checks: stage !== "finished" ? null : VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null, level }
333
+ : level === "off" ? { status: "not-run", exitCode: null, head: null, level } : null });
334
+ return { ...status, label: shared.headline, tone: workToneOf(shared.headline), detail: stage === "finished" && level !== "off" ? status.detail : shared.sentence };
335
+ }
336
+ function workStatusWordsOf(facts, resultDisplay) {
306
337
  const dispatch = facts.dispatch;
307
338
  const result = facts.state === "done" ? resultDisplay ?? resultStatusOf(facts.result, facts.publication) : null;
308
339
  const running = facts.liveRunId !== null || (dispatch?.condition === "running" && !REVIEW_TOKENS.has(dispatch.code));
@@ -343,7 +374,7 @@ export function workStatusOf(facts, resultDisplay) {
343
374
  return { ...result, views, rank: needs ? 0 : 3 };
344
375
  }
345
376
  if (facts.state === "cancelled") {
346
- return { token: "cancelled", label: "Cancelled", detail: dispatch?.detail ?? "Nothing else will run for this task.", tone: "muted", action: null, views, rank: 4 };
377
+ return { token: "cancelled", label: dispatch?.summary.startsWith("Replaced by ") ? dispatch.summary : "Cancelled", detail: dispatch?.detail ?? "Nothing else will run for this task.", tone: "muted", action: null, views, rank: 4 };
347
378
  }
348
379
  if (question !== null && facts.state !== "failed" && dispatch !== null && QUESTION_FIRST.has(dispatch.code)) {
349
380
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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"