toolroll 0.9.56 → 0.9.57

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.57 — 2026-10-09
4
+
5
+ - **Truthful status.** Finished tasks read Complete everywhere. With checks
6
+ off, criteria read "checked at release" instead of Needs you.
7
+ - **No false finish.** Completing or deploying refuses a failed or missing
8
+ check, an unmet criterion, a serious review finding, or missing evidence.
9
+ - **Clear revisions.** Revisions are named after their task: -v2, -v3.
10
+
3
11
  ## 0.9.56 — 2026-10-09
4
12
 
5
13
  - **One lead.** Your assistant in the console, on your phone, in chat and in
@@ -109,6 +109,11 @@ export declare function assignmentForCoordinator(store: Store, token: string, op
109
109
  deployment: {
110
110
  status: "not-recorded";
111
111
  };
112
+ readiness: {
113
+ verdict: import("./proof.js").ProofVerdict | null;
114
+ checksOff: boolean;
115
+ blockers: import("./task-status.js").CompletionBlocker[];
116
+ } | null;
112
117
  };
113
118
  }[];
114
119
  nextCursor: number;
@@ -1,10 +1,12 @@
1
1
  import { type Store, type ProofVerdictRow, type ProofAcceptanceRow } from "./store.js";
2
2
  import { type WorkAction, type WorkSummaryAccess } from "./work-summary.js";
3
3
  import { type VerifiedApprover } from "./principal.js";
4
+ import { type ProofVerdict } from "./proof.js";
4
5
  import { type CheckLevel } from "./check-levels.js";
5
6
  import { type BuildReviewView } from "./review-switch.js";
6
7
  import { type NeedKey, type WaitKey } from "./needs-you.js";
7
- import { type ChecksBatch } from "./task-status.js";
8
+ import { type ChecksBatch, type CompletionBlocker } from "./task-status.js";
9
+ import { type ReleaseCoverage, type ReleaseMemo } from "./release-coverage.js";
8
10
  import { type LeadClaim } from "./lead-voice.js";
9
11
  export type AssignmentAccess = WorkSummaryAccess;
10
12
  export type AssignmentOwner = {
@@ -24,6 +26,8 @@ export type AssignmentChecks = {
24
26
  running?: "quick" | "full" | null;
25
27
  /** A batch check (batch-checks.ts): waiting for its batch, or how it was checked. */
26
28
  batch?: ChecksBatch | null;
29
+ /** Checks Off: the passing full release check that contains this commit (release-coverage.ts), read now, never sealed. */
30
+ release?: ReleaseCoverage | null;
27
31
  };
28
32
  export type AssignmentReceipt = {
29
33
  digest: string;
@@ -118,6 +122,13 @@ export type AssignmentSnapshot = {
118
122
  earlierActive?: number;
119
123
  /** Of those, how many are running rather than only queued; present with earlierActive. */
120
124
  earlierRunning?: number;
125
+ /** The result as it reads today (task-status.ts): the verdict it shows, whether its checks were deliberately Off, and
126
+ * what refuses Complete. Derived at read time; never part of the sealed receipt. Null without a result. */
127
+ readiness?: {
128
+ verdict: ProofVerdict | null;
129
+ checksOff: boolean;
130
+ blockers: CompletionBlocker[];
131
+ } | null;
121
132
  };
122
133
  /** Status-first handoff for routine reads. Fetch get_assignment only when
123
134
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
@@ -169,9 +180,14 @@ export declare function assignmentBrief(assignment: AssignmentSnapshot | null):
169
180
  deployment: {
170
181
  status: "not-recorded";
171
182
  };
183
+ readiness: {
184
+ verdict: ProofVerdict | null;
185
+ checksOff: boolean;
186
+ blockers: CompletionBlocker[];
187
+ } | null;
172
188
  } | null;
173
189
  export declare function assignmentChecksForRun(store: Store, root: string | undefined, runId: number, now: Date): AssignmentChecks;
174
- export declare function assignmentOf(store: Store, taskId: string, now: Date, access: AssignmentAccess, root?: string): AssignmentSnapshot | null;
190
+ export declare function assignmentOf(store: Store, taskId: string, now: Date, access: AssignmentAccess, root?: string, memo?: ReleaseMemo): AssignmentSnapshot | null;
175
191
  type MutationResult = {
176
192
  ok: true;
177
193
  assignment: AssignmentSnapshot;
@@ -203,7 +219,7 @@ type Finished = {
203
219
  * (no reason needed), or an exception a report that doesn't match its changes needs a reason for. Null when
204
220
  * Accept and finish is a plain completion: nothing is owed (no proof, a failed check, or a verified result),
205
221
  * or it was already accepted. */
206
- export declare function owedAcceptance(receipt: AssignmentReceipt | null): "person-check" | "exception" | null;
222
+ export declare function owedAcceptance(receipt: AssignmentReceipt | null, blockers?: readonly CompletionBlocker[]): "person-check" | "exception" | null;
207
223
  /** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
208
224
  * transaction. Whether an acceptance is owed is decided here, against the receipt as it stands: only a
209
225
  * person's own check or a reasoned exception records one (with its ledger act); an exception without its
@@ -278,6 +294,11 @@ export declare function assignmentUpdates(store: Store, now: Date, access: Assig
278
294
  deployment: {
279
295
  status: "not-recorded";
280
296
  };
297
+ readiness: {
298
+ verdict: ProofVerdict | null;
299
+ checksOff: boolean;
300
+ blockers: CompletionBlocker[];
301
+ } | null;
281
302
  };
282
303
  }[];
283
304
  nextCursor: number;
@@ -14,13 +14,14 @@ import { verificationEvidence } from "./verification-evidence.js";
14
14
  import { reproveApprover } from "./principal.js";
15
15
  import { noteAssignmentStatus } from "./assignment-status.js";
16
16
  import { historicalAssessmentReason } from "./assignment-presentation.js";
17
- import { manualReviewOnly } from "./proof.js";
17
+ import { manualReviewOnly, shownVerdictOf } from "./proof.js";
18
18
  import { ACCEPT_NEEDS_REASON, cantAcceptYetOf } from "./result-acts.js";
19
- import { runCheckLevel } from "./check-levels.js";
19
+ import { requiredCheckCommandFor, runCheckLevel } from "./check-levels.js";
20
20
  import { followUpChecksOf, withFollowUps } from "./result-follow-ups.js";
21
21
  import { buildReviewOf, findingWords } from "./review-switch.js";
22
22
  import { NEEDS, WAITS, processNeedOf, resultHoldUpSentence } from "./needs-you.js";
23
- import { assignmentStageOf } from "./task-status.js";
23
+ import { assignmentStageOf, checkBackingOf, completionBlockersOf, requirementWordOf } from "./task-status.js";
24
+ import { releaseCoverageOf } from "./release-coverage.js";
24
25
  import { leadClaimOf } from "./lead-voice.js";
25
26
  /** Status-first handoff for routine reads. Fetch get_assignment only when
26
27
  * inspecting its exact evidence; do not repeat a full proof in every poll. */
@@ -35,7 +36,7 @@ export function assignmentBrief(assignment) {
35
36
  base: receipt.base, head: receipt.head, completionKind: receipt.completionKind,
36
37
  checks: receipt.checks, proofAcceptance: receipt.proofAcceptance, verdict: receipt.proof?.verdict ?? null,
37
38
  criteria: { passed: receipt.proof?.matrix.filter(row => row.state === "pass").length ?? 0, total: receipt.proof?.matrix.length ?? 0 }, evidence: receipt.evidence },
38
- publication: assignment.publication, review: assignment.review, deployment: assignment.deployment };
39
+ publication: assignment.publication, review: assignment.review, deployment: assignment.deployment, readiness: assignment.readiness ?? null };
39
40
  }
