toolroll 0.9.7 → 0.9.9

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 (97) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/dist/assignment-adapters.d.ts +1 -0
  3. package/dist/assignment-adapters.js +2 -0
  4. package/dist/assignment-brief.d.ts +2 -0
  5. package/dist/assignment-brief.js +2 -1
  6. package/dist/assignment-presentation.js +3 -1
  7. package/dist/assignment-status.d.ts +1 -1
  8. package/dist/assignment-ui.js +5 -0
  9. package/dist/assignment.d.ts +15 -0
  10. package/dist/assignment.js +56 -9
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-crew.js +4 -0
  14. package/dist/browser-workspace.d.ts +40 -0
  15. package/dist/build-review.d.ts +38 -0
  16. package/dist/build-review.js +341 -0
  17. package/dist/builder.d.ts +2 -0
  18. package/dist/builder.js +114 -8
  19. package/dist/chat-controls.d.ts +1 -1
  20. package/dist/chat-controls.js +1 -1
  21. package/dist/check-levels-ui.d.ts +5 -0
  22. package/dist/check-levels-ui.js +16 -2
  23. package/dist/check-progress.js +4 -3
  24. package/dist/checkout-cleanup.d.ts +40 -2
  25. package/dist/checkout-cleanup.js +129 -9
  26. package/dist/cli.js +2 -0
  27. package/dist/desktop-update.d.ts +52 -0
  28. package/dist/desktop-update.js +147 -5
  29. package/dist/dispatch.js +2 -1
  30. package/dist/dispose.d.ts +4 -3
  31. package/dist/dispose.js +12 -4
  32. package/dist/evidence.d.ts +6 -0
  33. package/dist/evidence.js +8 -0
  34. package/dist/exec.d.ts +5 -0
  35. package/dist/exec.js +30 -4
  36. package/dist/first-run.d.ts +1 -1
  37. package/dist/first-run.js +8 -6
  38. package/dist/invoke.js +5 -0
  39. package/dist/lead-context.d.ts +1 -1
  40. package/dist/lead-context.js +4 -2
  41. package/dist/lead-status.d.ts +10 -1
  42. package/dist/lead-status.js +21 -24
  43. package/dist/modes.d.ts +3 -1
  44. package/dist/modes.js +3 -1
  45. package/dist/needs-you.d.ts +55 -0
  46. package/dist/needs-you.js +83 -0
  47. package/dist/operate.d.ts +2 -0
  48. package/dist/operate.js +231 -36
  49. package/dist/plane-review.js +4 -0
  50. package/dist/process-custody.js +8 -0
  51. package/dist/process-recovery-eligibility.d.ts +1 -0
  52. package/dist/project-learning.d.ts +3 -3
  53. package/dist/project-learning.js +28 -17
  54. package/dist/proof.d.ts +3 -1
  55. package/dist/proof.js +4 -2
  56. package/dist/provider.js +16 -0
  57. package/dist/result-completion.d.ts +4 -0
  58. package/dist/result-completion.js +11 -0
  59. package/dist/retention-ui.d.ts +3 -1
  60. package/dist/retention-ui.js +4 -4
  61. package/dist/retention.d.ts +13 -6
  62. package/dist/retention.js +50 -12
  63. package/dist/review-context.d.ts +3 -0
  64. package/dist/review-context.js +45 -5
  65. package/dist/review-switch.d.ts +41 -0
  66. package/dist/review-switch.js +88 -0
  67. package/dist/reviewer.d.ts +24 -2
  68. package/dist/reviewer.js +49 -1
  69. package/dist/run-closeout.d.ts +89 -0
  70. package/dist/run-closeout.js +302 -0
  71. package/dist/serve.d.ts +15 -1
  72. package/dist/serve.js +158 -18
  73. package/dist/setup-guide.d.ts +2 -2
  74. package/dist/shared-deps.d.ts +94 -0
  75. package/dist/shared-deps.js +365 -0
  76. package/dist/storage.js +2 -0
  77. package/dist/store.d.ts +72 -1
  78. package/dist/store.js +258 -31
  79. package/dist/surface.js +5 -1
  80. package/dist/task-status.d.ts +22 -5
  81. package/dist/task-status.js +45 -24
  82. package/dist/telegram-progress.js +2 -1
  83. package/dist/telegram-status.js +3 -3
  84. package/dist/test-temp.d.ts +19 -0
  85. package/dist/test-temp.js +98 -0
  86. package/dist/toolroll-update-ui.js +7 -2
  87. package/dist/toolroll-update.d.ts +21 -1
  88. package/dist/toolroll-update.js +135 -5
  89. package/dist/work-index.d.ts +11 -0
  90. package/dist/work-index.js +103 -16
  91. package/dist/work-summary.d.ts +3 -1
  92. package/dist/work-summary.js +2 -1
  93. package/dist/workspace-ui.d.ts +3 -1
  94. package/dist/workspace-ui.js +10 -9
  95. package/dist/worktree.d.ts +16 -0
  96. package/dist/worktree.js +47 -0
  97. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,53 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.9.9 — 2026-10-02
