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.
@@ -12,7 +12,7 @@ import { type BuildReviewView } from "../review-switch.js";
12
12
  import { type AcceptanceCriterion, type approvalOf } from "../scope.js";
13
13
  import { type ReportItem } from "../scout-report.js";
14
14
  import { type Artifact, type DiffComment, type Publication, type RepairChainRow, type ReviewRetryState, type Run, type Store, type TaskState } from "../store.js";
15
- import { type PullRequestFact, type TaskStatus } from "../task-status.js";
15
+ import { type ReleaseState, type PullRequestFact, type TaskStatus } from "../task-status.js";
16
16
  import { type DisplayStatus, type PublicationFacts, type ReviewFacts } from "../workspace-ui.js";
17
17
  import { type Chrome, type Screen } from "./chrome.js";
18
18
  import { type Html } from "../html.js";
@@ -78,6 +78,7 @@ export declare function criterionMatrixHtml(matrix: readonly CriterionMatrixRow[
78
78
  links?: EvidenceLinkMap;
79
79
  fileAnchors?: ReadonlyMap<string, string>;
80
80
  verdict?: string | null;
81
+ release?: ReleaseState | undefined;
81
82
  }): Html;
82
83
  /** One policy summary beneath the requirements. Per-requirement concerns
83
84
  * are already visible in the matrix; do not repeat their full text here. */
@@ -674,6 +675,8 @@ export declare const CHECK_EXCERPT_LINES = 12;
674
675
  * change's first file, labelled as not cited), the screenshots it cites
675
676
  * (none cited: every validated one), and the agent's own note. */
676
677
  export declare function personCheckItems(proof: ProofBundleView, patchText: string | null, shots: readonly ResultScreenshot[], runId: number): BrowserCheckItem[];
678
+ /** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
679
+ export declare function hardBlockerOf(assignment: AssignmentSnapshot | null | undefined): string | null;
677
680
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
678
681
  * stopped behind the password. Null under every other headline. */
679
682
  export declare function needActionOf(assignment: AssignmentSnapshot | null, status: TaskStatus | null, csrf: string, returnTo: string, result?: {
@@ -21,7 +21,7 @@ import {} from "../scope.js";
21
21
  import { parseReport } from "../scout-report.js";
22
22
  import {} from "../store.js";
23
23
  import { runCostWords } from "../summary.js";
24
- import { demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
24
+ import { checkBackingOf, CHECKED_AT_RELEASE, demoChecksOf, pullRequestFactOf, requirementsOf, requirementWordOf, STATUS_MORE, statusDetailsHtml, statusIconSvg, statusWhyHtml } from "../task-status.js";
25
25
  import { ACCEPT_NEEDS_REASON, acceptWordsOf, cantAcceptYetOf, evidenceProblemOf, MISMATCH_HEADLINE, receiptHeadingOf, receiptPublicationWords, reportMismatchesOf, RESULT_DECISION_SENTENCE, resultHeadlineOf, resultStatusOf, REVIEW_TOKENS } from "../workspace-ui.js";
26
26
  import { createHash, randomBytes } from "node:crypto";
27
27
  import { buildsViews, oneLineOf, projectChip, safePrUrl, screen, sentenceCase, strokeIcon, when, whenTime } from "./chrome.js";
@@ -173,13 +173,14 @@ export function criterionMatrixHtml(matrix, options = {}) {
173
173
  const confirmed = row.assessment !== undefined && state === "pass";
174
174
  // Plain words (task-status brief): the retired assessment step is not a state a person acts on.
175
175
  // The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
176
- const label = requirementWordOf(row, options.verdict);
176
+ const label = requirementWordOf(row, options.verdict, options.release);
177
177
  const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
178
178
  const warnings = [];
179
179
  // Only replace the known boilerplate. Other recorded failure details
180
180
  // remain verbatim, so concision cannot hide a different problem.
181
181
  const detail = row.detail.filter(line => !(row.assessment && row.review && line === `${row.review.author} ${row.review.judgement === "contradicts" ? "contradicts" : "needs more evidence for"} criterion "${row.id}": ${row.review.note}`) && !(state === "manual-review" && /^criterion "[^"\n]+" requires manual-review evidence — an operator must accept it before this can verify$/.test(line)));
182
- if (!awaitingAssessment && state !== "pass" && detail.length > 0)
182
+ // A requirement a release check covers, or that waits only on one, has nothing wrong to say.
183
+ if (!awaitingAssessment && state !== "pass" && label !== "Met" && label !== CHECKED_AT_RELEASE && detail.length > 0)
183
184
  warnings.push(html `<ul class="requirement-issues">${detail.map(line => html `<li>${line}</li>`)}</ul>`);
184
185
  const review = row.review;
185
186
  if (review !== null && review.judgement !== "upholds") {
@@ -671,13 +672,16 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
671
672
  action: false,
672
673
  });
673
674
  parts.push(html `<div id="verification" data-cockpit-section="result">${panel.html}</div>`);
674
- const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
675
+ // What no acceptance resolves (a failed check, a HIGH finding): Complete is refused, so no Accept is offered and
676
+ // the one line says the next step.
677
+ const hardStop = hardBlockerOf(assignment);
678
+ const blocked = hardStop ?? cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
675
679
  const youCheck = panel.panel.youCheck;
676
680
  // Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
677
681
  // report that doesn't match its changes asks for), the same request records it first.
678
682
  const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
679
683
  : blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
680
- const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
684
+ const complete = hardStop === null && assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
681
685
  ? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
682
686
  if (complete !== null)
683
687
  parts.push(completionForm(view.taskId, run.id, complete.receipt, view.detail?.pullRequestTo ?? null, owed));
@@ -686,8 +690,10 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
686
690
  const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
687
691
  const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
688
692
  const base = !acceptsHere ? null : acceptWordsOf({
689
- checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
690
- unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
693
+ // Checks Off that a release check already covers read as passed (release-coverage.ts).
694
+ checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? checks.release != null ? "passed" : "off" : checks.status,
695
+ // Release-aware words (task-status.ts): a requirement waiting only on a release check, or covered by one, isn't unmet.
696
+ unmet: matrix.filter(row => !["Met", "You check", CHECKED_AT_RELEASE].includes(requirementWordOf(row, null, checks === undefined ? undefined : checkBackingOf(checks)))).length,
691
697
  action: complete !== null ? "complete" : "accept",
692
698
  publishing: view.detail?.publishing ?? "other",
693
699
  proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
@@ -700,7 +706,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
700
706
  const followUps = view.detail.followUps ?? null;
701
707
  const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
702
708
  const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
703
- (checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
709
+ (checks === undefined || ((checks.status === "not-run" || checks.status === "unavailable") && checks.release == null))
704
710
  // Back on this result's Checks tab, where #follow-ups shows the run it started.
705
711
  ? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
706
712
  const need = panel.panel.need;
@@ -1774,7 +1780,8 @@ export function resultPanelParts(detail, o) {
1774
1780
  checkParts.push(detail.signedCriteria > 0 ? html `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : html `<p class="meta">This scope signed no acceptance checks.</p>`);
1775
1781
  }
1776
1782
  else {
1777
- checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
1783
+ checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict,
1784
+ release: current?.receipt == null ? undefined : checkBackingOf(current.receipt.checks) }));
1778
1785
  }
1779
1786
  checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
1780
1787
  if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
@@ -1930,11 +1937,18 @@ export function personCheckItems(proof, patchText, shots, runId) {
1930
1937
  };
1931
1938
  });
1932
1939
  }
1940
+ /** The completion blocker no acceptance resolves (a failed check, an unresolved HIGH finding), in words, or null. */
1941
+ export function hardBlockerOf(assignment) {
1942
+ return assignment?.readiness?.blockers.find(one => one.key === "check-failed" || one.key === "high")?.message ?? null;
1943
+ }
1933
1944
  /** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
1934
1945
  * stopped behind the password. Null under every other headline. */
1935
1946
  export function needActionOf(assignment, status, csrf, returnTo, result) {
1936
1947
  if (assignment === null || status === null || status.headline !== "Needs you" || status.need == null)
1937
1948
  return null;
1949
+ // Accepting can't resolve it: Request changes is the way on, beside the reason.
1950
+ if (result !== undefined && hardBlockerOf(assignment) !== null)
1951
+ return null;
1938
1952
  const action = assignment.primaryAction;
1939
1953
  const rebuild = { href: null, confirm: null, rebuild: { action: `${taskHref(assignment.rootId)}/requeue` } };
1940
1954
  // Built to an earlier plan: Build again, in place (the task page's requeue).
@@ -165,8 +165,8 @@ export function createTasksHandlers(runtime) {
165
165
  // Console v2: results ready to review and work running now, from the same admitted work index.
166
166
  ...(() => {
167
167
  const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
168
- const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
169
- const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
168
+ const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project, root: evidenceRoot });
169
+ const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project, root: evidenceRoot });
170
170
  return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
171
171
  })(),
172
172
  };
@@ -195,7 +195,7 @@ export function createTasksHandlers(runtime) {
195
195
  const rollup = project === null && !unscopedMode;
196
196
  let work;
197
197
  try {
198
- work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
198
+ work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project, root: evidenceRoot });
199
199
  const facts = requestContext.getStore();
200
200
  if (facts !== undefined) {
201
201
  // These counts cover the exact admitted project lens, including
@@ -1382,7 +1382,7 @@ export function createTasksHandlers(runtime) {
1382
1382
  // CI repair, suggestion-first (M8.18): a red episode never spawns an
1383
1383
  // agent by itself — it EARNS a button, and the button creates one
1384
1384
  // unapproved task through the same revision machinery as review
1385
- // comments. Deterministic id = one draft per task/PR, ever.
1385
+ // comments. One draft per task/PR, ever: the merge blocker records it.
1386
1386
  const id = Number(draftRepair[1]);
1387
1387
  const found = store.getRun(id);
1388
1388
  if (found === null || !visible(taskRepoOf(found.taskRef))) {
@@ -1401,10 +1401,6 @@ export function createTasksHandlers(runtime) {
1401
1401
  // failure was SEEN on and when (audit C-2) — a PR that advanced since
1402
1402
  // is a different failure, and the click time is not an observation.
1403
1403
  const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
1404
- // Suffixes survive truncation (audit C-7): the prefix gives way, the
1405
- // identity-bearing tail never does.
1406
- const suffix = `-ci-${publication.prNumber}`;
1407
- const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
1408
1404
  const brief = {
1409
1405
  schema: 1,
1410
1406
  kind: "ci-repair",
@@ -1425,25 +1421,32 @@ export function createTasksHandlers(runtime) {
1425
1421
  // repair use (contract handoff task 2): the inherited terms come from
1426
1422
  // the source rows inside the seal; the scope read above only names
1427
1423
  // the digest this draft was composed against.
1428
- const sealed = store.sealRevision({
1429
- source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
1430
- brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
1431
- child: {
1432
- id: draftId,
1433
- title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
1434
- repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
1435
- `Read the failing checks on GitHub before approving; this draft carries no log content.`,
1436
- },
1437
- commentIds: null,
1438
- requestedBy: who.name,
1439
- }, now);
1424
+ // One draft per task/PR, ever: the PR's merge blocker names it, lifted or not, and is written in the seal's transaction.
1425
+ const sealed = store.transact(() => {
1426
+ const prior = store.raw().prepare("SELECT task_id FROM merge_blocker WHERE publication = ? AND task_id IS NOT NULL ORDER BY rowid LIMIT 1").get(publication.id);
1427
+ if (prior !== undefined)
1428
+ return { ok: false, reason: "duplicate", detail: String(prior["task_id"]) };
1429
+ const result = store.sealRevision({
1430
+ source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
1431
+ brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
1432
+ child: {
1433
+ title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
1434
+ repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
1435
+ `Read the failing checks on GitHub before approving; this draft carries no log content.`,
1436
+ },
1437
+ commentIds: null,
1438
+ requestedBy: who.name,
1439
+ }, now);
1440
+ // The merge blocker rides the SAME breath as the draft (merge grant,
1441
+ // findings 3/12/13): while this repair exists, the source PR merges
1442
+ // NOTHING — sticky until the operator's unblock act or the PR closes.
1443
+ if (result.ok)
1444
+ store.createMergeBlocker(publication.id, result.id, now);
1445
+ return result;
1446
+ });
1440
1447
  if (!sealed.ok) {
1441
- return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${draftId}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
1448
+ return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${sealed.detail}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
1442
1449
  }
1443
- // The merge blocker rides the SAME breath as the draft (merge grant,
1444
- // findings 3/12/13): while this repair exists, the source PR merges
1445
- // NOTHING — sticky until the operator's unblock act or the PR closes.
1446
- store.createMergeBlocker(publication.id, sealed.id, now);
1447
1450
  return redirect(response, taskHref(sealed.id));
1448
1451
  }
1449
1452
  async function resolveIncident(ctx) {
package/dist/store.d.ts CHANGED
@@ -3018,7 +3018,7 @@ export declare class Store {
3018
3018
  * transaction can never commit half a seal). A brief FILE whose seal
3019
3019
  * failed is an orphan on disk, not authority — no artifact row points at
3020
3020
  * it. Two concurrent seals race on the comment consumption count, the
3021
- * deterministic child id, or the repair chain's `source_run UNIQUE`, and
3021
+ * family's next version id, or the repair chain's `source_run UNIQUE`, and
3022
3022
  * exactly one wins; the loser refuses in words with zero rows.
3023
3023
  *
3024
3024
  * The child is filed UNAPPROVED, always: this method never stamps an
@@ -3044,10 +3044,9 @@ export declare class Store {
3044
3044
  bytes: number;
3045
3045
  capture: string;
3046
3046
  };
3047
- /** The child's own words: an optional deterministic id, its title,
3048
- * and the explicitly described repair appended to the SOURCE goal. */
3047
+ /** The child's own words: its title and the explicitly described repair
3048
+ * appended to the SOURCE goal. Its id is the family's next version. */
3049
3049
  child: {
3050
- id?: string;
3051
3050
  title: string;
3052
3051
  repair: string;
3053
3052
  };
@@ -3169,7 +3168,6 @@ export declare class Store {
3169
3168
  capture: string;
3170
3169
  };
3171
3170
  child: {
3172
- id: string;
3173
3171
  title: string;
3174
3172
  repair: string;
3175
3173
  };
package/dist/store.js CHANGED
@@ -8756,7 +8756,7 @@ export class Store {
8756
8756
  * transaction can never commit half a seal). A brief FILE whose seal
8757
8757
  * failed is an orphan on disk, not authority — no artifact row points at
8758
8758
  * it. Two concurrent seals race on the comment consumption count, the
8759
- * deterministic child id, or the repair chain's `source_run UNIQUE`, and
8759
+ * family's next version id, or the repair chain's `source_run UNIQUE`, and
8760
8760
  * exactly one wins; the loser refuses in words with zero rows.
8761
8761
  *
8762
8762
  * The child is filed UNAPPROVED, always: this method never stamps an
@@ -8848,8 +8848,9 @@ export class Store {
8848
8848
  const badText = validateTaskText({ title: args.child.title, goal: args.child.repair });
8849
8849
  if (badText !== null)
8850
8850
  return refuse(badText.reason, badText.message);
8851
+ // Every road names its child the same way: the family root and the next version, `<root>-v2`, `-v3`.
8851
8852
  const made = this.#insertConsoleTask({
8852
- ...(args.child.id === undefined ? {} : { id: args.child.id }),
8853
+ id: this.#nextRevisionId(ancestry.chain.at(-1) ?? args.source.task),
8853
8854
  title: args.child.title,
8854
8855
  ...(sourceRef.repo === null ? {} : { repo: sourceRef.repo }),
8855
8856
  goal,
@@ -8926,6 +8927,20 @@ export class Store {
8926
8927
  }
8927
8928
  return { chain, problem: null };
8928
8929
  }
8930
+ /** The next free `<root>-vN` id inside the caller's transaction: N counts the family's versions (the root is v1),
8931
+ * skipping any id already taken. The suffix survives truncation to the 64-character id bound. */
8932
+ #nextRevisionId(root) {
8933
+ const family = this.db.prepare(`WITH RECURSIVE fam(id, depth) AS (
8934
+ SELECT external_id, 0 FROM task_ref WHERE backend = ? AND external_id = ?
8935
+ UNION SELECT r.external_id, fam.depth + 1 FROM task_ref r JOIN fam ON r.revision_of = fam.id WHERE r.backend = ? AND fam.depth < 64)
8936
+ SELECT COUNT(DISTINCT id) AS n FROM fam`).get(BUILT_IN, root, BUILT_IN);
8937
+ const taken = (id) => this.db.prepare("SELECT 1 AS hit FROM task WHERE id = ? UNION ALL SELECT 1 FROM task_ref WHERE backend = ? AND external_id = ?").get(id, BUILT_IN, id) !== undefined;
8938
+ const named = (n) => `${root.slice(0, 64 - `-v${n}`.length)}-v${n}`;
8939
+ let n = Math.max(2, Number(family?.["n"] ?? 1) + 1);
8940
+ while (taken(named(n)))
8941
+ n++;
8942
+ return named(n);
8943
+ }
8929
8944
  revisionAncestryOf(taskId) {
8930
8945
  return this.revisionAncestryStatus(taskId).chain;
8931
8946
  }
@@ -82,7 +82,9 @@ export async function runTaskOutcomeCommand(action, positional, flags, context)
82
82
  if (!changed.ok)
83
83
  return emit(changed);
84
84
  const assignment = 'body' in changed ? changed.body : changed.assignment;
85
- return emit({ ok: true, result: assignment }, [`${current.rootId} · Complete`, `Result: ${current.activeTaskId} · run ${current.receipt.runId} · ${current.receipt.head ?? 'no commit'}`, current.receipt.checks.detail]);
85
+ // Checks deliberately Off: nothing to say about them here.
86
+ return emit({ ok: true, result: assignment }, [`${current.rootId} · Complete`, `Result: ${current.activeTaskId} · run ${current.receipt.runId} · ${current.receipt.head ?? 'no commit'}`,
87
+ ...(current.readiness?.checksOff ? [] : [current.receipt.checks.detail])]);
86
88
  }
87
89
  const feedback = flags.get('feedback'), runFlag = flags.get('run'), sourceFlag = flags.get('source'), keyFlag = flags.get('key');
88
90
  if (typeof feedback !== 'string' || !feedback.trim())
@@ -56,6 +56,24 @@ export type ChecksFact = {
56
56
  running?: "quick" | "full" | null;
57
57
  /** A batch check (batch-checks.ts): waiting for its batch, or how it was checked. */
58
58
  batch?: ChecksBatch | null;
59
+ /** Off: the passing full release check whose commit contains this one (release-coverage.ts), read at read time. */
60
+ release?: {
61
+ run: number;
62
+ head: string;
63
+ } | null;
64
+ };
65
+ /** What checks Off leaves for a release: `pending` until a passing full release check contains the commit, then
66
+ * `covered`. `running`: a check is waiting for its batch or running on this commit. */
67
+ export type ReleaseState = "pending" | "covered" | "running";
68
+ export declare const CHECKED_AT_RELEASE = "Checked at release";
69
+ export declare function releaseStateOf(checks: Pick<ChecksFact, "status" | "level" | "release"> | null | undefined): ReleaseState | undefined;
70
+ /** How the project check stands for requirements that rest on it, read now: `covered` once a check passed on this
71
+ * commit (its own, a follow-up, or a release check containing it), `pending` while checks Off wait for a release. */
72
+ export declare function checkBackingOf(checks: Pick<ChecksFact, "status" | "level" | "release" | "running" | "batch"> | null | undefined): ReleaseState | undefined;
73
+ /** What stands between a finished result and Complete (`completionBlockersOf`): each with the exact next step. */
74
+ export type CompletionBlocker = {
75
+ key: "check-failed" | "check-missing" | "criteria" | "high";
76
+ message: string;
59
77
  };
60
78
  /** `waiting`: the result waits to be checked with others. Otherwise how its check ran: `together` on the
61
79
  * temporary batch commit `tested` with `peers`; `split` (its batch failed), `conflict` or `alone` on its own commit. */
@@ -94,14 +112,22 @@ export type TaskStatusFacts = {
94
112
  checks?: ChecksFact | null;
95
113
  pullRequest?: PullRequestFact | null;
96
114
  /** `unverified`: the report is refuted, so no requirement counts as met however it was marked. `missed`: the ones it
97
- * failed or left unanswered. */
115
+ * failed or left unanswered. `atRelease`: checks Off, waiting for a release check. `unshown`: a required screenshot
116
+ * is missing. `ask`: what a person is asked to look at first (a requirement they check, or a missing screenshot). */
98
117
  requirements?: {
99
118
  met: number;
100
119
  total: number;
101
120
  yours: number;
102
121
  missed?: number;
103
122
  unverified?: boolean;
123
+ atRelease?: number;
124
+ unshown?: number;
125
+ ask?: string | null;
104
126
  } | null;
127
+ /** A person accepted this result's proof: their own check and a recorded exception need nothing more. */
128
+ accepted?: boolean;
129
+ /** What refuses Complete (`completionBlockersOf`); the first one is the sentence when it holds the result back. */
130
+ blockers?: readonly CompletionBlocker[];
105
131
  evidence?: {
106
132
  shortened: number;
107
133
  missing: number;
@@ -124,11 +150,14 @@ export type TaskStatusFacts = {
124
150
  /** What the person calls their lead (Settings → Lead); "Lead" when unnamed. */
125
151
  leadName?: string;
126
152
  };
127
- export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report">): Headline;
153
+ export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report" | "requirements" | "accepted" | "blockers">): Headline;
128
154
  /** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
129
155
  export declare const leadOnIt: (name?: string) => string;
130
156
  export declare const leadLapsed: (name?: string) => string;
131
- export declare function taskStatusOf(facts: TaskStatusFacts): TaskStatus;
157
+ /** The one status every surface shows (console list, task and result pages, CLI, chat, phone, `status`). */
158
+ export declare function statusOf(facts: TaskStatusFacts): TaskStatus;
159
+ /** The name most surfaces already import. */
160
+ export declare const taskStatusOf: typeof statusOf;
132
161
  export declare function plainReasonOf(stage: TaskStage, code: string, detail: string | null | undefined): string | null;
133
162
  export declare function stageOfCode(code: string, options?: {
134
163
  needsPerson?: boolean;
@@ -165,18 +194,41 @@ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "
165
194
  wait?: WaitKey;
166
195
  };
167
196
  type RequirementRow = {
197
+ id?: string;
198
+ statement?: string;
168
199
  state: string;
200
+ requiredEvidence?: readonly string[];
201
+ answered?: readonly {
202
+ kind: string;
203
+ }[];
204
+ detail?: readonly string[];
169
205
  assessment?: {
170
206
  evidenceState?: string;
207
+ detail?: readonly string[];
171
208
  } | undefined;
172
209
  review?: unknown;
173
210
  };
174
211
  /** One requirement's state in words: the card's Requirements row and the Checks tab read this one source, so they
175
- * can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does. */
176
- export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Not met" | "Unverified";
177
- export declare function requirementWordOf(row: RequirementRow, verdict?: string | null): RequirementWord;
212
+ * can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does.
213
+ * With checks Off, a requirement that waits only on the check reads Checked at release, then Met once a passing full
214
+ * release check contains the commit. */
215
+ export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Checked at release" | "Waiting for checks" | "Not met" | "Unverified";
216
+ export declare function requirementWordOf(row: RequirementRow, verdict?: string | null, release?: ReleaseState): RequirementWord;
178
217
  /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
179
- export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
218
+ export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined, release?: ReleaseState): NonNullable<TaskStatusFacts["requirements"]> | null;
219
+ /** What refuses Complete, for `task complete`, the console and chat completion and deploy-candidate alike: a failed
220
+ * check; a required check that is missing (checks deliberately Off are not missing: they are checked at release);
221
+ * an unresolved requirement (a person's own check is theirs to give by completing; a recorded acceptance resolves
222
+ * the rest); an unresolved HIGH review finding. A release check that covers the commit counts as passed. */
223
+ export declare function completionBlockersOf(f: {
224
+ report: boolean;
225
+ checks: Pick<ChecksFact, "status" | "exitCode" | "level" | "release" | "running" | "batch"> | null;
226
+ checkRequired: boolean;
227
+ matrix: readonly RequirementRow[] | null | undefined;
228
+ verdict: string | null | undefined;
229
+ accepted: boolean;
230
+ high: number;
231
+ }): CompletionBlocker[];
180
232
  /** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
181
233
  export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
182
234
  /** Saved evidence health from the receipt's artifact list and its caveats. */