toolroll 0.9.74 → 0.9.75

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 (83) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/assignment.d.ts +3 -0
  3. package/dist/assignment.js +27 -8
  4. package/dist/browser/workspace.js +27 -27
  5. package/dist/browser-workspace.d.ts +1 -1
  6. package/dist/chat-acceptance.d.ts +4 -1
  7. package/dist/chat-acceptance.js +13 -3
  8. package/dist/cli.js +1 -1
  9. package/dist/coding-handoff.js +4 -3
  10. package/dist/coding-ui.js +2 -2
  11. package/dist/coding-workspace.js +16 -4
  12. package/dist/contracts/console-api.d.ts +5 -1
  13. package/dist/contracts/console-api.js +10 -0
  14. package/dist/control-ui.d.ts +3 -2
  15. package/dist/control-ui.js +7 -2
  16. package/dist/demo.js +5 -3
  17. package/dist/desktop-update.d.ts +3 -0
  18. package/dist/desktop-update.js +43 -9
  19. package/dist/first-run.d.ts +8 -5
  20. package/dist/first-run.js +39 -6
  21. package/dist/flow-card-task.js +3 -2
  22. package/dist/flow-unattended.d.ts +2 -0
  23. package/dist/flow-unattended.js +12 -1
  24. package/dist/integrations.d.ts +4 -3
  25. package/dist/integrations.js +5 -6
  26. package/dist/lead-contract.d.ts +1 -1
  27. package/dist/lead-contract.js +2 -2
  28. package/dist/lead-follow.js +3 -1
  29. package/dist/openrouter-models.d.ts +5 -2
  30. package/dist/openrouter-models.js +7 -4
  31. package/dist/operate.js +2 -0
  32. package/dist/project-learning.js +44 -6
  33. package/dist/project-tools.js +19 -1
  34. package/dist/pull-request-ui.d.ts +1 -1
  35. package/dist/pull-request-ui.js +5 -1
  36. package/dist/push.d.ts +2 -0
  37. package/dist/push.js +24 -19
  38. package/dist/read-only-request.d.ts +4 -0
  39. package/dist/read-only-request.js +15 -0
  40. package/dist/repository-context.js +6 -1
  41. package/dist/result-follow-ups.js +20 -1
  42. package/dist/result-review.js +5 -1
  43. package/dist/serve.js +162 -6
  44. package/dist/server/agents-live-filed.d.ts +17 -0
  45. package/dist/server/agents-live-filed.js +97 -0
  46. package/dist/server/agents-live-room.js +4 -1
  47. package/dist/server/chat.js +31 -23
  48. package/dist/server/http.d.ts +5 -0
  49. package/dist/server/last-checks.d.ts +17 -0
  50. package/dist/server/last-checks.js +22 -0
  51. package/dist/server/pages.js +18 -4
  52. package/dist/server/people-tokens.js +7 -4
  53. package/dist/server/render-pages.d.ts +2 -1
  54. package/dist/server/render-pages.js +28 -24
  55. package/dist/server/render-results.d.ts +11 -2
  56. package/dist/server/render-results.js +54 -12
  57. package/dist/server/render-settings.js +3 -3
  58. package/dist/server/render-tasks.js +7 -4
  59. package/dist/server/route-table.js +14 -1
  60. package/dist/server/runtime.d.ts +16 -1
  61. package/dist/server/settings.js +82 -29
  62. package/dist/server/task-views.js +0 -1
  63. package/dist/server/tasks.js +11 -2
  64. package/dist/storage-ui.d.ts +2 -0
  65. package/dist/storage-ui.js +1 -1
  66. package/dist/storage.js +2 -1
  67. package/dist/store.d.ts +3 -0
  68. package/dist/store.js +33 -9
  69. package/dist/subagent-work.js +3 -0
  70. package/dist/task-status.d.ts +28 -1
  71. package/dist/task-status.js +93 -11
  72. package/dist/teardown-git.d.ts +27 -1
  73. package/dist/teardown-git.js +185 -39
  74. package/dist/telegram-progress.js +4 -4
  75. package/dist/telegram-status.js +8 -3
  76. package/dist/tool-secret-store.js +8 -0
  77. package/dist/toolroll-update-ui.d.ts +2 -0
  78. package/dist/toolroll-update-ui.js +4 -3
  79. package/dist/toolroll-update.js +10 -9
  80. package/dist/work-index.js +29 -12
  81. package/dist/workspace-ui.d.ts +5 -0
  82. package/dist/workspace-ui.js +38 -10
  83. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  * previous runtime and its backups. Every update, rollback, refusal and