4
+
5
+ - **Faster, lighter tests.** The unit suite runs about three times faster
6
+ (the biggest test files are split by area), tests use at most 4 workers,
7
+ and a project's Quick check can be a typecheck plus the tests for the
8
+ changed files.
9
+ - **Toolroll cleans up after itself.** When a run ends, it stops what the run
10
+ left running. Tests delete their temp folders, and `toolroll storage clean`
11
+ removes leftovers. Replaced release checks give up their checkouts, and
12
+ kept checkouts drop their dependencies until they're needed.
13
+ - **Checkouts share dependencies.** One install per lockfile, linked into
14
+ each checkout: about 1,900 files instead of 20,000, set up in under a second.
15
+ - **Every "Needs you" says what it needs, with the button that does it.**
16
+ Anything you can't act on shows as Waiting instead.
17
+ - **Finished tasks read Complete everywhere.** Status, the console and chat
18
+ agree, and old "process exit not recorded" entries clear themselves.
19
+ - **Updates never hang.** The updater settles leftover records itself, stops
20
+ waiting after 2 minutes otherwise, and says the one command that clears it.
21
+ Release checks now test upgrading from the last three releases.
22
+ - **Screenshots stay out of the repository.** Images under `evidence/` are
23
+ left out of commits and show as one line in the review.
24
+
25
+ ## 0.9.8 — 2026-10-01
26
+
27
+ - **One automatic review per build.** When a build's checks pass (or it
28
+ finishes, with checks Off) in a project with review on, one read-only
29
+ reviewer reads the scope, acceptance and diff and grades each finding HIGH,
30
+ MEDIUM or LOW. Any HIGH sends the task back once, with the findings as its
31
+ note; a HIGH on that revision comes to you. MEDIUM and LOW never block: they
32
+ show on the result as suggested follow-ups. A review that fails or times
33
+ out never blocks: the result reaches you marked Not reviewed. Turn it on or
34
+ off per project in Settings or with `toolroll review on|off --repo <path>`;
35
+ it's on by default under a hands-off mode. Release candidate checks are
36
+ never reviewed.
37
+ - **Evidence is kept 1, 7, 14 or 28 days, or forever.** Settings →
38
+ Retention and `toolroll retention set evidence 1d` take any of them. Until
39
+ someone chooses, evidence is kept 28 days: **after upgrading, the daily
40
+ sweep starts removing finished tasks' evidence older than 28 days.** A
41
+ period chosen before stays as it was; one the page no longer offers shows
42
+ as "30 days (custom)".
43
+ Nothing a task still needs is removed: unfinished tasks, results Ready for
44
+ review, anything on hold, live runs and release candidates.
45
+ - **A run whose process never started no longer blocks updates.** A spawn
46
+ that fails now settles its own record, and Toolroll settles a finished run's
47
+ leftover record once its processes are provably gone, noting it in the
48
+ ledger. For the rare record that can't be proven either way, an approver
49
+ can run `toolroll run settle <run> --why "<reason>"`; it refuses while
50
+ anything of the run is alive.
4
51
 
5
52
  ## 0.9.7 — 2026-10-01
6
53
 
@@ -140,6 +140,7 @@ export declare function assignmentForCoordinator(store: Store, token: string, op
140
140
  prUrl: string | null;
141
141
  remoteState: string | null;
142
142
  } | null;
143
+ review: import("./review-switch.js").BuildReviewView | null;
143
144
  deployment: {
144
145
  status: "not-recorded";
145
146
  };
@@ -1,5 +1,6 @@
1
1
  /** Assignment reads and delivery acknowledgments. These adapters never start
2
2
  * work, answer a decision, accept proof, or change an approval. */
3
+ import { buildReviewLines } from "./review-switch.js";
3
4
  import { readFileSync, statSync } from "node:fs";
4
5
  import { authenticateCoordinator } from "./coordinator.js";