40
41
  const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
41
42
  /** A lookup added with lead-quiet, read safely: a deploy proves completion with this code over the INSTALLED runtime's
@@ -51,7 +52,7 @@ function olderStoreSafe(read, absent) {
51
52
  }
52
53
  }
53
54
  /** A check as the receipt digest seals it: presentation-only fields (check-levels.ts) left out. */
54
- const sealedChecks = ({ level: _level, running: _running, batch: _batch, ...proof }) => proof;
55
+ const sealedChecks = ({ level: _level, running: _running, batch: _batch, release: _release, ...proof }) => proof;
55
56
  const OWNER_ACTION = "assignment claimed";
56
57
  const CHECK_ACTION = COMPLETION_ACTION;
57
58
  const actorOf = (owner) => `${owner.kind}:${owner.id}`;
@@ -127,7 +128,7 @@ function buildChecks(store, root, runId) {
127
128
  return unavailable("Saved checks could not be read.");
128
129
  }
129
130
  }
130
- export function assignmentOf(store, taskId, now, access, root) {
131
+ export function assignmentOf(store, taskId, now, access, root, memo) {
131
132
  const family = store.taskFamilyOf(taskId, access.repos, access.principal === "operator" && access.includeUnplaced === true);
132
133
  if (family === null)
133
134
  return null;
@@ -163,7 +164,10 @@ export function assignmentOf(store, taskId, now, access, root) {
163
164
  const reports = artifacts.filter(a => a.kind === "report");
164
165
  const unavailable = family.versions.flatMap(version => store.runsFor(version.refId).flatMap(run => store.artifactsFor(run.id).filter(artifact => root === undefined || !readVerifiedArtifact(root, artifact).ok)
165
166
  .map(artifact => `Saved ${artifact.kind} #${artifact.id} (run ${run.id}) is unavailable or changed.`)));
166
- const checks = result === null ? null : assignmentChecksForRun(store, root, result.id, now);
167
+ const ownChecks = result === null ? null : assignmentChecksForRun(store, root, result.id, now);
168
+ // Checks Off: a passing full release check that contains this commit covers it, read now from the existing receipts.
169
+ const checks = ownChecks === null || ownChecks.level !== "off" || ownChecks.status === "passed" || ownChecks.status === "failed" ? ownChecks
170
+ : { ...ownChecks, release: releaseCoverageOf(store, root, current.repo, result.headRevision, memo === undefined ? {} : { memo }) };
167
171
  const finishedBuild = result?.role === "builder" && (result.outcome === "built" || result.outcome === "no-change") &&
168
172
  result.finishedAt !== null && /^[a-f0-9]{40}$/.test(result.headRevision ?? "");
169
173
  // Finished work returns to its lead or user. Strict terms and previous model
@@ -339,6 +343,7 @@ export function assignmentOf(store, taskId, now, access, root) {
339
343
  attention, receipt: receipt?.digest ?? null }), acknowledged: false } : null;
340
344
  const publication = result === null ? null : store.publicationForRun(result.id);
341
345
  const lead = state === "complete" || state === "cancelled" || access.principal !== "operator" ? null : leadClaimOf(store, family.root.id, now, access.viewer);
346
+ const readiness = receipt === null ? null : readinessOf(store, receipt, current.repo, review);
342
347
  // Existing saved inputs and output, read only after family admission. Keep
343
348
  // polling briefs small; full reads disclose exactly which excerpts are shortened.
344
349
  const planId = result?.planRevision == null ? null : store.getPlanRevision(result.planRevision)?.artifact;
@@ -355,7 +360,21 @@ export function assignmentOf(store, taskId, now, access, root) {
355
360
  return { version: 1, rootId: family.root.id, activeTaskId: current.id, repo: current.repo, title: family.root.title,
356
361
  state, detail, primaryAction, attention: [...new Set(attention)], attempts, owner, receipt, savedContext, completion, handoff,
357
362
  publication: publication === null ? null : { state: publication.state, prUrl: publication.prUrl, remoteState: publication.remoteState },
358
- review, deployment: { status: "not-recorded" }, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }), ...(earlierActive.length === 0 ? {} : { earlierActive: earlierActive.length, earlierRunning }) };
363
+ review, deployment: { status: "not-recorded" }, readiness, ...(need === null ? {} : { need }), ...(lead === null ? {} : { lead }), ...(earlierActive.length === 0 ? {} : { earlierActive: earlierActive.length, earlierRunning }) };
364
+ }
365
+ /** The result's shown verdict and completion blockers (task-status.ts), from the receipt, the project's approved check
366
+ * and the automatic review. A HIGH finding a revision already took is resolved there. */
367
+ function readinessOf(store, receipt, repo, review) {
368
+ const report = receipt.completionKind === "research-report";
369
+ const high = review === null || review.sentBackAs !== null || review.state !== "reviewed" ? 0 : review.high.length;
370
+ const checkRequired = olderStoreSafe(() => requiredCheckCommandFor(store, repo, receipt.taskId, receipt.checks.level), null) !== null;
371
+ const matrix = receipt.proof?.matrix ?? [];
372
+ const backing = checkBackingOf(receipt.checks);
373
+ const verdict = shownVerdictOf(receipt.proof?.verdict ?? null, { checkFailed: !report && receipt.checks.status === "failed", highFindings: high,
374
+ reviewPending: review?.state === "pending", criteriaPassed: matrix.every(row => requirementWordOf(row, null, backing) === "Met") });
375
+ const blockers = completionBlockersOf({ report, checks: receipt.checks, checkRequired, matrix, verdict: receipt.proof?.verdict ?? null,
376
+ accepted: receipt.proofAcceptance !== null, high });
377
+ return { verdict, checksOff: !report && receipt.checks.level === "off", blockers };
359
378
  }