23
23
  * failure is a ledger entry.
24
24
  */
25
+ import { inReadOnlyRequest } from "./read-only-request.js";
25
26
  import { createHash, randomUUID, verify as signatureValid, X509Certificate } from "node:crypto";
26
27
  import { spawnSync } from "node:child_process";
27
28
  import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, writeFileSync } from "node:fs";
@@ -29,7 +30,7 @@ import { homedir, userInfo } from "node:os";
29
30
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
30
31
  import { fileURLToPath, pathToFileURL } from "node:url";
31
32
  import { createRequire } from "node:module";
32
- import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
33
+ import { clearWords, durableJson, LINGERING_LIMIT_MS, lingeringRun, lingeringWords, settleLeftoverRecords, sqliteLock, lockHeld, desktopUpdateWaitingOf, stoppedWaitingWords, updateWaitingWords, verifiedDatabaseBackup } from "./desktop-update.js";
33
34
  import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate, updateAdmissionPaused, updateGateOwned } from "./desktop-update-gate.js";
34
35
  import { processMayBeAlive } from "./process-liveness.js";
35
36
  import { removeTerminalUpdateGate } from "./terminal-catalog.js";
@@ -1445,11 +1446,8 @@ export function runtimeUpdateStatus(stateDir) {
1445
1446
  j = null;
1446
1447
  }
1447
1448
  let running = false;
1448
- if (j && !runtimeUpdateTerminal(j.phase)) {
1449
- const lock = sqliteLock(join(j.stageDir, "worker.sqlite"));
1450
- running = lock === null;
1451
- lock?.close();
1452
- }
1449
+ if (j && !runtimeUpdateTerminal(j.phase))
1450
+ running = lockHeld(join(j.stageDir, "worker.sqlite"));
1453
1451
  const whatsNew = j && j.kind === "update" && j.phase === "complete" && !j.seen ? { version: j.to.version, notes: j.notes ?? [] } : null;
1454
1452
  let last = null;
1455
1453
  try {
@@ -1525,10 +1523,13 @@ export function releaseStalledUpdate(stateDir, now) {
1525
1523
  * waiting update, `toolroll update`'s or the desktop app's. */
1526
1524
  export function waitingUpdate(databaseFile, inTheWay, now, desktopStates) {
1527
1525
  const stateDir = dirname(databaseFile);
1528
- try {
1529
- releaseStalledUpdate(stateDir, now);
1526
+ // A page visit only reads (docs/plans/get-side-effects.md): the console's background pass releases a stalled update.
1527
+ if (!inReadOnlyRequest()) {
1528
+ try {
1529
+ releaseStalledUpdate(stateDir, now);
1530
+ }
1531
+ catch { /* an unreadable record is shown as it is */ }
1530
1532
  }
1531
- catch { /* an unreadable record is shown as it is */ }
1532
1533
  return updateWaitingOf(stateDir, inTheWay) ?? desktopUpdateWaitingOf(databaseFile, inTheWay, desktopStates);
1533
1534
  }
1534
1535
  export function markWhatsNewSeen(stateDir) {
@@ -10,7 +10,7 @@ import { readProjectAccess } from './project-access.js';
10
10
  import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
11
11
  import { scopeTermsProblem, stopFactOf } from './store.js';
12
12
  import { assignmentChecksForRun } from './assignment.js';
13
- import { checkBackingOf, completionBlockersOf, leadOnIt, plainReasonOf, replacedWords, requirementsOf, stageOfCode, taskStatusOf, unverifiedWhenRefuted, workToneOf } from './task-status.js';
13
+ import { approvedCheckOf, checkBackingOf, completionBlockersOf, leadOnIt, plainReasonOf, replacedWords, requirementsOf, stageOfCode, taskStatusOf, unverifiedWhenRefuted, workToneOf } from './task-status.js';
14
14
  import { releaseCoverageOf } from './release-coverage.js';
15
15
  import { buildReviewOf } from './review-switch.js';
16
16
  import { shownVerdictOf } from './proof.js';
@@ -429,7 +429,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
429
429
  // automatic review and, with checks Off, the release check that covers it.
430
430
  const memo = new Map();
431
431
  const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
432
- return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
432
+ return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
433
433
  groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), nextCursor, limit, view };
434
434
  }
435
435
  export function workCountsByProject(store, now, access) {
@@ -489,22 +489,36 @@ function listChecksOf(own, level, follows) {
489
489
  return null;
490
490
  return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
491
491
  }
492
+ /** A check as a read without the evidence files may show it: only what never claims a pass it can't prove. */
493
+ function unreadChecksOf(checks) {
494
+ if (checks === null)
495
+ return null;
496
+ return checks.status === 'failed' || (checks.level === 'off' && checks.status !== 'passed' && checks.release == null) ? checks : null;
497
+ }
492
498
  /** A ready result's requirements, acceptance and completion blockers from its saved records, as assignmentOf reads them. */
493
499
  function resultReadinessFacts(store, now, root, row, recorded, memo) {
494
500
  const run = n(row, 'result_id'), repo = s(row, 'repo');
495
501
  if (!run)
496
- return { checks: recorded };
502
+ return { checks: root === undefined ? unreadChecksOf(recorded) : recorded };
497
503
  const report = s(row, 'result_role') === 'scout';
498
- // No recorded check result (a build from before results were recorded, or none ran): read its saved receipt, as the page does.
499
- const saved = root !== undefined && (recorded === null || recorded.status === 'not-run') ? assignmentChecksForRun(store, root, run, now) : null;
504
+ // Read its saved receipt, as the page does: a recorded result (or a verdict alone) is not a check that can still be
505
+ // read, and the row must never say passed while the task page says the check couldn't be read.
506
+ const read = root !== undefined && !report ? assignmentChecksForRun(store, root, run, now) : null;
507
+ // A recorded failure is never lost because its saved files can't be read now (fail closed: negatives stand).
508
+ const saved = read !== null && recorded?.status === 'failed' && (read.status === 'unavailable' || read.status === 'not-run')
509
+ ? { ...read, status: 'failed', exitCode: recorded.exitCode } : read;
500
510
  const listed = saved === null ? recorded : { status: saved.status, exitCode: saved.exitCode, head: s(row, 'result_head'),
501
511
  ...(saved.level == null ? {} : { level: saved.level }), ...(saved.running == null ? {} : { running: saved.running }), ...(saved.batch == null ? {} : { batch: saved.batch }) };
502
- const checks = listed?.level === 'off' && listed.status !== 'passed' && listed.status !== 'failed'
512
+ const covered = listed?.level === 'off' && listed.status !== 'passed' && listed.status !== 'failed'
503
513
  ? { ...listed, release: releaseCoverageOf(store, root, repo, s(row, 'result_head'), { memo }) } : listed;
504
514
  const proof = store.proofVerdictFor(run), accepted = store.proofAcceptance(run) !== null;
515
+ // The approved check as the task page reads it (task-status.ts): a recorded pass with no readable check doesn't count.
516
+ // Without the evidence root nothing can be read, so the row makes no claim about the check at all: a recorded
517
+ // failure or checks Off still say so; a pass, or a missing check, is the result's own page to say.
518
+ const checks = report ? covered : root === undefined ? unreadChecksOf(covered) : approvedCheckOf(proof?.verdict ?? null, proof?.reasons ?? [], covered);
505
519
  const review = buildReviewOf(store, run);
506
520
  const high = review === null || review.sentBackAs !== null || review.state !== 'reviewed' ? 0 : review.high.length;
507
- const checkRequired = requiredCheckCommandFor(store, repo, String(row['id']), checks?.level) !== null;
521
+ const checkRequired = root === undefined && checks === null ? false : requiredCheckCommandFor(store, repo, String(row['id']), checks?.level) !== null;
508
522
  const backing = checkBackingOf(checks);
509
523
  const requirements = requirementsOf(proof?.matrix, backing);
510
524
  const verdict = shownVerdictOf(proof?.verdict ?? null, { checkFailed: !report && checks?.status === 'failed', highFindings: high, reviewPending: review?.state === 'pending',
@@ -512,7 +526,7 @@ function resultReadinessFacts(store, now, root, row, recorded, memo) {
512
526
  const blockers = completionBlockersOf({ report, checks, checkRequired, matrix: proof?.matrix, verdict: proof?.verdict ?? null, accepted, high });
513
527
  return { checks, requirements: unverifiedWhenRefuted(requirements, verdict), ...(accepted ? { accepted } : {}), ...(blockers.length === 0 ? {} : { blockers }) };
514
528
  }
515
- function itemOf(row, principal, probe, leadName, readiness) {
529
+ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
516
530
  const code = String(row['code']), id = String(row['id']);
517
531
  const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
518
532
  const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
@@ -624,15 +638,18 @@ function itemOf(row, principal, probe, leadName, readiness) {
624
638
  : coded.stage === 'needs-you' && (coded.need === undefined || coded.need === 'other') && INDEX_NEED[code] !== undefined ? { stage: 'needs-you', need: INDEX_NEED[code] } : coded;
625
639
  const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
626
640
  const finished = reading.stage === 'finished' || reading.stage === 'complete';
627
- const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' || verdict === 'verified' ? 'passed' : null;
628
- const listedChecks = !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']);
629
- const ready = reading.stage === 'finished' && readiness !== undefined ? readiness(row, listedChecks) : null;
641
+ // Only a recorded check is a pass; a verdict alone isn't one (task-status.ts approvedCheckOf reads it with the receipt).
642
+ const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' ? 'passed' : null;
643
+ const recordedChecks = !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']);
644
+ // A read without the evidence files claims no pass (unreadChecksOf); the readiness read below says the rest.
645
+ const listedChecks = readable ? recordedChecks : unreadChecksOf(recordedChecks);
646
+ const ready = reading.stage === 'finished' && readiness !== undefined ? readiness(row, recordedChecks) : null;
630
647
  // Toolroll marked it complete because its work landed: the reason, said once.
631
648
  const landed = code === 'complete' ? landedReasonOf(s(row, 'checked_actor'), s(row, 'checked_detail')) : null;
632
649
  const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
633
650
  ...(process === null ? {} : { needContext: { build: process.build } }),
634
651
  reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
635
- checks: ready?.checks ?? listedChecks,
652
+ checks: ready !== null ? ready.checks : listedChecks,
636
653
  ...(ready === null ? {} : { requirements: ready.requirements ?? null, ...(ready.accepted ? { accepted: true } : {}), ...(ready.blockers === undefined ? {} : { blockers: ready.blockers }) }),
637
654
  completedBy: code !== 'complete' || landed !== null ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
638
655
  ...(landed === null ? {} : { landed }),
@@ -22,6 +22,7 @@ export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type EvidenceP
22
22
  import { type ProofVerdict } from "./proof.js";
23
23
  import type { ReviewRetryState, TaskState } from "./store.js";
24
24
  import type { TaskControlView } from "./task-control.js";
25
+ import { type ChecksFact } from "./task-status.js";
25
26
  import { type Html } from "./html.js";
26
27
  /** The Work destination's views — shortcuts over the same rows, never a
27
28
  * persisted state. All is the default. */
@@ -53,6 +54,10 @@ export type ResultFacts = {
53
54
  * per run so an older selected result keeps its own words; undefined
54
55
  * when the caller did not read it, null when never requested. */
55
56
  review?: ReviewFacts | null;
57
+ /** The saved approved check as read now (assignmentChecksForRun), for `approvedCheckOf`: the words say the check
58
+ * passed only when this reading does. Undefined: the caller read no check (a list or dispatch read), so the words
59
+ * name only what the verdict recorded when the build finished. */
60
+ checks?: ChecksFact | null;
56
61
  };
57
62
  /** The result's review facts as `reviewRetryStateOf` records them, plus
58
63
  * the one liveness fact the words depend on. */
@@ -19,7 +19,7 @@
19
19
  import { ACCEPT_NEEDS_REASON, FAILED_CHECK, cantAcceptYetOf, evidenceProblemOf } from "./result-acts.js";
20
20
  export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf };
21
21
  import { manualReviewOnly, plainReasonWords } from "./proof.js";
22
- import { OPEN_RESULT, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
22
+ import { CHECK_COMMAND_CHANGED, CHECK_UNREADABLE, OPEN_RESULT, approvedCheckOf, approvedCheckStateOf, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
23
23
  import { whenUtc } from "./when-html.js";
24
24
  import { html, postForm } from "./html.js";
25
25
  export const WORK_VIEWS = [
@@ -122,9 +122,15 @@ export function resultStatusOf(result, publication = null) {
122
122
  return (result?.role === "builder" ? reviewStatusOf(result.review ?? null) : null) ?? storedResultStatusOf(result, publication);
123
123
  }
124
124
  /** What the machine itself recorded about a result, in a clause. */
125
- function machineVerdictWords(result) {
126
- if (result.verdict === "verified")
125
+ function machineVerdictWords(result, check) {
126
+ if (check !== undefined && check?.status === "failed" && evidenceProblemOf(result.verdict, result.reasons) !== "checks-failed") {
127
+ return `the approved check failed against it${check.exitCode === null ? "" : ` (exit ${check.exitCode})`}`;
128
+ }
129
+ if (result.verdict === "verified" && passedNow(check))
127
130
  return "it verified the result before the acceptance";
131
+ // Unread here, or unreadable now: never restated as a pass.
132
+ if (result.verdict === "verified")
133
+ return check === undefined ? "open the result for its checks" : "its check result couldn't be read";
128
134
  if (result.verdict === "attested")
129
135
  return "the checks on record are the agent's own report";
130
136
  const problem = evidenceProblemOf(result.verdict, result.reasons);
@@ -136,6 +142,14 @@ function machineVerdictWords(result) {
136
142
  return "its evidence did not match the sealed record";
137
143
  return "its required evidence was missing";
138
144
  }
145
+ /** The approved check passed (task-status.ts approvedCheckStateOf): its own pass, or checks Off covered by a passing
146
+ * release check. A re-run underway keeps the pass it had. */
147
+ const passedNow = (check) => {
148
+ const state = approvedCheckStateOf(check);
149
+ return state === "passed" || (state === "running" && check?.status === "passed");
150
+ };
151
+ /** A result whose check this surface didn't read: no pass is claimed, the result page says. */
152
+ const UNREAD_CHECK = "Open the result to see its checks.";
139
153
  function storedResultStatusOf(result, publication) {
140
154
  if (result === null || result.runId === null) {
141
155
  return {
@@ -151,6 +165,8 @@ function storedResultStatusOf(result, publication) {
151
165
  }
152
166
  const published = publicationStatusOf(publication);
153
167
  const withPublication = (detail) => (published === null ? detail : `${detail} ${published.detail}`);
168
+ // Did the approved check pass for this result (task-status.ts): the one answer the Checks row and the road read.
169
+ const check = result.checks === undefined ? undefined : approvedCheckOf(result.verdict, result.reasons, result.checks);
154
170
  const humanReview = manualReviewOnly({ verdict: result.verdict ?? "", reasons: result.reasons });
155
171
  if (result.accepted && humanReview) {
156
172
  return { token: "accepted-exception", label: "Accepted by a person", detail: withPublication("A person accepted this result. The recorded checks are unchanged."), tone: "done", action: { label: "Open the acceptance", kind: "open-review" } };
@@ -162,14 +178,18 @@ function storedResultStatusOf(result, publication) {
162
178
  // Acceptance records a person's decision; it neither proves nor
163
179
  // disproves anything about the checks. The machine's own verdict is
164
180
  // restated as recorded.
165
- detail: withPublication(`An approver accepted this result by hand, and the recorded exception says why. The machine's verdict is unchanged: ${machineVerdictWords(result)}.`),
181
+ detail: withPublication(`An approver accepted this result by hand, and the recorded exception says why. The machine's verdict is unchanged: ${machineVerdictWords(result, check)}.`),
166
182
  tone: "done",
167
183
  action: { label: "Open the recorded exception", kind: "open-review" },
168
184
  };
169
185
  }
170
- const problem = evidenceProblemOf(result.verdict, result.reasons);
186
+ const recordedProblem = evidenceProblemOf(result.verdict, result.reasons);
187
+ // A failed check that a later check on the same commit passed no longer fails the result; a check that failed
188
+ // since a passing verdict does.
189
+ const problem = recordedProblem === "checks-failed" && passedNow(check) ? "none"
190
+ : recordedProblem !== "mismatched" && check?.status === "failed" ? "checks-failed" : recordedProblem;
171
191
  if (problem === "checks-failed") {
172
- const exit = failedCheckExit(result.reasons);
192
+ const exit = check?.status === "failed" ? check.exitCode : failedCheckExit(result.reasons);
173
193
  return {
174
194
  token: "checks-failed",
175
195
  label: "Changes saved, but checks failed",
@@ -218,7 +238,14 @@ function storedResultStatusOf(result, publication) {
218
238
  action: { label: "See what is missing", kind: "open-review" },
219
239
  };
220
240
  }
221
- if (result.verdict === "attested") {
241
+ // Any verdict short of a pass the check confirms: never "passed" when the check can't be read now.
242
+ if (check !== undefined && !passedNow(check) && result.verdict === "verified") {
243
+ return check?.status === "running"
244
+ ? { token: "checks-running", label: "Checks running", detail: withPublication("The approved check is running on this result."), tone: "live", action: { label: OPEN_RESULT, kind: "open-result" } }
245
+ : { token: "check-unreadable", label: "Result saved — check result couldn't be read", detail: withPublication(`${check?.cause === "command-changed" ? CHECK_COMMAND_CHANGED : CHECK_UNREADABLE} This result isn't verified.`),
246
+ tone: "problem", action: { label: "Open the check", kind: "open-review" } };
247
+ }
248
+ if (result.verdict === "attested" && !passedNow(check)) {
222
249
  return {
223
250
  token: "agent-attested",
224
251
  label: "Result saved — checks reported by the agent",
@@ -231,7 +258,7 @@ function storedResultStatusOf(result, publication) {
231
258
  return {
232
259
  token: published.token,
233
260
  label: published.label,
234
- detail: `The approved check passed against this result. ${published.detail}`,
261
+ detail: check === undefined ? `${UNREAD_CHECK} ${published.detail}` : `The approved check passed against this result. ${published.detail}`,
235
262
  tone: published.token === "pr-closed" ? "muted" : "done",
236
263
  action: published.token === "merge-observed" ? { label: OPEN_RESULT, kind: "open-result" } : { label: "Open the pull request", kind: "open-pr" },
237
264
  };
@@ -239,7 +266,8 @@ function storedResultStatusOf(result, publication) {
239
266
  return {
240
267
  token: "ready-to-review",
241
268
  label: "Ready",
242
- detail: withPublication("The approved check passed against this result."),
269
+ // Only a check read here may be called passed; a surface that read none says where to see it.
270
+ detail: withPublication(check === undefined ? UNREAD_CHECK : "The approved check passed against this result."),
243
271
  tone: "ready",
244
272
  action: { label: OPEN_RESULT, kind: "open-result" },
245
273
  };
@@ -301,7 +329,7 @@ export function dispatchActionLabel(dispatch) {
301
329
  }
302
330
  /** Result tokens a done task can wear, as the shared stage they mean. A
303
331
  * result that is missing its record waits on a person; a failed check fails. */
304
- const RESULT_NEEDS_YOU = new Set(["no-build-record", "verification-needed", "evidence-mismatch", "record-incomplete", "evidence-damaged"]);
332
+ const RESULT_NEEDS_YOU = new Set(["no-build-record", "verification-needed", "evidence-mismatch", "record-incomplete", "evidence-damaged", "check-unreadable"]);
305
333
  /** Result tokens that only follow a passing project check. */
306
334
  const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed", "pr-closed"]);
307
335
  /** A finished result's display status in the shared headline's words: a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.74",
3
+ "version": "0.9.75",
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"