5
6
  import { assignmentOf, assignmentUpdates, claimAssignment, checkAssignment } from "./assignment.js";
@@ -113,6 +114,7 @@ function assignmentLines(assignment) {
113
114
  ...(receipt.agentReport === null ? [] : [`Agent report: ${receipt.agentReport}`]),
114
115
  `Receipt: ${receipt.digest}`,
115
116
  ]),
117
+ ...buildReviewLines(assignment.review ?? null).map(line => line.trimStart().replace(/^review: /, "Review: ")),
116
118
  ...(assignment.savedContext === undefined ? [] : [
117
119
  ...(assignment.savedContext.goal === null ? [] : [`Goal: ${assignment.savedContext.goal}`]),
118
120
  ...(assignment.savedContext.outOfScope === null ? [] : [`Out of scope: ${assignment.savedContext.outOfScope}`]),
@@ -23,6 +23,8 @@ export type AssignmentCatchUp = {
23
23
  } | null;
24
24
  nextAction: AssignmentSnapshot["primaryAction"];
25
25
  attention: string[];
26
+ /** Where the next action leads in the console (navigation only). */
27
+ nextHref?: string | null;
26
28
  decisions: {
27
29
  id: number;
28
30
  runId: number;
@@ -1,5 +1,6 @@
1
1
  /** A bounded catch-up over saved project/task records, not conversation memory.
2
2
  * Callers authenticate first and pass their current project ceiling. */
3
+ import { browserWorkActionHref } from './browser-crew.js';
3
4
  import { createHash } from "node:crypto";
4
5
  import { workIndexPage } from "./work-index.js";
5
6
  import { publicChatText } from "./chat-display.js";
@@ -72,7 +73,7 @@ export function assignmentCatchUp(store, now, access, query = {}, evidenceRoot)
72
73
  runId: item.liveRunId ?? item.unfinishedRunId ?? item.resultRunId, resultRunId: item.resultRunId,
73
74
  outcome: saved?.["handoff"] == null ? null : text(String(saved["handoff"]), 400),
74
75
  checks: item.resultRunId === null ? null : { status: "not-read", exitCode: null, detail: "Saved checks were not revalidated. Use assignment show to inspect them." },
75
- nextAction: item.primaryAction, attention: attention.slice(0, 3).map(one => text(one, 160)),
76
+ nextAction: item.primaryAction, nextHref: browserWorkActionHref(item), attention: attention.slice(0, 3).map(one => text(one, 160)),
76
77
  decisions: decisions.map(row => ({ id: Number(row["id"]), runId: Number(row["run"]), question: text(String(row["question"]), 240), state: String(row["state"]),
77
78
  overdue: row["state"] !== "answered" && row["deadline"] !== null && String(row["deadline"]) <= now.toISOString(), choice: row["choice"] == null ? null : text(String(row["choice"]), 80) })),
78
79
  };
@@ -35,9 +35,11 @@ export function assignmentPresentationOf(assignment, options = {}) {
35
35
  // rank stay the existing projection's (pages and tests key off them).
36
36
  const shared = { label: taskStatus.headline, tone: workToneOf(taskStatus.headline), detail: taskStatus.sentence, action: null };
37
37
  const needsYou = taskStatus.headline === 'Needs you';
38
+ // Waiting needs no one: never listed under Needs you.
39
+ const waiting = taskStatus.headline === 'Waiting';
38
40
  const status = state === 'working'
39
41
  ? { ...shared, token: `assignment-${state}`,
40
- views: [...new Set([...(work?.views?.filter(view => view !== 'completed') ?? ['all']), ...(needsYou ? ['needs-you'] : [])])], rank: needsYou ? 0 : work?.rank ?? 2 }
42
+ views: [...new Set([...(work?.views?.filter(view => view !== 'completed' && !(waiting && view === 'needs-you')) ?? ['all']), ...(needsYou ? ['needs-you'] : [])])], rank: needsYou ? 0 : waiting ? 2 : work?.rank ?? 2 }
41
43
  : { ...shared, token: `assignment-${state}`,
42
44
  views: state === 'ready-to-check' || state === 'needs-decision' ? ['all', 'needs-you'] : state === 'complete' ? ['all', 'completed']
43
45
  : state === 'checking' && work?.views?.includes('running') ? ['all', 'running'] : ['all'],
@@ -17,7 +17,7 @@ export declare function assignmentStatusBrief(assignment: AssignmentSnapshot): {
17
17
  } | null;
18
18
  runId: number | null;
19
19
  primaryAction: {
20
- code: "inspect-decisions" | "inspect-run" | "inspect-stop" | "inspect-task" | "open-pr" | "reconcile-run" | import("./dispatch.js").DispatchAction;
20
+ code: "confirm-stopped" | "inspect-decisions" | "inspect-run" | "inspect-stop" | "inspect-task" | "open-pr" | "reconcile-run" | "sign-in" | import("./dispatch.js").DispatchAction;
21
21
  label: string;
22
22
  target: import("./work-summary.js").WorkTarget;
23
23
  } | null;
@@ -27,6 +27,11 @@ export function assignmentActionHref(assignment) {
27
27
  return `/d/${decisionId}`;
28
28
  if (action.code === 'inspect-decisions')
29
29
  return '/';
30
+ if (action.code === 'sign-in')
31
+ return '/settings#providers';
32
+ // The confirmation sits on the task page, behind the password.
33
+ if (action.code === 'confirm-stopped')
34
+ return `/t/${encodeURIComponent(assignment.rootId)}#confirm-stopped`;
30
35
  if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(assignment.publication?.prUrl ?? ''))
31
36
  return assignment.publication.prUrl;
32
37
  if (runId !== null && (action.code === 'open-result' || action.code === 'open-pr' || action.code === 'retry-review')) {
@@ -2,6 +2,8 @@ import { type Store, type ProofVerdictRow, type ProofAcceptanceRow } from "./sto
2
2
  import { type WorkAction, type WorkSummaryAccess } from "./work-summary.js";
3
3
  import { type VerifiedApprover } from "./principal.js";
4
4
  import { type CheckLevel } from "./check-levels.js";
5
+ import { type BuildReviewView } from "./review-switch.js";
6
+ import { type NeedKey, type WaitKey } from "./needs-you.js";
5
7
  export type AssignmentAccess = WorkSummaryAccess;
6
8
  export type AssignmentOwner = {
7
9
  kind: "coordinator" | "lead";
@@ -92,9 +94,20 @@ export type AssignmentSnapshot = {
92
94
  prUrl: string | null;
93
95
  remoteState: string | null;
94
96
  } | null;
97
+ /** The result's one automatic review, when its project had review on:
98
+ * pending, HIGH findings, suggested follow-ups (MEDIUM/LOW), or not reviewed. */
99
+ review: BuildReviewView | null;
95
100
  deployment: {
96
101
  status: "not-recorded";
97
102
  };
103
+ /** What this assignment asks of a person, or waits for when no person can act (needs-you.ts). */
104
+ need?: {
105
+ key: NeedKey;
106
+ build: number | null;
107
+ } | {
108
+ wait: WaitKey;
109
+ build: number | null;
110
+ } | null;
98
111
  };
99
112
  /** Status-first handoff for routine reads. Fetch get_assignment only when
100
113
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
@@ -142,6 +155,7 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
142
155
  prUrl: string | null;
143
156
  remoteState: string | null;
144
157
  } | null;
158
+ review: BuildReviewView | null;
145
159
  deployment: {
146
160
  status: "not-recorded";
147
161
  };
@@ -220,6 +234,7 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
220
234
  prUrl: string | null;
221
235
  remoteState: string | null;
222
236
  } | null;
237
+ review: BuildReviewView | null;
223
238
  deployment: {
224
239
  status: "not-recorded";
225
240
  };
@@ -6,7 +6,7 @@ import { currentActor, withActor } from "./actor.js";
6
6
  import { replacedWords } from "./task-status.js";
7
7
  import { COMPLETION_ACTION, familyChangedFiles } from "./result-completion.js";
8
8
  import { homedir } from "node:os";
9
- import { readSchemaVersion } from "./store.js";
9
+ import { readSchemaVersion, stopFactOf } from "./store.js";
10
10
  import { approvalOf } from "./scope.js";
11
11
  import { openWorkDecisionOf, taskWorkSummaryOf, workDecisionAction } from "./work-summary.js";
12
12
  import { evidenceRoot, readVerifiedArtifact, readVerifiedReport } from "./evidence.js";
@@ -16,6 +16,9 @@ import { noteAssignmentStatus } from "./assignment-status.js";
16
16
  import { historicalAssessmentReason } from "./assignment-presentation.js";
17
17
  import { runCheckLevel } from "./check-levels.js";
18
18
  import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
19
+ import { buildReviewOf, findingWords } from "./review-switch.js";
20
+ import { NEEDS, WAITS, processNeedOf } from "./needs-you.js";
21
+ import { assignmentStageOf } from "./task-status.js";
19
22
  /** Status-first handoff for routine reads. Fetch get_assignment only when
20
23
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
21
24
  export function assignmentBrief(assignment) {
@@ -29,7 +32,7 @@ export function assignmentBrief(assignment) {
29
32
  base: receipt.base, head: receipt.head, completionKind: receipt.completionKind,
30
33
  checks: receipt.checks, proofAcceptance: receipt.proofAcceptance, verdict: receipt.proof?.verdict ?? null,
31
34
  criteria: { passed: receipt.proof?.matrix.filter(row => row.state === "pass").length ?? 0, total: receipt.proof?.matrix.length ?? 0 }, evidence: receipt.evidence },
32
- publication: assignment.publication, deployment: assignment.deployment };
35
+ publication: assignment.publication, review: assignment.review, deployment: assignment.deployment };
33
36
  }
34
37
  const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
35
38
  /** A lookup added with lead-quiet, read safely: a deploy proves completion with this code over the INSTALLED runtime's
@@ -182,11 +185,17 @@ export function assignmentOf(store, taskId, now, access, root) {
182
185
  let detail = work.status.detail;
183
186
  let primaryAction = work.primaryAction;
184
187
  const live = work.liveRunId !== null || work.status.token === "running";
185
- const processProblem = result === null ? null : store.stopQuiescenceProblem(result.id);
188
+ const processFact = result === null ? null : stopFactOf(store, result.id);
189
+ const processProblem = processFact?.problem ?? null;
190
+ let need = null;
186
191
  // The exact completed result, scope, family and custody fences apply to
187
192
  // every deliverable. Human acceptance remains its own recorded authority;
188
193
  // it never changes a machine verdict or supplies a missing report.
189
- const ready = current.state === "done" && result !== null && completionKind !== null &&
194
+ // Kept out of the sealed receipt (sealedChecks): the review never changes the result's digest.
195
+ const review = result === null ? null : olderStoreSafe(() => buildReviewOf(store, result.id), null);
196
+ // A result under its one automatic review has not reached the person yet.
197
+ const reviewing = review?.state === "pending" && current.state === "done";
198
+ const ready = !reviewing && current.state === "done" && result !== null && completionKind !== null &&
190
199
  scope !== null && scope.termsProblem == null && approvalOf(scope).approved && scope.digest === result.scopeDigest &&
191
200
  store.activeHolds(current.refId, now).length === 0 && store.applicableStopFor(result.id) === null &&
192
201
  processProblem === null &&
@@ -200,10 +209,23 @@ export function assignmentOf(store, taskId, now, access, root) {
200
209
  primaryAction = successor === null ? { code: "inspect-task", label: "View assignment", target: { taskId: current.id, runId: null, decisionId: null }, access: "read", retry: "read-again" }
201
210
  : { code: "inspect-task", label: `Open ${successor}`, target: { taskId: successor, runId: null, decisionId: null }, access: "read", retry: "read-again" };
202
211
  }
203
- else if (!live && processProblem !== null && result !== null) {
204
- state = "needs-decision";
205
- detail = processProblem;
206
- primaryAction = { code: "inspect-run", label: "Inspect run", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
212
+ else if (!live && processFact !== null && result !== null) {
213
+ // Toolroll can't confirm the build stopped. A person confirms it when
214
+ // nothing of it may still run; otherwise the work waits, needing no one.
215
+ const process = processNeedOf(processFact);
216
+ if ("need" in process) {
217
+ state = "needs-decision";
218
+ detail = NEEDS[process.need].sentence({ build: process.build });
219
+ primaryAction = { code: "confirm-stopped", label: NEEDS[process.need].action.label, target: { taskId: current.id, runId: process.build, decisionId: null },
220
+ access: access.principal === "operator" ? "operator-control" : "operator-handoff", retry: "refresh-before-acting" };
221
+ need = { key: process.need, build: process.build };
222
+ }
223
+ else {
224
+ state = "working";
225
+ detail = WAITS[process.wait]({ build: process.build });
226
+ primaryAction = { code: "inspect-run", label: "View the build", target: { taskId: current.id, runId: process.build, decisionId: null }, access: "read", retry: "read-again" };
227
+ need = { wait: process.wait, build: process.build };
228
+ }
207
229
  }
208
230
  else if (current.state === "done" && questions.length > 0 && work.status.tone !== "problem" &&
209
231
  !["stopping", "stopped", "review-failed", "review-exhausted"].includes(work.status.token) &&
@@ -213,6 +235,11 @@ export function assignmentOf(store, taskId, now, access, root) {
213
235
  detail = question.decision.question;
214
236
  primaryAction = workDecisionAction(question.taskId, question.decision, access.principal);
215
237
  }
238
+ else if (reviewing && result !== null) {
239
+ state = "checking";
240
+ detail = "An automatic review is reading this result before it reaches you.";
241
+ primaryAction = { code: "inspect-run", label: "Open result", target: { taskId: current.id, runId: result.id, decisionId: null }, access: "read", retry: "read-again" };
242
+ }
216
243
  else if (ready && receipt !== null) {
217
244
  state = "ready-to-check";
218
245
  detail = completionKind === "research-report" ? "The research report is ready for the lead to read."
@@ -242,6 +269,14 @@ export function assignmentOf(store, taskId, now, access, root) {
242
269
  ? completionKind === null ? "The research report is missing or incomplete. Inspect the saved report before checking this handoff."
243
270
  : "The saved report is awaiting resolution of its current scope or hold."
244
271
  : "Inspect the saved result and resolve its remaining execution or scope issue.";
272
+ // Built to an earlier plan: the result page can't resolve it (accepting would leave it stuck), building again can.
273
+ if (current.state === "done" && result !== null && scope !== null && scope.termsProblem == null && approvalOf(scope).approved && !!result.scopeDigest && scope.digest !== result.scopeDigest &&
274
+ store.activeHolds(current.refId, now).length === 0 && unfinished === null && store.currentLiveLease(current.refId, now) === null && store.finalResultReason(result.id) === null) {
275
+ detail = NEEDS.rebuild.sentence({ build: result.id });
276
+ primaryAction = { code: "retry-task", label: NEEDS.rebuild.action.label, target: { taskId: current.id, runId: result.id, decisionId: null },
277
+ access: access.principal === "operator" ? "operator-control" : "operator-handoff", retry: "refresh-before-acting" };
278
+ need = { key: "rebuild", build: result.id };
279
+ }
245
280
  }
246
281
  if (earlierActive.length > 0) {
247
282
  attention.push(`${earlierActive.length} earlier task version${earlierActive.length === 1 ? " is" : "s are"} still active.`);
@@ -256,6 +291,13 @@ export function assignmentOf(store, taskId, now, access, root) {
256
291
  }
257
292
  if (state === "needs-decision")
258
293
  attention.push(detail);
294
+ // Every Needs you action wears its need's own words (needs-you.ts), on every surface.
295
+ if (state === "needs-decision" && primaryAction !== null && need === null) {
296
+ const read = assignmentStageOf({ state, primaryAction, review }, { token: work.status.token, views: work.status.views });
297
+ // A result review keeps its own, more specific words (See what is missing, Open the failed check).
298
+ if (read.stage === "needs-you" && read.need !== undefined && read.need !== "other" && read.need !== "review-result")
299
+ primaryAction = { ...primaryAction, label: NEEDS[read.need].action.label };
300
+ }
259
301
  if (receipt !== null) {
260
302
  if (finishedBuild && receipt.checks.status !== "passed")
261
303
  attention.push(receipt.checks.detail);
@@ -263,6 +305,11 @@ export function assignmentOf(store, taskId, now, access, root) {
263
305
  attention.push(...(proof?.reasons ?? []).filter(reason => !historicalAssessmentReason(reason)));
264
306
  attention.push(...receipt.caveats.filter(reason => !proof?.reasons.includes(reason)));
265
307
  }
308
+ if (review !== null && review.sentBackAs === null) {
309
+ if (review.state === "not-reviewed")
310
+ attention.push(`Not reviewed: ${review.reason ?? "the automatic review did not finish"}.`);
311
+ attention.push(...review.high.map(one => `Review: ${findingWords(one)}`));
312
+ }
266
313
  if (state !== "complete")
267
314
  completion = null;
268
315
  if (current.state !== "cancelled")
@@ -292,7 +339,7 @@ export function assignmentOf(store, taskId, now, access, root) {
292
339
  return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
293
340
  state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
294
341
  publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
295
- deployment: { status: "not-recorded" } };
342
+ review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }) };
296
343
  }
297
344
  function admittedOwner(store, taskId, owner, now, root) {
298
345
  if (owner.kind !== "coordinator")