360
379
  function admittedOwner(store, taskId, owner, now, root) {
361
380
  if (owner.kind !== "coordinator")
@@ -417,6 +436,10 @@ function acknowledgeCurrent(store, current, receiptDigest, actor, now, root, acc
417
436
  if (current.state !== "ready-to-check" && current.state !== "complete")
418
437
  return { ok: false, reason: "not-ready", message: "This assignment still has unresolved execution, scope or decisions." };
419
438
  if (current.state !== "complete") {
439
+ // The one readiness guard: a failed or missing required check, an unresolved requirement or HIGH finding refuses.
440
+ const blockers = current.readiness?.blockers ?? [];
441
+ if (blockers.length > 0)
442
+ return { ok: false, reason: "not-ready", message: blockers.map(one => one.message).join(" ") };
420
443
  // v102: declared paths are only a promise. A result whose actual diff reaches protected files, on a
421
444
  // scope one person approved, completes only by someone else — two people have then seen the work.
422
445
  const problem = store.protectedResultProblem(receipt.taskId, familyChangedFiles(store, receipt.taskId, receipt.runId, root), actor.startsWith("operator:") ? actor.slice("operator:".length) : null);
@@ -476,11 +499,14 @@ class Undone extends Error {
476
499
  * (no reason needed), or an exception a report that doesn't match its changes needs a reason for. Null when
477
500
  * Accept and finish is a plain completion: nothing is owed (no proof, a failed check, or a verified result),
478
501
  * or it was already accepted. */
479
- export function owedAcceptance(receipt) {
502
+ export function owedAcceptance(receipt, blockers = []) {
480
503
  if (receipt === null || receipt.proofAcceptance !== null)
481
504
  return null;
482
505
  if (personCheckPending(receipt))
483
506
  return "person-check";
507
+ // An unresolved requirement is resolved only by an acceptance that says why.
508
+ if (blockers.some(one => one.key === "criteria"))
509
+ return "exception";
484
510
  return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? "exception" : null;
485
511
  }
486
512
  /** Accept and finish: the person's acceptance of the exact result they read and its completion, in one
@@ -505,7 +531,7 @@ export function acceptAndCompleteAsOperator(store, taskId, input, who, now, root
505
531
  return { ok: false, reason: "stale", message: "This result changed. Open the current result before accepting it." };
506
532
  }
507
533
  // Already complete, or nothing owed: the plain completion of the receipt as read.
508
- const owed = before.state === "ready-to-check" ? owedAcceptance(receipt) : null;
534
+ const owed = before.state === "ready-to-check" ? owedAcceptance(receipt, before.readiness?.blockers ?? []) : null;
509
535
  // An exception is accepted only with its reason: never a completion that skips it.
510
536
  if (owed === "exception" && input.note === null)
511
537
  return { ok: false, reason: "needs-reason", message: `${ACCEPT_NEEDS_REASON}.` };
@@ -19,7 +19,7 @@ export function browserCrewOf(store, now, access, options = {}) {
19
19
  return { crew: [], crewTruncated: false };
20
20
  const limit = Number.isFinite(options.limit)
21
21
  ? Math.max(1, Math.min(BROWSER_CREW_LIMIT, Math.floor(options.limit))) : BROWSER_CREW_LIMIT;
22
- const page = workIndexPage(store, now, access, { limit, project });
22
+ const page = workIndexPage(store, now, access, { limit, project, ...(options.evidenceRoot === undefined ? {} : { root: options.evidenceRoot }) });
23
23
  return browserCrewFromIndex(page);
24
24
  }
25
25
  /** Navigation input must already be admitted by the caller. No extra project
@@ -232,9 +232,9 @@ export async function processChatEvent(options) {
232
232
  if (command.kind === "help")
233
233
  state.plan(event.id, [{ text: PHONE_HELP }], now_);
234
234
  else if (command.kind === "status")
235
- state.plan(event.id, [{ text: phoneStatus(store, repos, now_, focusedTitle) }], now_);
235
+ state.plan(event.id, [{ text: phoneStatus(store, repos, now_, focusedTitle, options.evidenceRoot) }], now_);
236
236
  else if (command.kind === "tasks")
237
- state.plan(event.id, [{ text: phoneTaskListText(phoneTaskChoices(store, repos, now_), focusedTitle) }], now_);
237
+ state.plan(event.id, [{ text: phoneTaskListText(phoneTaskChoices(store, repos, now_, null, 8, options.evidenceRoot), focusedTitle) }], now_);
238
238
  else if (command.kind === "lead") {
239
239
  store.setChatFocus(surface, binding.id, null, now_);
240
240
  state.plan(event.id, [{ text: PHONE_BACK_TO_LEAD }], now_);
@@ -243,7 +243,7 @@ export async function processChatEvent(options) {
243
243
  const pick = resolvePhoneTask(store, repos, now_, command.id);
244
244
  if (pick.kind === "one") {
245
245
  store.setChatFocus(surface, binding.id, pick.id, now_);
246
- const view = phoneTaskView(store, repos, pick.view, now_);
246
+ const view = phoneTaskView(store, repos, pick.view, now_, options.evidenceRoot);
247
247
  // The status names its task (and result): a reply to it is about that task.
248
248
  state.plan(event.id, [{ text: phoneFocusText(view.text), ...(view.link === null ? {} : { link: view.link }), task: pick.view, ...(view.run === null ? {} : { run: view.run }) }], now_);
249
249
  }
@@ -69,7 +69,7 @@ export function finishedLine(fact) {
69
69
  if (fact.checks === "passed")
70
70
  return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept and finish it?"}`;
71
71
  if (fact.checks === "off")
72
- return `${ready}. Checks are off for this project, so look it over first.`;
72
+ return `${ready}. Its checks run at release, so look it over first.`;
73
73
  return `${ready}, but no tests ran. Look it over first.`;
74
74
  case "Failed":
75
75
  if (fact.leadOnIt === true)
@@ -52,6 +52,9 @@ export declare function checkCommandFor(store: Store, repo: string, level: Check
52
52
  level: CheckLevel;
53
53
  command: VerifyCommand | null;
54
54
  };
55
+ /** Readiness uses the result's level, falling back to the task/project for legacy
56
+ * runs. Quick's approved command (or its Full fallback) is required; Off is not. */
57
+ export declare function requiredCheckCommandFor(store: Store, repo: string | null, taskId: string, level: CheckLevel | null | undefined): VerifyCommand | null;
55
58
  /** The builder records which level a run used, so its result says it honestly. */
56
59
  export declare function recordRunCheckLevel(store: Store, run: {
57
60
  id: number;
@@ -118,6 +118,11 @@ export function checkCommandFor(store, repo, level) {
118
118
  }
119
119
  return { level: "full", command: store.liveVerifyCommand(repo) };
120
120
  }
121
+ /** Readiness uses the result's level, falling back to the task/project for legacy
122
+ * runs. Quick's approved command (or its Full fallback) is required; Off is not. */
123
+ export function requiredCheckCommandFor(store, repo, taskId, level) {
124
+ return repo === null ? null : checkCommandFor(store, repo, level ?? effectiveCheckLevel(store, repo, taskId).level).command;
125
+ }
121
126
  /** The builder records which level a run used, so its result says it honestly. */
122
127
  export function recordRunCheckLevel(store, run, level, from, now) {
123
128
  store.recordAction({ at: now.toISOString(), actor: "system", repo: run.repo, taskId: run.taskId, runId: run.id, action: RUN_LEVEL_ACTION, outcome: level, source: "work",
package/dist/dispose.js CHANGED
@@ -707,10 +707,6 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
707
707
  };
708
708
  const briefBytes = Buffer.from(JSON.stringify(draftBrief, null, 2), "utf8");
709
709
  const key = writeEvidenceFile(evidenceRoot, sourceRunId, `repair-brief-${sourceRunId}-${attempt}.json`, briefBytes);
710
- // Suffixes survive truncation (the CI-repair rule, verbatim): the prefix
711
- // gives way, the identity-bearing tail never does.
712
- const suffix = observation ? `-evidence-${attempt}` : `-fix-${attempt}`;
713
- const draftId = `${rootTask.slice(0, 64 - suffix.length)}${suffix}`;
714
710
  // The draft files through the ONE revision boundary: the source's goal,
715
711
  // exclusions, touches, rubric, risk, quality, posture, budget, overrides
716
712
  // and pins are read from the SOURCE ROWS inside the seal — the scope
@@ -719,7 +715,6 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
719
715
  source: { task: ref.externalId, run: sourceRunId, scopeDigest: scope.digest },
720
716
  brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored repair brief (exit 0)" },
721
717
  child: {
722
- id: draftId,
723
718
  title: observation ? `Collect missing evidence for ${ref.externalId}` : verification === undefined ? `repair ${ref.externalId}: ${unresolved.length} criteri${unresolved.length === 1 ? "on" : "a"} unmet` : `Fix failed checks for ${ref.externalId}`,
724
719
  repair: observation ? `Collect only the missing observations for ${unresolved.join(", ")}. Keep the saved candidate unchanged. The machine runs supported focused test observations and reuses its intact passing gate; a fresh review assesses the added evidence.` : verification === undefined ? `repair exactly the unmet criteria named below; a comment cannot widen the scope. Unmet: ${unresolved.join(", ")}.` : "Diagnose the saved failed project check and repair within the original scope. Preserve the acceptance criteria and verification command; the revision brief binds the exact failure evidence.",
725
720
  },
@@ -105,7 +105,7 @@ export declare function repairStaleStatuses(store: Store, now: Date): {
105
105
  * `repos`: the only projects to report (a remote person limited to some projects); null is the whole installation.
106
106
  * A limited report counts only those projects' tasks and leaves out what belongs to the installation as a whole:
107
107
  * the release check, plan windows and the lead line are empty, never another project's. */
108
- export declare function installationStatus(store: Store, now: Date, viewer?: string | null, repos?: readonly string[] | null): InstallationStatus;
108
+ export declare function installationStatus(store: Store, now: Date, viewer?: string | null, repos?: readonly string[] | null, root?: string): InstallationStatus;
109
109
  /** `limited`: a report over some projects (installationStatus's `repos`), which has no release check or plan windows to state. */
110
110
  export declare function renderInstallationStatus(status: InstallationStatus, limited?: boolean): string[];
111
111
  export declare function renderTaskWait(snapshot: TaskWaitSnapshot, outcome?: string): string;
@@ -183,7 +183,7 @@ export function repairStaleStatuses(store, now) {
183
183
  * `repos`: the only projects to report (a remote person limited to some projects); null is the whole installation.
184
184
  * A limited report counts only those projects' tasks and leaves out what belongs to the installation as a whole:
185
185
  * the release check, plan windows and the lead line are empty, never another project's. */
186
- export function installationStatus(store, now, viewer = currentActor()?.account ?? null, repos = null) {
186
+ export function installationStatus(store, now, viewer = currentActor()?.account ?? null, repos = null, root) {
187
187
  const admitted = (row) => repos === null || typeof row["repo"] === "string" && repos.includes(row["repo"]);
188
188
  const access = { principal: "operator", repos, includeUnplaced: repos === null };
189
189
  const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.id AS task_ref, ref.external_id AS task, ref.repo
@@ -262,7 +262,7 @@ export function installationStatus(store, now, viewer = currentActor()?.account
262
262
  observedAt: String(row["observed_at"]),
263
263
  })),
264
264
  signIn: pauses.map(one => ({ provider: one.provider, reason: signInReason(one), command: signInCommand(one), since: one.openedAt })),
265
- tasks: workIndexPage(store, now, { ...access, viewer }, { limit: 8 }).items
265
+ tasks: workIndexPage(store, now, { ...access, viewer }, { limit: 8, ...(root === undefined ? {} : { root }) }).items
266
266
  .map(one => ({ task: one.rootId, title: one.title, headline: one.status.label, sentence: one.status.detail })),
267
267
  lead: repos !== null ? null : (() => {
268
268
  // Only the asker's own lead: another person's lead is never "Your lead".
package/dist/operate.js CHANGED
@@ -179,7 +179,7 @@ import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFr
179
179
  import { WorktreePool } from "./worktree.js";
180
180
  import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
181
181
  import { worktreeAdoptionNotice } from "./worktree-notices.js";
182
- import { workIndexPage, WorkIndexCursorError } from "./work-index.js";
182
+ import { workIndexPage, workIndexTask, WorkIndexCursorError } from "./work-index.js";
183
183
  import { readExecutorOf } from "./read-executor.js";
184
184
  import { parseWorkView } from "./workspace-ui.js";
185
185
  import { taskWorkSummaryOf } from "./work-summary.js";
@@ -8307,10 +8307,10 @@ async function statusCommand(positional, flags, context) {
8307
8307
  // that belongs to the installation or other projects (updates, integrations, builds by project, unsent replies).
8308
8308
  const lens = context.principal?.lens ?? null;
8309
8309
  if (lens !== null) {
8310
- const status = installationStatus(context.store, context.clock(), viewer, lens);
8310
+ const status = installationStatus(context.store, context.clock(), viewer, lens, context.evidenceRoot);
8311
8311
  return succeed(context.write, context.json, command, { ...status, projects: [] }, () => renderInstallationStatus(status, true));
8312
8312
  }
8313
- const status = installationStatus(context.store, context.clock(), viewer);
8313
+ const status = installationStatus(context.store, context.clock(), viewer, null, context.evidenceRoot);
8314
8314
  // One line, only when a newer Toolroll exists; offline or switched off says nothing.
8315
8315
  const current = PACKAGE_VERSION;
8316
8316
  const method = installMethod(context.installBin);
@@ -8927,6 +8927,7 @@ function listTasks(flags, context) {
8927
8927
  view: parseWorkView(rawView ?? null), limit, cursor: text(flags, 'cursor') ?? null,
8928
8928
  ...(wanted === undefined ? {} : { state: wanted }),
8929
8929
  ...(text(flags, 'repo') === undefined ? {} : { project: text(flags, 'repo') }),
8930
+ root: context.evidenceRoot,
8930
8931
  };
8931
8932
  const refused = (error) => {
8932
8933
  if (!(error instanceof WorkIndexCursorError))
@@ -8990,10 +8991,18 @@ function showTask(positional, context) {
8990
8991
  const repo = store.lookupRef(blocker)?.repo;
8991
8992
  return repo != null && lens.includes(repo);
8992
8993
  });
8994
+ const work = taskWorkSummaryOf(store, id, now, access);
8995
+ const current = assignmentOf(store, id, now, access, context.evidenceRoot);
8996
+ // Read the same saved facts and statusOf path as task list. File health remains
8997
+ // in the assignment diagnostics and action-time guards; it cannot rewrite history.
8998
+ const listed = workIndexTask(store, id, now, access, context.evidenceRoot);
8999
+ const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
9000
+ const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
8993
9001
  const detail = {
8994
9002
  task,
8995
- work: taskWorkSummaryOf(store, id, now, access),
8996
- assignment: assignmentBrief(assignmentOf(store, id, now, access, context.evidenceRoot)),
9003
+ work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
9004
+ status: shared === null ? null : { headline: shared.headline, sentence: shared.sentence },
9005
+ assignment: assignmentBrief(current),
8997
9006
  ref: ref.id,
8998
9007
  // One phrase for every inaccessible blocker, so even their number stays private.
8999
9008
  blockedBy: visibleBlockers.length === blockers.length ? visibleBlockers : [...visibleBlockers, "a task in another project"],
@@ -9033,8 +9042,8 @@ function showTask(positional, context) {
9033
9042
  `${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
9034
9043
  ` ${task.title}`,
9035
9044
  ...(detail.work === null ? [] : [
9036
- ` status: ${detail.work.status.label}`,
9037
- ...(detail.work.primaryAction === null ? [] : [` next: ${detail.work.primaryAction.label}${detail.work.primaryAction.target.decisionId === null ? "" : ` — decision #${detail.work.primaryAction.target.decisionId}`}`]),
9045
+ ` status: ${shared?.headline ?? detail.work.status.label}${shared === null ? "" : ` — ${shared.sentence}`}`,
9046
+ ...(primaryAction === null ? [] : [` next: ${primaryAction.label}${primaryAction.target.decisionId === null ? "" : ` — decision #${primaryAction.target.decisionId}`}`]),
9038
9047
  ]),
9039
9048
  // The closed machine-authored verdict (Priority 2), computed once at
9040
9049
  // completion — never re-derived here. Same words `verdictWords`
@@ -9042,7 +9051,7 @@ function showTask(positional, context) {
9042
9051
  ...(task.state !== "done" || detail.proofVerdict === null
9043
9052
  ? []
9044
9053
  : [
9045
- ` proof: ${proofVerdictWords(detail.proofVerdict, detail.proofReasons).word}${detail.proofAccepted ? " (accepted)" : ""}`,
9054
+ ` proof: ${proofVerdictWords(current?.receipt?.runId === latestFinished?.id ? current?.readiness?.verdict ?? detail.proofVerdict : detail.proofVerdict, detail.proofReasons).word}${detail.proofAccepted ? " (accepted)" : ""}`,
9046
9055
  ...(detail.proofReasons.length > 0 ? [` ${detail.proofReasons.join("; ")}`] : []),
9047
9056
  ...matrixWords(detail.proofMatrix),
9048
9057
  ...(detail.semanticCoverage === null ? [] : coverageWords(detail.semanticCoverage).map(line => ` ${line}`)),
package/dist/proof.d.ts CHANGED
@@ -437,6 +437,15 @@ export declare function matrixWords(matrix: readonly CriterionMatrixRow[]): stri
437
437
  /** One criterion's context standing in words — shared by the CLI matrix
438
438
  * and the console badge title so the two never drift. */
439
439
  export declare function coverageStateWords(coverage: CriterionCoverage): string;
440
+ /** The verdict a result shows today, read from the stored one and the facts recorded since, never written back: a
441
+ * failed check or an unresolved HIGH review finding refutes; a pending review, or any acceptance criterion that has
442
+ * not passed, keeps it short. It only ever lowers the stored verdict. */
443
+ export declare function shownVerdictOf(stored: ProofVerdict | null, facts: {
444
+ checkFailed: boolean;
445
+ highFindings: number;
446
+ reviewPending: boolean;
447
+ criteriaPassed: boolean;
448
+ }): ProofVerdict | null;
440
449
  /** Plain words for a verdict, shared by every surface (the `summary.ts`
441
450
  * pattern) so the task page, run page, board, CLI, and chat cannot say
442
451
  * three different things about the same run. */
package/dist/proof.js CHANGED
@@ -976,6 +976,18 @@ export function coverageStateWords(coverage) {
976
976
  return `GAP — inherited context is missing: ${coverage.gaps.join("; ")}${coverage.items.length > 0 ? ` (partial: ${coverage.items.join(", ")})` : ""}${prior}`;
977
977
  }
978
978
  }
979
+ /** The verdict a result shows today, read from the stored one and the facts recorded since, never written back: a
980
+ * failed check or an unresolved HIGH review finding refutes; a pending review, or any acceptance criterion that has
981
+ * not passed, keeps it short. It only ever lowers the stored verdict. */
982
+ export function shownVerdictOf(stored, facts) {
983
+ if (facts.checkFailed || facts.highFindings > 0 || stored === "refuted")
984
+ return "refuted";
985
+ if (stored === null)
986
+ return null;
987
+ if (facts.reviewPending || !facts.criteriaPassed)
988
+ return "short";
989
+ return stored;
990
+ }
979
991
  /** Plain words for a verdict, shared by every surface (the `summary.ts`
980
992
  * pattern) so the task page, run page, board, CLI, and chat cannot say
981
993
  * three different things about the same run. */
@@ -0,0 +1,16 @@
1
+ import type { Store } from "./store.js";
2
+ export type ReleaseCoverage = {
3
+ run: number;
4
+ head: string;
5
+ };
6
+ /** How many of the newest passing release checks one read considers. */
7
+ export declare const RELEASE_SCAN_LIMIT = 20;
8
+ export type IsAncestor = (repo: string, ancestor: string, descendant: string) => boolean | null;
9
+ /** `git merge-base --is-ancestor`: true, false, or null when Git couldn't say. */
10
+ export declare const gitIsAncestor: IsAncestor;
11
+ export type ReleaseMemo = Map<string, ReleaseCoverage[]>;
12
+ /** The release check covering `head` in `repo`, or null. `memo` keeps one read's receipts. */
13
+ export declare function releaseCoverageOf(store: Store, root: string | undefined, repo: string | null, head: string | null, options?: {
14
+ memo?: ReleaseMemo;
15
+ isAncestor?: IsAncestor;
16
+ }): ReleaseCoverage | null;
@@ -0,0 +1,92 @@
1
+ /** Whether a release check already covers a result whose checks were Off: a
2
+ * passing FULL release check (run_check.release) on the same project whose
3
+ * checked commit contains the result's commit. Read at read time from the
4
+ * existing records and Git; nothing is written back, so no proof, receipt,
5
+ * scope digest or schema changes. Fails closed: a receipt that can't be read,
6
+ * a missing commit or any Git error leaves the result uncovered. */
7
+ import { execFileSync } from "node:child_process";
8
+ import { verificationEvidence } from "./verification-evidence.js";
9
+ import { runCheckLevel } from "./check-levels.js";
10
+ /** How many of the newest passing release checks one read considers. */
11
+ export const RELEASE_SCAN_LIMIT = 20;
12
+ const SHA = /^[a-f0-9]{40}$/;
13
+ /** Ancestry between two fixed commits never changes, so a settled answer is kept; errors are not. */
14
+ const ancestry = new Map();
15
+ const ANCESTRY_LIMIT = 4096;
16
+ /** `git merge-base --is-ancestor`: true, false, or null when Git couldn't say. */
17
+ export const gitIsAncestor = (repo, ancestor, descendant) => {
18
+ if (!SHA.test(ancestor) || !SHA.test(descendant))
19
+ return null;
20
+ if (ancestor === descendant)
21
+ return true;
22
+ const key = `${repo}\0${ancestor}\0${descendant}`;
23
+ const known = ancestry.get(key);
24
+ if (known !== undefined)
25
+ return known;
26
+ let answer;
27
+ try {
28
+ execFileSync("git", ["-C", repo, "merge-base", "--is-ancestor", ancestor, descendant], { stdio: "ignore", timeout: 5_000 });
29
+ answer = true;
30
+ }
31
+ catch (error) {
32
+ // Exit 1 is Git's "no"; anything else (a missing object, no repository, a timeout) is no answer.
33
+ answer = error.status === 1 ? false : null;
34
+ }
35
+ if (answer !== null) {
36
+ if (ancestry.size >= ANCESTRY_LIMIT)
37
+ ancestry.clear();
38
+ ancestry.set(key, answer);
39
+ }
40
+ return answer;
41
+ };
42
+ /** The newest passing full release checks of one project, each with its sealed, verified commit. Read once per
43
+ * caller-held memo, so a list of results reads the receipts once. */
44
+ function passingReleases(store, root, repo) {
45
+ let rows;
46
+ try {
47
+ rows = store.handle.prepare(`SELECT rc.run FROM run_check AS rc INDEXED BY run_check_release
48
+ JOIN run ON run.id = rc.run JOIN task_ref AS ref ON ref.id = run.task_ref
49
+ WHERE rc.release = 1 AND rc.status = 'passed' AND rc.exit_code = 0 AND ref.repo = ?
50
+ ORDER BY rc.run DESC LIMIT ${RELEASE_SCAN_LIMIT}`).all(repo);
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ return rows.flatMap(row => {
56
+ const run = Number(row["run"]);
57
+ try {
58
+ // Full only: a quick or Off release check covers nothing.
59
+ const level = runCheckLevel(store, run);
60
+ if (level !== null && level !== "full")
61
+ return [];
62
+ const gate = verificationEvidence(store, root, run);
63
+ if (!gate.ok || gate.bytes === null)
64
+ return [];
65
+ const receipt = JSON.parse(gate.bytes);
66
+ const head = store.getRun(run)?.headRevision ?? null;
67
+ if (receipt.run !== run || receipt.result?.ran !== true || receipt.result.exitCode !== 0)
68
+ return [];
69
+ if (typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:"))
70
+ return [];
71
+ if (typeof receipt.head !== "string" || !SHA.test(receipt.head) || receipt.head !== head)
72
+ return [];
73
+ return [{ run, head: receipt.head }];
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ });
79
+ }
80
+ /** The release check covering `head` in `repo`, or null. `memo` keeps one read's receipts. */
81
+ export function releaseCoverageOf(store, root, repo, head, options = {}) {
82
+ if (root === undefined || repo === null || head === null || !SHA.test(head))
83
+ return null;
84
+ const memo = options.memo ?? new Map();
85
+ let releases = memo.get(repo);
86
+ if (releases === undefined) {
87
+ releases = passingReleases(store, root, repo);
88
+ memo.set(repo, releases);
89
+ }
90
+ const isAncestor = options.isAncestor ?? gitIsAncestor;
91
+ return releases.find(one => isAncestor(repo, head, one.head) === true) ?? null;
92
+ }
@@ -40,7 +40,7 @@ import { refuse, screen } from "./chrome.js";
40
40
  import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
41
41
  import { chatAckPage, chatPage, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
42
42
  import {} from "./render-pages.js";
43
- import { completionForm, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
43
+ import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
44
44
  import { browserCaller, oneLineUa } from "./session.js";
45
45
  export function createChatHandlers(runtime) {
46
46
  const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
@@ -185,7 +185,9 @@ export function createChatHandlers(runtime) {
185
185
  const roomId = url.searchParams.get('conversation');
186
186
  const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
187
187
  // Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
188
- const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id;
188
+ // Never offered where Complete is refused whatever is accepted (a failed check, a HIGH finding).
189
+ const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id &&
190
+ hardBlockerOf(focusTask.assignment) === null;
189
191
  const resultPanel = resultRun === null
190
192
  ? null
191
193
  : html `${resultPanelHtml(resultDetailOf(resultRun, who, now), {
@@ -267,7 +269,7 @@ export function createChatHandlers(runtime) {
267
269
  if (repos.length > 0) {
268
270
  try {
269
271
  fleetSnapshot = store.chatSnapshot(repos, now);
270
- const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100 }).items;
272
+ const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100, root: evidenceRoot }).items;
271
273
  fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
272
274
  fleetSnapshot.attentionCount = needsYouBadge(null);
273
275
  }
@@ -1291,9 +1293,9 @@ export function createChatHandlers(runtime) {
1291
1293
  agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
1292
1294
  activity: runActivityOf(store, run, now) };
1293
1295
  });
1294
- const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT });
1295
- const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT });
1296
- const done = workIndexPage(store, now, access, { view: "completed", limit: WORK_INDEX_MAX_LIMIT });
1296
+ const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
1297
+ const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
1298
+ const done = workIndexPage(store, now, access, { view: "completed", limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot });
1297
1299
  const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
1298
1300
  const counts = [
1299
1301
  { key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },