toolroll 0.9.33 → 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 (74) hide show
  1. package/CHANGELOG.md +57 -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.css +1 -1
  6. package/dist/browser/workspace.js +17 -17
  7. package/dist/browser-crew.js +1 -0
  8. package/dist/browser-workspace.d.ts +5 -3
  9. package/dist/builder.js +4 -7
  10. package/dist/chat-acceptance.d.ts +1 -1
  11. package/dist/cli.d.ts +1 -1
  12. package/dist/cli.js +124 -63
  13. package/dist/contracts/contract-test.d.ts +45 -0
  14. package/dist/contracts/contract-test.js +69 -0
  15. package/dist/contracts/contract.d.ts +87 -0
  16. package/dist/contracts/contract.js +308 -0
  17. package/dist/contracts/flow-propose.d.ts +325 -0
  18. package/dist/contracts/flow-propose.js +41 -0
  19. package/dist/contracts/flow.d.ts +19400 -0
  20. package/dist/contracts/flow.js +359 -0
  21. package/dist/contracts/handoff.d.ts +91 -0
  22. package/dist/contracts/handoff.js +82 -0
  23. package/dist/contracts/plan.d.ts +58 -0
  24. package/dist/contracts/plan.js +67 -0
  25. package/dist/contracts/proof.d.ts +136 -0
  26. package/dist/contracts/proof.js +110 -0
  27. package/dist/contracts/verification-receipt.d.ts +148 -0
  28. package/dist/contracts/verification-receipt.js +87 -0
  29. package/dist/demo.js +28 -11
  30. package/dist/desktop-update.d.ts +1 -1
  31. package/dist/evidence.d.ts +3 -40
  32. package/dist/evidence.js +7 -3
  33. package/dist/flow-gallery.d.ts +3 -2
  34. package/dist/flow-share.d.ts +10 -32
  35. package/dist/flow-share.js +65 -83
  36. package/dist/flow-starters.d.ts +2 -1
  37. package/dist/flow-triggers.d.ts +15 -72
  38. package/dist/flow-triggers.js +94 -83
  39. package/dist/flows.d.ts +37 -218
  40. package/dist/flows.js +350 -385
  41. package/dist/mate-tools.js +14 -44
  42. package/dist/mcp-connect.d.ts +3 -0
  43. package/dist/mcp-connect.js +16 -4
  44. package/dist/operate.js +83 -12
  45. package/dist/plan.d.ts +12 -12
  46. package/dist/plan.js +109 -72
  47. package/dist/planner-source.js +2 -1
  48. package/dist/planner.js +55 -25
  49. package/dist/process-recovery-eligibility.d.ts +2 -2
  50. package/dist/process-recovery-eligibility.js +13 -8
  51. package/dist/project-admission.d.ts +93 -0
  52. package/dist/project-admission.js +134 -0
  53. package/dist/project-delete-ui.js +5 -2
  54. package/dist/proof.d.ts +15 -91
  55. package/dist/proof.js +90 -249
  56. package/dist/result-evidence-readers.d.ts +42 -0
  57. package/dist/result-evidence-readers.js +91 -0
  58. package/dist/runner.d.ts +10 -0
  59. package/dist/runner.js +16 -0
  60. package/dist/scope.js +4 -3
  61. package/dist/serve.d.ts +4 -0
  62. package/dist/serve.js +112 -160
  63. package/dist/setup-guide.d.ts +2 -2
  64. package/dist/store.d.ts +2 -0
  65. package/dist/store.js +4 -0
  66. package/dist/surface.js +16 -3
  67. package/dist/task-review-brief.d.ts +75 -0
  68. package/dist/task-review-brief.js +179 -0
  69. package/dist/text-limits.d.ts +87 -0
  70. package/dist/text-limits.js +86 -0
  71. package/dist/verification-evidence.js +24 -12
  72. package/dist/when-html.d.ts +2 -0
  73. package/dist/when-html.js +13 -0
  74. package/package.json +3 -2
@@ -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/store.d.ts CHANGED
@@ -6980,6 +6980,8 @@ export declare class Store {
6980
6980
  /** Remember a project was opened. Upsert keeps added_at; recency always moves.
6981
6981
  * A project added for the first time starts on Quick checks. */
6982
6982
  upsertProject(path: string, name: string, now: Date): void;
6983
+ /** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
6984
+ forgetProject(path: string): void;
6983
6985
  /** Most recently opened first — the opener page's order. */
6984
6986
  listProjects(): {
6985
6987
  path: string;
package/dist/store.js CHANGED
@@ -20463,6 +20463,10 @@ export class Store {
20463
20463
  stampNewProjectLevel(this, path, now);
20464
20464
  });
20465
20465
  }
