toolroll 0.9.34 → 0.9.36

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 (52) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +7 -0
  3. package/dist/assignment.d.ts +1 -0
  4. package/dist/assignment.js +2 -2
  5. package/dist/browser-workspace.d.ts +3 -3
  6. package/dist/builder.js +4 -7
  7. package/dist/chat-acceptance.d.ts +1 -1
  8. package/dist/contracts/contract.d.ts +33 -5
  9. package/dist/contracts/contract.js +143 -16
  10. package/dist/contracts/flow-propose.d.ts +325 -0
  11. package/dist/contracts/flow-propose.js +41 -0
  12. package/dist/contracts/flow.d.ts +19400 -0
  13. package/dist/contracts/flow.js +359 -0
  14. package/dist/contracts/handoff.d.ts +91 -0
  15. package/dist/contracts/handoff.js +82 -0
  16. package/dist/contracts/proof.d.ts +136 -0
  17. package/dist/contracts/proof.js +110 -0
  18. package/dist/contracts/verification-receipt.d.ts +148 -0
  19. package/dist/contracts/verification-receipt.js +87 -0
  20. package/dist/demo.js +28 -11
  21. package/dist/desktop-update.d.ts +2 -2
  22. package/dist/evidence.d.ts +3 -40
  23. package/dist/evidence.js +5 -1
  24. package/dist/flow-gallery.d.ts +3 -2
  25. package/dist/flow-share.d.ts +10 -32
  26. package/dist/flow-share.js +65 -83
  27. package/dist/flow-starters.d.ts +2 -1
  28. package/dist/flow-triggers.d.ts +15 -72
  29. package/dist/flow-triggers.js +94 -83
  30. package/dist/flows.d.ts +37 -218
  31. package/dist/flows.js +350 -385
  32. package/dist/mate-tools.js +14 -44
  33. package/dist/mcp-connect.d.ts +3 -0
  34. package/dist/mcp-connect.js +16 -4
  35. package/dist/operate.js +31 -2
  36. package/dist/plan.d.ts +5 -5
  37. package/dist/plan.js +5 -25
  38. package/dist/process-recovery-eligibility.d.ts +2 -2
  39. package/dist/process-recovery-eligibility.js +13 -8
  40. package/dist/proof.d.ts +15 -91
  41. package/dist/proof.js +90 -249
  42. package/dist/result-evidence-readers.d.ts +42 -0
  43. package/dist/result-evidence-readers.js +91 -0
  44. package/dist/serve.js +8 -99
  45. package/dist/setup-guide.d.ts +2 -2
  46. package/dist/surface.js +8 -1
  47. package/dist/task-review-brief.d.ts +75 -0
  48. package/dist/task-review-brief.js +179 -0
  49. package/dist/text-limits.d.ts +70 -0
  50. package/dist/text-limits.js +70 -0
  51. package/dist/verification-evidence.js +24 -12
  52. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from "./result-evidence-readers.js";
1
2
  import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