20466
+ /** A project removed from the lists (project-admission.ts): only its row goes; its tasks, results and settings stay. */
20467
+ forgetProject(path) {
20468
+ this.db.prepare("DELETE FROM project WHERE path = ?").run(path);
20469
+ }
20466
20470
  /** Most recently opened first — the opener page's order. */
20467
20471
  listProjects() {
20468
20472
  return this.db
package/dist/surface.js CHANGED
@@ -81,8 +81,14 @@ export const COMMAND_GUIDE = [
81
81
  operator('conversation read', 'explicitly record your read position at --message within --conversation'),
82
82
  operator('conversation follow', 'enable or pause automatic lead summaries with --conversation and --enabled true|false, within the existing paid chat authorization; passive chat --follow only reads updates'),
83
83
  operator('conversation stop', 'request stopping the exact --message in --conversation; retain uncertain work until its owner settles it'),
84
- operator("repos add", "connect a repository to the installation"),
85
- operator("repos remove", "disconnect a repository"),
84
+ { invocation: "repos add", synopsis: "add a project: a Git repository, in the console at once; the lead adds with its token", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
85
+ positionals: [{ name: "path", required: false, meaning: "the repository (default: the one you are in)" }],
86
+ flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }],
87
+ notableReasons: ["not-git", "unavailable", "refused", "locked"] },
88
+ { invocation: "repos remove", synopsis: "remove a project from the console and the builder; its tasks and results stay saved", audience: "agent", agentMayInvoke: true, mutation: "identity-idempotent",
89
+ positionals: [{ name: "path", required: true, meaning: "the repository" }],
90
+ flags: [jsonFlag, { name: "token", takesValue: true, meaning: "the lead token (or TOOLROLL_LEAD_TOKEN)" }],
91
+ notableReasons: ["refused", "locked"] },
86
92
  operator("repos add-from-github", "preview, clone from GitHub, and connect - the console onboarding ceremony as a CLI verb"),
87
93
  { invocation: "contract", synopsis: "the machine contract: envelope version + capability tokens; --commands dumps this guide", audience: "agent", agentMayInvoke: true, mutation: "none",
88
94
  flags: [jsonFlag, { name: "commands", takesValue: false, meaning: "dump the declared command guide" }] },
@@ -272,7 +278,14 @@ export const COMMAND_GUIDE = [
272
278
  operator("task approve", "the yes — nothing builds without one; binds to the scope digest"),
273
279
  operator("task requeue", "exit a stall: incidents resolved, strikes cleared, queued again"),
274
280
  operator("task regate", "explicitly rerun the approved check on the last exact commit — no agent or automatic retry"),
275
- 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"] },
276
289
  operator("task accept", "record an explicit acceptance with limitations; does not change checks or prove deployment"),
277
290
  operator("task repair", "inspect or approve an existing historical repair draft; use task revise for new feedback"),
278
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,11 +16,98 @@ 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. */
22
83
  readonly stageOutput: 12000;
84
+ /** The planner's whole handoff file, in UTF-8 bytes. */
85
+ readonly planPayloadBytes: number;
86
+ /** The plan document (markdown), in UTF-8 bytes. */
87
+ readonly planDocumentBytes: number;
88
+ /** The plan document's Approach section. */
89
+ readonly planApproach: 2000;
90
+ /** One Milestones, Dependencies, Risks or Proof item in the plan document. */
91
+ readonly planItem: 600;
92
+ /** One path a plan expects to touch. */
93
+ readonly planTouch: 200;
94
+ /** A planner's amendment note: why the filed contract must change. */
95
+ readonly planAmendment: 1000;
96
+ /** An acceptance criterion's id, statement and advisory how, in UTF-8 bytes. */
97
+ readonly acceptanceIdBytes: 40;
98
+ readonly acceptanceStatementBytes: 1000;
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;
23
109
  };
110
+ export type TextLimitKey = keyof typeof TEXT_LIMITS;
24
111
  /** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
25
112
  * holds, well inside its 28 KB message. */
26
113
  export declare const PLATFORM_LIMITS: {
@@ -16,10 +16,96 @@ 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. */
22
83
  stageOutput: 12_000,
84
+ /** The planner's whole handoff file, in UTF-8 bytes. */
85
+ planPayloadBytes: 64 * 1024,
86
+ /** The plan document (markdown), in UTF-8 bytes. */
87
+ planDocumentBytes: 16 * 1024,
88
+ /** The plan document's Approach section. */
89
+ planApproach: 2_000,
90
+ /** One Milestones, Dependencies, Risks or Proof item in the plan document. */
91
+ planItem: 600,
92
+ /** One path a plan expects to touch. */
93
+ planTouch: 200,
94
+ /** A planner's amendment note: why the filed contract must change. */
95
+ planAmendment: 1_000,
96
+ /** An acceptance criterion's id, statement and advisory how, in UTF-8 bytes. */
97
+ acceptanceIdBytes: 40,
98
+ acceptanceStatementBytes: 1_000,
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,
23
109
  };
24
110
  /** One chat message on each platform. Longer text is split across messages, never cut. Teams: the text one Adaptive Card
25
111
  * holds, well inside its 28 KB message. */
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { adjudicate } from "./proof.js";
8
8
  import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
9
9
  import { readVerifiedArtifact, storeEvidence, scanForSecrets, redactSecretLines } from "./evidence.js";
10
+ import { readVerificationReceipt } from "./contracts/verification-receipt.js";
10
11
  import { liveQuickCommand, quickVerifyKey, runCheckLevel } from "./check-levels.js";
11
12
  export const VERIFICATION_RECEIPT_CAPTURE = "machine verification receipt v1";
12
13
  export const REVIEW_GATE_NAME = "REVIEW-VERIFICATION.json";
@@ -18,11 +19,16 @@ export function sealVerificationReceipt(store, root, runId, head, command, resul
18
19
  const log = store.artifactsFor(runId).find(a => a.kind === "check-log");
19
20
  if (!log)
20
21
  throw new Error("Verification receipt requires its retained log");
21
- const raw = JSON.stringify({
22
+ const receipt = {
22
23
  version: reusedFrom === undefined ? 1 : 2, run: runId, head, base: source.baseRevision, scopeDigest: source.scopeDigest,
23
24
  ...(reusedFrom === undefined ? {} : { reusedFrom, executedHere: false }),
24
25
  command, result, log: binding(log),
25
- }, null, 1);
26
+ };
27
+ const checked = readVerificationReceipt(receipt);
28
+ if (!checked.ok)
29
+ throw new Error(`The verification receipt does not match its contract: ${checked.issues.map(issue => issue.line).join("; ")}`);
30
+ // Sealed in the writer's own key order, as every receipt before the schema: its bytes and digests stay comparable.
31
+ const raw = JSON.stringify(receipt, null, 1);
26
32
  const hits = scanForSecrets(raw);
27
33
  storeEvidence(store, root, runId, "structured-output", "verification-receipt.json", Buffer.from(hits.length ? redactSecretLines(raw, hits) : raw), reusedFrom === undefined ? VERIFICATION_RECEIPT_CAPTURE : "machine verification reuse v1", now, { captureStatus: "ok", redacted: hits.length > 0 });
28
34
  }