2
3
  import { aboutYouOf, checkAboutYou, saveAboutYou, ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "./lead-about.js";
3
4
  import { withActor } from "./actor.js";
@@ -53,7 +54,7 @@ import { FORM_PATH, flowFormPage, receiveFlowForm, shareFlowButton, stopSharingF
53
54
  import { addFlowTriggerTo, checkFlowTriggerNow, HOOK_PATH, pressFlowButton, receiveFlowHook, removeFlowTrigger, renewFlowHook, removeLinearKey, saveHooksBase, saveLinearKey, saveLinearSigningSecret } from "./flow-triggers.js";
54
55
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, FLOW_HREF, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
55
56
  import { chooseFlowCard } from "./flow-send.js";
56
- import { FLOW_TEMPLATES, validateFlowDefinition } from "./flows.js";
57
+ import { FlowContractError, FLOW_TEMPLATES, validateFlowDefinition, withZoneNames } from "./flows.js";
57
58
  import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
58
59
  import { changeLearning, learningView } from "./project-learning.js";
59
60
  import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
@@ -1136,6 +1137,8 @@ export function createDecisionServer(options) {
1136
1137
  response.end(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="0;url=${escape(to)}"><title>Toolroll</title>${HANDOFF_STYLE}<p>${escape(words)} <a href="${escape(to)}">Back to Toolroll</a></p>`);
1137
1138
  };
1138
1139
  const state = url.searchParams.get("state") ?? "";
1140
+ // Every arrival is logged (never the code or state): a sign-in that never came back is then visible as an absence.
1141
+ logEvent("info", "connect.callback", { error: url.searchParams.get("error"), known: connectVisits.has(state) });
1139
1142
  if (!startedHere(request, state))
1140
1143
  return done("/settings/tools", "problem", "That sign-in was started in another browser. Connect again from this one.");
1141
1144
  const visit = connectVisits.get(state);
@@ -7179,12 +7182,13 @@ export function createDecisionServer(options) {
7179
7182
  return redirect(response, "/flows");
7180
7183
  }
7181
7184
  if (action === "save") {
7182
- let saved;
7185
+ let saved, drawn;
7183
7186
  try {
7184
- saved = validateFlowDefinition(JSON.parse(body.get("definition") ?? "null"));
7187
+ drawn = JSON.parse(body.get("definition") ?? "null");
7188
+ saved = validateFlowDefinition(drawn);
7185
7189
  }
7186
7190
  catch (error) {
7187
- return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof Error ? error.message : "That flow isn't valid." });
7191
+ return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof FlowContractError ? withZoneNames(error.lines, drawn) : error instanceof Error ? error.message : "That flow isn't valid." });
7188
7192
  }
7189
7193
  const name = (body.get("name") ?? flow.name).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || flow.name;
7190
7194
  // The owner (v86) is whom "the owner decides" zones ask: someone who can approve on this project.
@@ -22504,32 +22508,6 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
22504
22508
  const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
22505
22509
  return screen("builds", [`<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
22506
22510
  }
22507
- function structuredHandoffView(artifacts, root) {
22508
- const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
22509
- if (artifact === undefined)
22510
- return null;
22511
- const read = readVerifiedArtifact(root, artifact);
22512
- if (!read.ok)
22513
- return null;
22514
- try {
22515
- const parsed = JSON.parse(read.content.toString("utf8"));
22516
- if (parsed === null || typeof parsed !== "object" || typeof parsed["conclusion"] !== "string")
22517
- return null;
22518
- const conclusion = oneLineOf(parsed["conclusion"], 600);
22519
- if (conclusion === "" || hasForbiddenControls(conclusion))
22520
- return null;
22521
- const list = (name) => Array.isArray(parsed[name])
22522
- ? parsed[name]
22523
- .filter((one) => typeof one === "string" && one.trim() !== "" && !hasForbiddenControls(one))
22524
- .slice(0, 8)
22525
- .map(one => oneLineOf(one, 240))
22526
- : [];
22527
- return { conclusion, changes: list("changes"), verification: list("verification"), followUps: list("followUps") };
22528
- }
22529
- catch {
22530
- return null;
22531
- }
22532
- }
22533
22511
  /** What a scout found, each with its link and picture, then any screenshot no item shows. Every link is the cited
22534
22512
  * http(s) address the report parser admitted; every picture is served from verified evidence. */
22535
22513
  function reportItemsHtml(items, shots, runId) {
@@ -22606,7 +22584,6 @@ function sharedResultFactsOf(run, proof, terminal, handoff, report, publication)
22606
22584
  publicationWords: receiptPublicationWords(publication),
22607
22585
  };
22608
22586
  }
22609
- const SCREENSHOT_CAPTURE = /^agent-claimed screenshot at (.+) \(validated (?:png|jpeg)\)/;
22610
22587
  function proofBundleView(store, run, artifacts, root) {
22611
22588
  const verdictRow = store.proofVerdictFor(run.id);
22612
22589
  const acceptanceRow = store.proofAcceptance(run.id);
@@ -22722,74 +22699,6 @@ function completionReceiptView(store, run, artifacts, root, review = null) {
22722
22699
  facts: sharedResultFactsOf(run, proof, terminal, handoff, report, publication),
22723
22700
  };
22724
22701
  }
22725
- const CAPTURE_EXIT = /\(exit ([0-9]{1,4})\)\s*$/;
22726
- /**
22727
- * Assemble the terminal-diff card's facts. Every branch names its state:
22728
- * a failed capture (nonzero exit recorded in the capture string) reads as
22729
- * the failure it is, unverifiable bytes read as their problem, and absence
22730
- * returns null so old runs simply show nothing rather than a broken card.
22731
- */
22732
- function terminalDiffView(artifacts, root) {
22733
- const patchArtifact = artifacts.find(one => one.kind === "terminal-diff");
22734
- const statArtifact = artifacts.find(one => one.kind === "diff-stat");
22735
- if (patchArtifact === undefined && statArtifact === undefined)
22736
- return null;
22737
- const view = { patch: null, stat: null };
22738
- if (patchArtifact !== undefined) {
22739
- const exit = CAPTURE_EXIT.exec(patchArtifact.capture);
22740
- if (exit !== null && exit[1] !== "0") {
22741
- view.patch = { problem: `capture failed — ${patchArtifact.capture}` };
22742
- }
22743
- else {
22744
- const read = readVerifiedArtifact(root, patchArtifact);
22745
- view.patch = read.ok
22746
- ? { text: read.content.toString("utf8"), truncated: patchArtifact.truncated, artifactId: patchArtifact.id }
22747
- : { problem: `stored but unverifiable — ${read.problem}` };
22748
- }
22749
- }
22750
- if (statArtifact !== undefined) {
22751
- const exit = CAPTURE_EXIT.exec(statArtifact.capture);
22752
- if (exit !== null && exit[1] !== "0") {
22753
- view.stat = { problem: `capture failed — ${statArtifact.capture}` };
22754
- }
22755
- else {
22756
- const read = readVerifiedArtifact(root, statArtifact);
22757
- if (!read.ok) {
22758
- view.stat = { problem: `stored but unverifiable — ${read.problem}` };
22759
- }
22760
- else {
22761
- try {
22762
- const parsed = JSON.parse(read.content.toString("utf8"));
22763
- // Every field this page renders is type-proved (arc 6, finding 7):
22764
- // "an object with a base key" was accepting any shape at all.
22765
- const count = (value) => typeof value === "number" && Number.isFinite(value);
22766
- const wellFormed = parsed !== null &&
22767
- typeof parsed === "object" &&
22768
- typeof parsed["base"] === "string" &&
22769
- typeof parsed["head"] === "string" &&
22770
- count(parsed["fileCount"]) &&
22771
- count(parsed["additions"]) &&
22772
- count(parsed["deletions"]) &&
22773
- count(parsed["binaryCount"]) &&
22774
- typeof parsed["filesTruncated"] === "boolean" &&
22775
- Array.isArray(parsed["files"]) &&
22776
- parsed["files"].every(one => one !== null &&
22777
- typeof one === "object" &&
22778
- typeof one["path"] === "string" &&
22779
- (count(one["additions"]) || one["additions"] === null) &&
22780
- (count(one["deletions"]) || one["deletions"] === null));
22781
- view.stat = wellFormed
22782
- ? parsed
22783
- : { problem: "stat is not the shape this page knows" };
22784
- }
22785
- catch {
22786
- view.stat = { problem: "stat did not parse as JSON" };
22787
- }
22788
- }
22789
- }
22790
- }
22791
- return view;
22792
- }
22793
22702
  const REVIEW_DIFF_LINE_CAP = 4_000;
22794
22703
  /** Parse only the stable structure Git's unified patch format guarantees.
22795
22704
  * Unknown metadata remains visible, and a patch that cannot be structured
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
+ content?: never;
18
19
  ok: false;
19
20
  message: string;
20
21
  plan?: never;
21
- content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
+ content?: never;
33
34
  ok: false;
34
35
  message: string;
35
36
  plan?: never;
36
- content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
package/dist/surface.js CHANGED
@@ -278,7 +278,14 @@ export const COMMAND_GUIDE = [
278
278
  operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
279
279
  operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
280
280
  operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
281
- operator("task review", "show a run's automatic review: HIGH findings, suggested follow-ups, or why it was not reviewed"),
281
+ { invocation: "task review", synopsis: "read a task's compact result packet with --brief, or a run's automatic findings without it", audience: "agent", agentMayInvoke: true, mutation: "none",
282
+ positionals: [{ name: "id", required: true, meaning: "task id with --brief; run id otherwise" }],
283
+ flags: [jsonFlag, dbFlag,
284
+ { name: "brief", takesValue: false, meaning: "saved conclusion, changed files, checks, findings, screenshots, plan and next actions; missing fields say not recorded" },
285
+ { name: "run", takesValue: true, meaning: "with --brief, select an exact finished attempt instead of the latest" },
286
+ { name: "all", takesValue: false, meaning: "include LOW findings within the compact packet's limit; extra findings are counted" },
287
+ { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }],
288
+ notableReasons: ["usage", "unauthenticated", "not-found"] },
282
289
  operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
283
290
  operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
284
291
  operator("task route", "which configured agents plan, build and repair this task; phase and risk changes require renewed approval"),
@@ -0,0 +1,75 @@
1
+ /** A read-only packet of saved result facts. Never opens a worktree or emits logs, patches or saved shell commands. */
2
+ import type { Store } from './store.js';
3
+ import { type AssignmentAccess } from './assignment.js';
4
+ import { type FindingSeverity } from './review-switch.js';
5
+ declare const MISSING: 'not recorded';
6
+ type Missing = typeof MISSING;
7
+ type Finding = {
8
+ severity: FindingSeverity;
9
+ file: string;
10
+ line: number;
11
+ scenario: string;
12
+ };
13
+ export type ReviewBrief = {
14
+ header: {
15
+ task: string;
16
+ run: number | Missing;
17
+ provider: string;
18
+ model: string;
19
+ minutes: number | Missing;
20
+ outcome: string;
21
+ base: string;
22
+ head: string;
23
+ state: string;
24
+ };
25
+ conclusion: string;
26
+ changedFiles: {
27
+ files: {
28
+ path: string;
29
+ additions: number | Missing;
30
+ deletions: number | Missing;
31
+ }[];
32
+ total: number;
33
+ omitted: number;
34
+ } | Missing;
35
+ check: {
36
+ status: string;
37
+ exit: number | Missing;
38
+ failed: string[] | Missing;
39
+ };
40
+ review: {
41
+ state: string;
42
+ reason: string;
43
+ findings: Finding[];
44
+ counts: Record<FindingSeverity, number>;
45
+ omitted: Record<FindingSeverity, number>;
46
+ } | Missing;
47
+ screenshots: {
48
+ files: {
49
+ path: string;
50
+ bytes: number | Missing;
51
+ }[];
52
+ omitted: number;
53
+ } | Missing;
54
+ plan: {
55
+ titles: string[];
56
+ omitted: number;
57
+ } | Missing;
58
+ nextActions: string[];
59
+ };
60
+ /** Select only finished build/report attempts in the admitted family. An explicit run never crosses that boundary. */
61
+ export declare function taskReviewBrief(store: Store, task: string, root: string, now: Date, access: AssignmentAccess, options?: {
62
+ run?: number;
63
+ all?: boolean;
64
+ secrets?: readonly string[];
65
+ }): {
66
+ ok: true;
67
+ brief: ReviewBrief;
68
+ } | {
69
+ ok: false;
70
+ reason: string;
71
+ message: string;
72
+ };
73
+ /** Text and JSON share this bounded projection; --all includes LOW details within the same line budget. */
74
+ export declare function renderReviewBrief(brief: ReviewBrief): string[];
75
+ export {};
@@ -0,0 +1,179 @@
1
+ import { assignmentChecksForRun, assignmentOf } from './assignment.js';
2
+ import { assignmentPresentationOf } from './assignment-presentation.js';
3
+ import { readVerifiedArtifact, readVerifiedProofForRun, reportShotsOf, scanForSecrets } from './evidence.js';
4
+ import { SCREENSHOT_CAPTURE, structuredHandoffView, terminalDiffView } from './result-evidence-readers.js';
5
+ import { buildReviewOf } from './review-switch.js';
6
+ import { parseReport } from './scout-report.js';
7
+ import { sanitizeTranscriptLine } from './live.js';
8
+ import { redactSecretAssignments } from './builder.js';
9
+ import { parseExecutionPlanDocument } from './plan.js';
10
+ const MISSING = 'not recorded';
11
+ const SEVERITIES = ['HIGH', 'MEDIUM', 'LOW'];
12
+ /** Redact before shortening so a clipped credential cannot escape recognition. Apply to every data string in both formats. */
13
+ function safeText(value, secrets, cap = 300) {
14
+ if (typeof value !== 'string' || !value.trim())
15
+ return MISSING;
16
+ const flat = value.replace(/\s+/g, ' ').trim();
17
+ const cleaned = sanitizeTranscriptLine(flat);
18
+ if (secrets.some(secret => secret !== '' && value.includes(secret)) ||
19
+ /\blt_[a-f0-9]{12}_[A-Za-z0-9_-]{43}\b|\bBearer\s+\S+/i.test(flat) ||
20
+ scanForSecrets(flat).length || redactSecretAssignments(flat) !== flat || cleaned.startsWith('[a credential-shaped'))
21
+ return '[redacted]';
22
+ return cleaned.length <= cap ? cleaned : `${cleaned.slice(0, cap - 1)}…`;
23
+ }
24
+ function verifiedText(root, artifact) {
25
+ if (!artifact || artifact.truncated || artifact.captureStatus === 'failed')
26
+ return null;
27
+ const read = readVerifiedArtifact(root, artifact);
28
+ return read.ok ? read.content.toString('utf8') : null;
29
+ }
30
+ /** Select only finished build/report attempts in the admitted family. An explicit run never crosses that boundary. */
31
+ export function taskReviewBrief(store, task, root, now, access, options = {}) {
32
+ const family = store.taskFamilyOf(task, access.repos, access.principal === 'operator' && access.includeUnplaced === true);
33
+ if (family === null)
34
+ return { ok: false, reason: 'not-found', message: 'That task is unavailable in your projects.' };
35
+ const runs = family.versions.flatMap(version => store.runsFor(version.refId))
36
+ .filter(run => (run.role === 'builder' || run.role === 'scout') && run.finishedAt !== null && run.outcome !== null)
37
+ .sort((a, b) => b.id - a.id);
38
+ const run = options.run === undefined ? runs[0] ?? null : runs.find(one => one.id === options.run) ?? null;
39
+ if (options.run !== undefined && run === null)
40
+ return { ok: false, reason: 'not-found', message: 'That finished build or report is unavailable for this task.' };
41
+ const assignment = assignmentOf(store, task, now, access, root);
42
+ const safe = (value, cap) => safeText(value, options.secrets ?? [], cap);
43
+ const sha = (value) => /^[a-f0-9]{40,64}$/.test(value ?? '') ? value.slice(0, 12) : MISSING;
44
+ const artifacts = run === null ? [] : store.artifactsFor(run.id);
45
+ // Pass only the inventory: this reader does not need to open the patch for a brief.
46
+ const statView = terminalDiffView(artifacts.filter(one => one.kind === 'diff-stat' && one.captureStatus !== 'failed'), root)?.stat;
47
+ const stat = statView && !('problem' in statView) ? statView : null;
48
+ const handoff = structuredHandoffView(artifacts, root, value => safe(value, 600));
49
+ const reportText = run?.role === 'scout' ? verifiedText(root, artifacts.find(one => one.kind === 'report')) : null;
50
+ const report = reportText === null ? null : parseReport(reportText, { stored: true });
51
+ const conclusion = handoff?.conclusion ?? (report?.ok ? report.report.summary : run?.handoff);
52
+ const duration = run?.finishedAt ? (Date.parse(run.finishedAt) - Date.parse(run.startedAt)) / 60_000 : NaN;
53
+ const recordedCheck = run === null ? null : store.runCheckFor(run.id);
54
+ const receipt = assignment?.receipt?.runId === run?.id ? assignment?.receipt : null;
55
+ // The shared assignment reader includes later checks on the same candidate. Its result wins;
56
+ // old named failures must not ride a newer passing (or differently failed) check.
57
+ const checks = receipt?.checks ?? (run === null ? null : assignmentChecksForRun(store, root, run.id, now));
58
+ const status = checks && checks.status !== 'unavailable' && checks.status !== 'not-run' ? checks.status : recordedCheck?.status ?? checks?.status ?? MISSING;
59
+ const exit = checks && checks.status === status ? checks.exitCode : recordedCheck?.exitCode;
60
+ const sameCheck = recordedCheck?.status === status && recordedCheck.exitCode === exit &&
61
+ (!checks?.logArtifactId || artifacts.some(one => one.id === checks.logArtifactId && one.kind === 'check-log'));
62
+ const progress = run === null ? null : store.checkProgress(run.id);
63
+ const failed = sameCheck ? [...new Set([
64
+ ...recordedCheck.suites.filter(one => one.status === 'failed').map(one => safe(one.name)),
65
+ ...Object.entries(progress?.suites ?? {}).filter(([, suite]) => suite.state === 'failed').map(([name, suite]) => `${name}: ${suite.failed} failed`),
66
+ ])] : [];
67
+ const review = run === null ? null : buildReviewOf(store, run.id);
68
+ const findings = review === null ? [] : [...review.high, ...review.followUps].filter(one => one && SEVERITIES.includes(one.severity));
69
+ const counts = Object.fromEntries(SEVERITIES.map(severity => [severity, findings.filter(one => one.severity === severity).length]));
70
+ const shown = SEVERITIES.flatMap(severity => severity === 'LOW' && !options.all ? [] : findings.filter(one => one.severity === severity))
71
+ .slice(0, 12).map(one => ({ severity: one.severity, file: safe(one.file), line: Number.isSafeInteger(one.line) ? one.line : 0, scenario: safe(one.scenario) }));
72
+ const omitted = Object.fromEntries(SEVERITIES.map(severity => [severity, counts[severity] - shown.filter(one => one.severity === severity).length]));
73
+ const proof = run === null ? null : readVerifiedProofForRun(store, root, run.id);
74
+ const shots = artifacts.filter(one => one.kind === 'screenshot');
75
+ const reportShots = report?.ok && run !== null ? reportShotsOf(artifacts, root, run.id, report.report.images) : [];
76
+ const pathOf = (artifact) => SCREENSHOT_CAPTURE.exec(artifact.capture)?.[1] ??
77
+ (reportShots.some(one => one.artifactId === artifact.id) ? `evidence/${artifact.key}` : null);
78
+ const declared = proof?.ok ? proof.proof.screenshots.map(one => one.path) : [];
79
+ const paths = [...new Set([...declared, ...shots.flatMap(one => pathOf(one) ?? [])])];
80
+ const screenshots = paths.map(path => {
81
+ const artifact = shots.find(one => pathOf(one) === path);
82
+ return { path: safe(path), bytes: artifact && artifact.captureStatus !== 'failed' && readVerifiedArtifact(root, artifact).ok ? artifact.bytesStored : MISSING };
83
+ });
84
+ const plan = run === null ? null : planTitles(store, root, run);
85
+ const current = run !== null && receipt !== null && family.current.refId === run.taskRef;
86
+ const nextActions = [];
87
+ if (!run)
88
+ nextActions.push('Open the task to inspect its progress.');
89
+ else if (!current)
90
+ nextActions.push('Open the current result before completing or requesting a revision.');
91
+ else {
92
+ if (assignment?.state === 'ready-to-check')
93
+ nextActions.push(`Complete: toolroll task complete ${safe(family.root.id)} --digest ${receipt.digest}`);
94
+ const diff = artifacts.find(one => one.kind === 'terminal-diff');
95
+ if (run.role === 'builder' && diff && readVerifiedArtifact(root, diff).ok) {
96
+ nextActions.push(`Revise: toolroll task revise ${safe(family.root.id)} --run ${run.id} --feedback "requested change"`);
97
+ }
98
+ else if (run.role === 'builder')
99
+ nextActions.push('Revision unavailable: saved changes are not recorded or cannot be read.');
100
+ if (assignment?.state === 'ready-to-check' || assignment?.state === 'complete') {
101
+ if (run.role === 'builder' && run.outcome === 'built')
102
+ nextActions.push(assignment.state === 'complete'
103
+ ? 'Merge into a release through the normal release approval and check process.'
104
+ : 'After completion, merge into a release through the normal release approval and check process.');
105
+ }
106
+ else
107
+ nextActions.push(`Open the task: ${safe(assignment?.detail)}`);
108
+ }
109
+ return { ok: true, brief: {
110
+ header: { task: safe(run === null ? task : store.externalIdFor(run.taskRef)), run: run?.id ?? MISSING,
111
+ provider: safe(run?.provider), model: safe(run?.model), minutes: Number.isFinite(duration) && duration >= 0 ? Math.round(duration * 10) / 10 : MISSING,
112
+ outcome: safe(run?.outcome), base: sha(run?.baseRevision), head: sha(run?.headRevision), state: run !== null && !current ? 'Earlier result' : assignment === null ? MISSING : assignmentPresentationOf(assignment).status.label },
113
+ conclusion: safe(conclusion, 600),
114
+ changedFiles: stat === null ? MISSING : { files: [...stat.files].sort((a, b) => ((b.additions ?? 0) + (b.deletions ?? 0)) - ((a.additions ?? 0) + (a.deletions ?? 0)))
115
+ .slice(0, 15).map(one => ({ path: safe(one.path), additions: one.additions ?? MISSING, deletions: one.deletions ?? MISSING })), total: stat.fileCount, omitted: Math.max(0, stat.fileCount - Math.min(15, stat.files.length)) },
116
+ check: { status: status === 'unavailable' ? MISSING : status, exit: exit ?? MISSING, failed: status !== 'failed' ? [] : failed.length ? failed : MISSING },
117
+ review: review === null ? MISSING : { state: review.state, reason: safe(review.reason), findings: shown, counts, omitted },
118
+ screenshots: screenshots.length ? { files: screenshots.slice(0, 8), omitted: Math.max(0, screenshots.length - 8) } : MISSING,
119
+ plan: plan === null || plan.length === 0 ? MISSING : { titles: plan.slice(0, 5).map(one => safe(one)), omitted: Math.max(0, plan.length - 5) },
120
+ nextActions,
121
+ } };
122
+ }
123
+ function planTitles(store, root, run) {
124
+ const revision = run.planRevision == null ? null : store.getPlanRevision(run.planRevision);
125
+ // Historical runs cannot borrow a plan written after they started.
126
+ const artifact = revision === null ? store.runsFor(run.taskRef).filter(one => one.role === 'planner' && one.id < run.id)
127
+ .sort((a, b) => b.id - a.id).flatMap(one => store.artifactsFor(one.id)).find(one => one.kind === 'plan') : store.getArtifact(revision.artifact);
128
+ const text = verifiedText(root, artifact);
129
+ if (text === null)
130
+ return null;
131
+ const parsed = parseExecutionPlanDocument(text);
132
+ return parsed.ok ? parsed.document.milestones : text.split(/\r?\n/).filter(line => /^#{1,6}\s+\S/.test(line)).map(line => line.replace(/^#{1,6}\s+/, ''));
133
+ }
134
+ /** Text and JSON share this bounded projection; --all includes LOW details within the same line budget. */
135
+ export function renderReviewBrief(brief) {
136
+ const h = brief.header;
137
+ const lines = [`${h.task} · run ${h.run} · ${h.provider}/${h.model} · ${h.minutes} min · ${h.outcome} · ${h.base} → ${h.head} · ${h.state}`,
138
+ `Conclusion: ${brief.conclusion}`, 'Changed files:'];
139
+ if (brief.changedFiles === MISSING)
140
+ lines.push(` ${MISSING}`);
141
+ else {
142
+ lines.push(...brief.changedFiles.files.map(one => ` ${one.path} +${one.additions} / -${one.deletions}`));
143
+ if (brief.changedFiles.total === 0)
144
+ lines.push(' none');
145
+ if (brief.changedFiles.omitted)
146
+ lines.push(` and ${brief.changedFiles.omitted} more`);
147
+ }
148
+ const check = brief.check;
149
+ lines.push(`Check: ${check.status} · exit ${check.exit}${check.failed === MISSING ? ` · failed parts: ${MISSING}` : check.failed.length ? ` · failed: ${check.failed.join('; ')}` : ''}`);
150
+ if (brief.review === MISSING)
151
+ lines.push(`Automatic review: ${MISSING}`);
152
+ else {
153
+ lines.push(`Automatic review: ${brief.review.state}${brief.review.reason === MISSING ? '' : ` · ${brief.review.reason}`}`);
154
+ lines.push(...brief.review.findings.map(one => ` ${one.severity} ${one.file}:${one.line} — ${one.scenario}`));
155
+ for (const severity of SEVERITIES)
156
+ if (brief.review.omitted[severity])
157
+ lines.push(` ${severity}: ${brief.review.omitted[severity]} more (${brief.review.counts[severity]} total)`);
158
+ if (Object.values(brief.review.counts).every(count => count === 0) && brief.review.state === 'reviewed')
159
+ lines.push(' no findings');
160
+ }
161
+ lines.push('Screenshots:');
162
+ if (brief.screenshots === MISSING)
163
+ lines.push(` ${MISSING}`);
164
+ else {
165
+ lines.push(...brief.screenshots.files.map(one => ` ${one.path} (${one.bytes}${typeof one.bytes === 'number' ? ' bytes' : ''})`));
166
+ if (brief.screenshots.omitted)
167
+ lines.push(` and ${brief.screenshots.omitted} more`);
168
+ }
169
+ lines.push('Plan:');
170
+ if (brief.plan === MISSING)
171
+ lines.push(` ${MISSING}`);
172
+ else {
173
+ lines.push(...brief.plan.titles.map(one => ` ${one}`));
174
+ if (brief.plan.omitted)
175
+ lines.push(` and ${brief.plan.omitted} more`);
176
+ }
177
+ lines.push('Next:', ...brief.nextActions.map(one => ` ${one}`));
178
+ return lines;
179
+ }
@@ -16,6 +16,67 @@ export declare const TEXT_LIMITS: {
16
16
  readonly noteBytes: 16000;
17
17
  /** A flow work zone's instructions; nothing is held back for the card's details (they are attached when long). */
18
18
  readonly flowInstructions: 8000;
19
+ /** A flow's zones and steps (src/contracts/flow.ts): a zone's name, a step's id as written, and a reference to another step. */
20
+ readonly flowTitle: 60;
21
+ readonly flowStepId: 32;
22
+ readonly flowRef: 60;
23
+ /** A Message zone's message, or the comment an Update zone leaves. */
24
+ readonly flowMessage: 1000;
25
+ /** Who decides a "Person decides" zone (a sign-in name), and the AI teammate who works a zone, as written. */
26
+ readonly flowDecider: 64;
27
+ readonly flowTeammate: 40;
28
+ /** A Build zone's other project: its path. */
29
+ readonly flowRepo: 1000;
30
+ /** A script's or teammate's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
31
+ readonly flowAnswer: 40;
32
+ readonly flowChoice: 40;
33
+ readonly flowSortMeans: 200;
34
+ readonly flowSortQuestion: 300;
35
+ readonly flowSortLevel: 120;
36
+ /** A Web request zone's address, one header's value and the body it sends (an email's body too). */
37
+ readonly flowUrl: 2000;
38
+ readonly flowHeader: 500;
39
+ readonly flowBody: 8000;
40
+ /** An email's recipients and subject. */
41
+ readonly flowEmailTo: 500;
42
+ readonly flowEmailSubject: 200;
43
+ /** A tool zone's tool (MCP server), its function, and the arguments as JSON. */
44
+ readonly flowToolServer: 64;
45
+ readonly flowToolName: 100;
46
+ readonly flowToolArgs: 4000;
47
+ /** A wait or reminder said in words ("3 days"), and a time zone's name. */
48
+ readonly flowDuration: 40;
49
+ readonly flowTimeZone: 60;
50
+ /** A flow's name, and a flow file's about line, needs, parameter questions and answers, and scripts. */
51
+ readonly flowName: 80;
52
+ readonly flowFileAbout: 600;
53
+ readonly flowFileNeed: 80;
54
+ readonly flowParameter: 200;
55
+ readonly flowScriptAbout: 160;
56
+ readonly flowScriptBody: 20000;
57
+ readonly flowScriptFile: 200;
58
+ /** A trigger's settings: a button's label and questions, a schedule and the card it makes, GitHub, Linear, webhook and email terms. */
59
+ readonly triggerButton: 40;
60
+ readonly triggerQuestion: 80;
61
+ readonly triggerSchedule: 80;
62
+ readonly triggerTitle: 200;
63
+ readonly triggerDescription: 2000;
64
+ readonly triggerGithubRepo: 140;
65
+ readonly triggerLabel: 50;
66
+ readonly triggerBranch: 100;
67
+ readonly triggerTeam: 12;
68
+ readonly triggerState: 40;
69
+ readonly triggerWebhookTitle: 120;
70
+ readonly triggerWebhookField: 80;
71
+ readonly triggerFolder: 100;
72
+ readonly triggerSender: 300;
73
+ readonly triggerSubject: 100;
74
+ /** A schedule's secrets written as one line ("API_KEY, CRM_TOKEN"). */
75
+ readonly triggerSecrets: 500;
76
+ /** A card the lead adds to a flow: its title and description; a script it saves inline. */
77
+ readonly flowCardTitle: 200;
78
+ readonly flowCardDescription: 4000;
79
+ readonly flowScriptInline: 1600;
19
80
  /** A research report's summary, in UTF-8 bytes. */
20
81
  readonly reportSummary: 2500;
21
82
  /** What one flow step passes on to the steps after it. */
@@ -36,6 +97,15 @@ export declare const TEXT_LIMITS: {
36
97
  readonly acceptanceIdBytes: 40;
37
98
  readonly acceptanceStatementBytes: 1000;
38
99
  readonly acceptanceHowBytes: 500;
100
+ /** A builder's whole proof file, in UTF-8 bytes. */
101
+ readonly proofPayloadBytes: number;
102
+ /** A proof criterion's id, in UTF-8 bytes. */
103
+ readonly proofCriterionIdBytes: 40;
104
+ /** How a proof criterion was met, in UTF-8 bytes. */
105
+ readonly proofHowBytes: 500;
106
+ /** One proof line — a criterion statement, evidence ref, check command or summary, changed path, caveat, or
107
+ * screenshot path or caption — in UTF-8 bytes. */
108
+ readonly proofLineBytes: 300;
39
109
  };
40
110
  export type TextLimitKey = keyof typeof TEXT_LIMITS;
41
111
  /** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
@@ -16,6 +16,67 @@ export const TEXT_LIMITS = {
16
16
  noteBytes: 16_000,
17
17
  /** A flow work zone's instructions; nothing is held back for the card's details (they are attached when long). */
18
18
  flowInstructions: 8_000,
19
+ /** A flow's zones and steps (src/contracts/flow.ts): a zone's name, a step's id as written, and a reference to another step. */
20
+ flowTitle: 60,
21
+ flowStepId: 32,
22
+ flowRef: 60,
23
+ /** A Message zone's message, or the comment an Update zone leaves. */
24
+ flowMessage: 1_000,
25
+ /** Who decides a "Person decides" zone (a sign-in name), and the AI teammate who works a zone, as written. */
26
+ flowDecider: 64,
27
+ flowTeammate: 40,
28
+ /** A Build zone's other project: its path. */
29
+ flowRepo: 1_000,
30
+ /** A script's or teammate's answer, a choice's button, a Sort zone's answer and what it means, its question, and a score's level. */
31
+ flowAnswer: 40,
32
+ flowChoice: 40,
33
+ flowSortMeans: 200,
34
+ flowSortQuestion: 300,
35
+ flowSortLevel: 120,
36
+ /** A Web request zone's address, one header's value and the body it sends (an email's body too). */
37
+ flowUrl: 2_000,
38
+ flowHeader: 500,
39
+ flowBody: 8_000,
40
+ /** An email's recipients and subject. */
41
+ flowEmailTo: 500,
42
+ flowEmailSubject: 200,
43
+ /** A tool zone's tool (MCP server), its function, and the arguments as JSON. */
44
+ flowToolServer: 64,
45
+ flowToolName: 100,
46
+ flowToolArgs: 4_000,
47
+ /** A wait or reminder said in words ("3 days"), and a time zone's name. */
48
+ flowDuration: 40,
49
+ flowTimeZone: 60,
50
+ /** A flow's name, and a flow file's about line, needs, parameter questions and answers, and scripts. */
51
+ flowName: 80,
52
+ flowFileAbout: 600,
53
+ flowFileNeed: 80,
54
+ flowParameter: 200,
55
+ flowScriptAbout: 160,
56
+ flowScriptBody: 20_000,
57
+ flowScriptFile: 200,
58
+ /** A trigger's settings: a button's label and questions, a schedule and the card it makes, GitHub, Linear, webhook and email terms. */
59
+ triggerButton: 40,
60
+ triggerQuestion: 80,
61
+ triggerSchedule: 80,
62
+ triggerTitle: 200,
63
+ triggerDescription: 2_000,
64
+ triggerGithubRepo: 140,
65
+ triggerLabel: 50,
66
+ triggerBranch: 100,
67
+ triggerTeam: 12,
68
+ triggerState: 40,
69
+ triggerWebhookTitle: 120,
70
+ triggerWebhookField: 80,
71
+ triggerFolder: 100,
72
+ triggerSender: 300,
73
+ triggerSubject: 100,
74
+ /** A schedule's secrets written as one line ("API_KEY, CRM_TOKEN"). */
75
+ triggerSecrets: 500,
76
+ /** A card the lead adds to a flow: its title and description; a script it saves inline. */
77
+ flowCardTitle: 200,
78
+ flowCardDescription: 4_000,
79
+ flowScriptInline: 1_600,
19
80
  /** A research report's summary, in UTF-8 bytes. */
20
81
  reportSummary: 2_500,
21
82
  /** What one flow step passes on to the steps after it. */
@@ -36,6 +97,15 @@ export const TEXT_LIMITS = {
36
97
  acceptanceIdBytes: 40,
37
98
  acceptanceStatementBytes: 1_000,
38
99
  acceptanceHowBytes: 500,
100
+ /** A builder's whole proof file, in UTF-8 bytes. */
101
+ proofPayloadBytes: 64 * 1024,
102
+ /** A proof criterion's id, in UTF-8 bytes. */
103
+ proofCriterionIdBytes: 40,
104
+ /** How a proof criterion was met, in UTF-8 bytes. */
105
+ proofHowBytes: 500,
106
+ /** One proof line — a criterion statement, evidence ref, check command or summary, changed path, caveat, or
107
+ * screenshot path or caption — in UTF-8 bytes. */
108
+ proofLineBytes: 300,
39
109
  };
40
110
  /** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
41
111
  * holds, well inside its 28 KB message. */