@@ -73,21 +79,27 @@ export function verificationEvidence(store, root, runId) {
73
79
  catch {
74
80
  return fail("The verification receipt cannot be read.");
75
81
  }
76
- if ((receipt?.version !== 1 && receipt?.version !== 2) || receipt.run !== runId || receipt.head !== source.headRevision || receipt.base !== source.baseRevision || receipt.scopeDigest !== source.scopeDigest || JSON.stringify(receipt.command) !== JSON.stringify(command) || JSON.stringify(receipt.log) !== JSON.stringify(binding(log)))
82
+ // The schema checks the shape; the bindings below check it against the store. The receipt itself stays exactly
83
+ // as sealed: its bytes are this view's bytes and part of its digest.
84
+ const read = readVerificationReceipt(receipt);
85
+ if (!read.ok)
86
+ return fail(`The verification receipt is malformed: ${read.issues.map(issue => issue.line).join("; ")}`);
87
+ const checked = read.value;
88
+ if (checked.run !== runId || checked.head !== source.headRevision || checked.base !== source.baseRevision || checked.scopeDigest !== source.scopeDigest || JSON.stringify(receipt.command) !== JSON.stringify(command) || JSON.stringify(receipt.log) !== JSON.stringify(binding(log)))
77
89
  return fail("The candidate, approved command or retained log changed since verification.");
78
90
  if (receipt.version === 2) {
79
91
  try {
80
92
  const brief = observationBrief(store, root, source.taskRef);
81
- const reused = receipt.reusedFrom;
93
+ const reused = checked.reusedFrom;
82
94
  if (!brief || !readObservationEvidence(store, root, runId) || !reused || reused.run !== brief.sourceRun || reused.run >= runId || reused.digest !== brief.gateDigest ||
83
- source.baseRevision !== brief.head || source.headRevision !== brief.head || receipt.executedHere !== false)
95
+ source.baseRevision !== brief.head || source.headRevision !== brief.head || checked.executedHere !== false)
84
96
  return fail("The reused gate is not bound to an unchanged observation follow-up.");
85
97
  const original = verificationEvidence(store, root, reused.run);
86
98
  if (!original.ok || !original.bytes || original.digest !== reused.digest)
87
99
  return fail("The original passing gate no longer verifies.");
88
100
  const previous = JSON.parse(original.bytes);
89
101
  const originalLog = store.getArtifact(previous.log.artifactId);
90
- if (previous.head !== source.headRevision || previous.result.exitCode !== 0 || JSON.stringify(previous.command) !== JSON.stringify(command) ||
102
+ if (previous.head !== source.headRevision || !("exitCode" in previous.result) || previous.result.exitCode !== 0 || JSON.stringify(previous.command) !== JSON.stringify(command) ||
91
103
  JSON.stringify(previous.result) !== JSON.stringify(receipt.result) || originalLog?.sha256 !== log.sha256 ||
92
104
  originalLog.bytesOriginal !== log.bytesOriginal || originalLog.redacted !== log.redacted || originalLog.truncated !== log.truncated)
93
105
  return fail("The reused gate's candidate, command, result or copied log changed.");
@@ -96,10 +108,10 @@ export function verificationEvidence(store, root, runId) {
96
108
  return fail("The reused gate or its observations no longer verify.");
97
109
  }
98
110
  }
99
- else if (receipt.reusedFrom !== undefined)
111
+ else if (checked.reusedFrom !== undefined || checked.executedHere !== undefined)
100
112
  return fail("The gate reuse receipt has an unsupported version.");
101
- const result = receipt.result;
102
- if (result?.configured !== true || typeof result.ran !== "boolean" || (result.ran ? !Number.isInteger(result.exitCode) || result.exitCode < 0 || result.exitCode > 255 : result.attemptFailed !== true))
113
+ const result = checked.result;
114
+ if (!result.configured)
103
115
  return fail("The machine verification result is malformed.");
104
116
  if (verified && (!result.ran || result.exitCode !== 0))
105
117
  return fail("The passing machine verdict disagrees with its verification receipt.");
@@ -142,8 +154,8 @@ function sealedCommandKey(root, receipts) {
142
154
  if (!sealed.ok)
143
155
  return null;
144
156
  try {
145
- const repo = JSON.parse(sealed.content.toString("utf8")).command?.repo;
146
- return typeof repo === "string" ? repo : null;
157
+ const read = readVerificationReceipt(JSON.parse(sealed.content.toString("utf8")));
158
+ return read.ok ? read.value.command.repo : null;
147
159
  }
148
160
  catch {
149
161
  return null;
@@ -244,7 +256,7 @@ export function reuseObservationVerification(store, root, runId, now) {
244
256
  if (!original.ok || !original.bytes || original.digest !== brief.gateDigest)
245
257
  throw Error("The original passing gate is unavailable or changed.");
246
258
  const receipt = JSON.parse(original.bytes), log = store.getArtifact(receipt.log.artifactId);
247
- if (!log || receipt.head !== brief.head || receipt.result?.ran !== true || receipt.result.exitCode !== 0)
259
+ if (!log || receipt.head !== brief.head || !("ran" in receipt.result) || receipt.result.ran !== true || receipt.result.exitCode !== 0)
248
260
  throw Error("The source gate did not pass this candidate.");
249
261
  const read = readVerifiedArtifact(root, log);
250
262
  if (!read.ok)
@@ -10,6 +10,8 @@ export declare function deadlineWords(deadline: {
10
10
  at: string;
11
11
  label: string;
12
12
  }, now?: Date, zone?: string): string;
13
+ /** How long ago, in one short mark: "now", "4m", "2h", "3d". A future stamp reads "now"; an unreadable one, "". */
14
+ export declare function shortAge(iso: string, now?: Date): string;
13
15
  /** The exact minute in `zone` ("2026-09-30 16:39", with " UTC" when it is UTC): a time's title, one hover away. */
14
16
  export declare function fullWhen(iso: string, zone?: string): string;
15
17
  /** Every server-rendered stamp (`whenHtml`'s `<time data-when>`) inside `root`, reworded by the one formatter in the