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
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The planner's handoff: one schema for the file a planner writes, the JSON Schema it is given (`--json-schema`) and
3
+ * the parser that reads it back (`parsePlan` in plan.ts). The rules JSON Schema cannot state — the plan document's
4
+ * sections, UTF-8 byte limits, control characters, duplicate criterion ids, the Proof naming every criterion and the
5
+ * silent-amendment contract check — run in plain code after parsing, each with a named error.
6
+ */
7
+ import { z } from "zod";
8
+ import { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "../scope.js";
9
+ import { limited, readVersioned, toModelSchema, versioned } from "./contract.js";
10
+ /** How many paths a plan may say it touches. */
11
+ export const PLAN_TOUCHES = 32;
12
+ /** One acceptance criterion: the scope's own shape (scope.ts `AcceptanceCriterion`), `how` advisory and unsigned. */
13
+ export const acceptanceCriterionSchema = z.strictObject({
14
+ id: limited("acceptance id", "acceptanceIdBytes").min(1),
15
+ statement: limited("acceptance statement", "acceptanceStatementBytes").min(1),
16
+ how: limited("acceptance how", "acceptanceHowBytes").nullable().optional(),
17
+ evidence: z.array(z.enum(EVIDENCE_KINDS)).min(1).max(EVIDENCE_KINDS.length),
18
+ });
19
+ export const PLAN_VERSION = 1;
20
+ export const planSchema = versioned(PLAN_VERSION, {
21
+ goal: limited("goal", "goal").min(1),
22
+ outOfScope: limited("outOfScope", "goal").nullable().optional(),
23
+ touches: z.array(limited("touch", "planTouch").min(1)).max(PLAN_TOUCHES).optional(),
24
+ acceptance: z.array(acceptanceCriterionSchema).min(1).max(ACCEPTANCE_LIMITS.criteria),
25
+ plan: limited("plan", "planDocumentBytes").min(1),
26
+ amendment: limited("amendment", "planAmendment").nullable().optional(),
27
+ });
28
+ /** What the planner's `--json-schema` is: the plan schema, exactly. */
29
+ export const PLAN_MODEL_SCHEMA = toModelSchema(planSchema);
30
+ const PLAN_FIELDS = ["goal", "outOfScope", "touches", "acceptance", "plan", "amendment"];
31
+ const CRITERION_FIELDS = ["id", "statement", "how", "evidence"];
32
+ function pick(body, fields) {
33
+ const out = {};
34
+ for (const field of fields) {
35
+ if (Object.prototype.hasOwnProperty.call(body, field) && body[field] !== undefined)
36
+ out[field] = body[field];
37
+ }
38
+ return out;
39
+ }
40
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
41
+ /**
42
+ * A plan written before plans carried `version` (every plan through 0.9.33), as version 1. The old parser read only
43
+ * the fields it knew and ignored any others, read a null goal, plan or acceptance as missing and null touches as
44
+ * none; this keeps exactly that, so every plan that parsed then parses now. Values themselves are never rewritten.
45
+ */
46
+ export function upgradeUnversionedPlan(body) {
47
+ const out = { version: PLAN_VERSION, ...pick(body, PLAN_FIELDS) };
48
+ for (const field of ["goal", "plan", "touches", "acceptance"]) {
49
+ if (out[field] === null)
50
+ delete out[field];
51
+ }
52
+ if (Array.isArray(out["acceptance"])) {
53
+ out["acceptance"] = out["acceptance"].map(criterion => (isRecord(criterion) ? pick(criterion, CRITERION_FIELDS) : criterion));
54
+ }
55
+ return out;
56
+ }
57
+ export const PLAN_UPGRADES = { 0: upgradeUnversionedPlan };
58
+ /** The payload as version 1, upgraded when it was saved unversioned; null for anything that is not an object. */
59
+ export function planPayloadBody(input) {
60
+ if (!isRecord(input))
61
+ return null;
62
+ return Object.prototype.hasOwnProperty.call(input, "version") ? input : upgradeUnversionedPlan(input);
63
+ }
64
+ /** Read a plan payload: version 1 as itself, an unversioned one upgraded, a newer one refused plainly. */
65
+ export function readPlanPayload(input) {
66
+ return readVersioned(planSchema, input, PLAN_UPGRADES);
67
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The builder's proof: one schema for the file a builder writes (the proof manifest), the `proof.json` the machine
3
+ * stores, and the reader that reads both back (`parseProof` in proof.ts). The rules JSON Schema cannot state — UTF-8
4
+ * byte limits, blank text, control characters, relative screenshot paths and duplicate ids or paths — run in plain
5
+ * code after parsing, each with a path-named error.
6
+ */
7
+ import { z } from "zod";
8
+ import { type ContractResult } from "./contract.js";
9
+ /** How many entries each proof list may hold. Text limits live in TEXT_LIMITS. */
10
+ export declare const PROOF_COUNTS: {
11
+ readonly criteria: 12;
12
+ readonly evidencePerCriterion: 4;
13
+ readonly checks: 12;
14
+ readonly changed: 64;
15
+ readonly caveats: 8;
16
+ readonly screenshots: 8;
17
+ };
18
+ /** Every proof limit by its long-standing name: the counts above and the byte limits from TEXT_LIMITS. */
19
+ export declare const PROOF_LIMITS: {
20
+ readonly criteria: 12;
21
+ readonly evidencePerCriterion: 4;
22
+ readonly checks: 12;
23
+ readonly changed: 64;
24
+ readonly caveats: 8;
25
+ readonly screenshots: 8;
26
+ readonly payload: number;
27
+ readonly criterionId: 40;
28
+ readonly criterionStatement: 300;
29
+ readonly criterionHow: 500;
30
+ readonly evidenceRef: 300;
31
+ readonly checkCommand: 300;
32
+ readonly checkSummary: 300;
33
+ readonly changedPath: 300;
34
+ readonly caveat: 300;
35
+ readonly screenshotPath: 300;
36
+ readonly screenshotCaption: 300;
37
+ };
38
+ export declare const CRITERION_VERDICTS: readonly ["met", "not-met", "not-checked", "pending-verification"];
39
+ /** One typed reference a criterion cites: an existing check's command, screenshot's path, a path in `changed`, or
40
+ * (kind `manual-review`) free text pointing at nothing machine-checkable. */
41
+ export declare const criterionEvidenceSchema: z.ZodObject<{
42
+ kind: z.ZodEnum<{
43
+ "changed-path": "changed-path";
44
+ check: "check";
45
+ "manual-review": "manual-review";
46
+ screenshot: "screenshot";
47
+ }>;
48
+ ref: z.ZodString;
49
+ }, z.core.$strict>;
50
+ /** One criterion the proof answers. `pending-verification` asserts all agent-owned work is met and only the
51
+ * machine-owned final check remains. */
52
+ export declare const proofCriterionSchema: z.ZodObject<{
53
+ id: z.ZodString;
54
+ statement: z.ZodString;
55
+ how: z.ZodString;
56
+ verdict: z.ZodEnum<{
57
+ met: "met";
58
+ "not-checked": "not-checked";
59
+ "not-met": "not-met";
60
+ "pending-verification": "pending-verification";
61
+ }>;
62
+ evidence: z.ZodArray<z.ZodObject<{
63
+ kind: z.ZodEnum<{
64
+ "changed-path": "changed-path";
65
+ check: "check";
66
+ "manual-review": "manual-review";
67
+ screenshot: "screenshot";
68
+ }>;
69
+ ref: z.ZodString;
70
+ }, z.core.$strict>>;
71
+ }, z.core.$strict>;
72
+ export declare const proofCheckSchema: z.ZodObject<{
73
+ command: z.ZodString;
74
+ summary: z.ZodString;
75
+ exitCode: z.ZodInt;
76
+ }, z.core.$strict>;
77
+ /** A screenshot the proof claims; its path is where the file lived in the worktree, its bytes are checked by the
78
+ * caller. */
79
+ export declare const proofScreenshotSchema: z.ZodObject<{
80
+ path: z.ZodString;
81
+ caption: z.ZodString;
82
+ }, z.core.$strict>;
83
+ export declare const PROOF_VERSION = 2;
84
+ /** The proof as stored and as every reader returns it: every list present, nothing unknown. */
85
+ export declare const proofSchema: z.ZodObject<{
86
+ version: z.ZodLiteral<2>;
87
+ criteria: z.ZodArray<z.ZodObject<{
88
+ id: z.ZodString;
89
+ statement: z.ZodString;
90
+ how: z.ZodString;
91
+ verdict: z.ZodEnum<{
92
+ met: "met";
93
+ "not-checked": "not-checked";
94
+ "not-met": "not-met";
95
+ "pending-verification": "pending-verification";
96
+ }>;
97
+ evidence: z.ZodArray<z.ZodObject<{
98
+ kind: z.ZodEnum<{
99
+ "changed-path": "changed-path";
100
+ check: "check";
101
+ "manual-review": "manual-review";
102
+ screenshot: "screenshot";
103
+ }>;
104
+ ref: z.ZodString;
105
+ }, z.core.$strict>>;
106
+ }, z.core.$strict>>;
107
+ checks: z.ZodArray<z.ZodObject<{
108
+ command: z.ZodString;
109
+ summary: z.ZodString;
110
+ exitCode: z.ZodInt;
111
+ }, z.core.$strict>>;
112
+ changed: z.ZodArray<z.ZodString>;
113
+ caveats: z.ZodArray<z.ZodString>;
114
+ screenshots: z.ZodArray<z.ZodObject<{
115
+ path: z.ZodString;
116
+ caption: z.ZodString;
117
+ }, z.core.$strict>>;
118
+ }, z.core.$strict>;
119
+ export type ParsedProof = z.infer<typeof proofSchema>;
120
+ export type ParsedCriterion = z.infer<typeof proofCriterionSchema>;
121
+ export type CriterionEvidenceRef = z.infer<typeof criterionEvidenceSchema>;
122
+ export type ParsedCheck = z.infer<typeof proofCheckSchema>;
123
+ export type ParsedScreenshot = z.infer<typeof proofScreenshotSchema>;
124
+ /**
125
+ * A version 1 proof — what a builder writes (`{ "version": 1, "screenshots": [...] }`) and every proof stored through
126
+ * 0.9.34 — as version 2. Version 1 read only the fields it knew and ignored any others, and read a missing or null
127
+ * list (a criterion's `evidence` too) as empty; this keeps exactly that. Values themselves are never rewritten.
128
+ */
129
+ export declare function upgradeProofV1(body: Record<string, unknown>): Record<string, unknown>;
130
+ export declare const PROOF_UPGRADES: {
131
+ readonly 1: typeof upgradeProofV1;
132
+ };
133
+ /** The payload as version 2 (upgraded from version 1 when needed), for the plain-code rules; null for a non-object. */
134
+ export declare function proofPayloadBody(input: unknown): Record<string, unknown> | null;
135
+ /** Read a proof payload: version 2 as itself, version 1 upgraded, a newer one refused plainly. */
136
+ export declare function readProofPayload(input: unknown): ContractResult<ParsedProof>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The builder's proof: one schema for the file a builder writes (the proof manifest), the `proof.json` the machine
3
+ * stores, and the reader that reads both back (`parseProof` in proof.ts). The rules JSON Schema cannot state — UTF-8
4
+ * byte limits, blank text, control characters, relative screenshot paths and duplicate ids or paths — run in plain
5
+ * code after parsing, each with a path-named error.
6
+ */
7
+ import { z } from "zod";
8
+ import { TEXT_LIMITS } from "../text-limits.js";
9
+ import { EVIDENCE_KINDS } from "../scope.js";
10
+ import { limited, readVersioned, versioned } from "./contract.js";
11
+ /** How many entries each proof list may hold. Text limits live in TEXT_LIMITS. */
12
+ export const PROOF_COUNTS = { criteria: 12, evidencePerCriterion: 4, checks: 12, changed: 64, caveats: 8, screenshots: 8 };
13
+ /** Every proof limit by its long-standing name: the counts above and the byte limits from TEXT_LIMITS. */
14
+ export const PROOF_LIMITS = {
15
+ payload: TEXT_LIMITS.proofPayloadBytes,
16
+ ...PROOF_COUNTS,
17
+ criterionId: TEXT_LIMITS.proofCriterionIdBytes,
18
+ criterionStatement: TEXT_LIMITS.proofLineBytes,
19
+ criterionHow: TEXT_LIMITS.proofHowBytes,
20
+ evidenceRef: TEXT_LIMITS.proofLineBytes,
21
+ checkCommand: TEXT_LIMITS.proofLineBytes,
22
+ checkSummary: TEXT_LIMITS.proofLineBytes,
23
+ changedPath: TEXT_LIMITS.proofLineBytes,
24
+ caveat: TEXT_LIMITS.proofLineBytes,
25
+ screenshotPath: TEXT_LIMITS.proofLineBytes,
26
+ screenshotCaption: TEXT_LIMITS.proofLineBytes,
27
+ };
28
+ export const CRITERION_VERDICTS = ["met", "not-met", "not-checked", "pending-verification"];
29
+ const line = (field) => limited(field, "proofLineBytes").min(1);
30
+ /** One typed reference a criterion cites: an existing check's command, screenshot's path, a path in `changed`, or
31
+ * (kind `manual-review`) free text pointing at nothing machine-checkable. */
32
+ export const criterionEvidenceSchema = z.strictObject({
33
+ kind: z.enum(EVIDENCE_KINDS),
34
+ ref: line("evidence ref"),
35
+ });
36
+ /** One criterion the proof answers. `pending-verification` asserts all agent-owned work is met and only the
37
+ * machine-owned final check remains. */
38
+ export const proofCriterionSchema = z.strictObject({
39
+ id: limited("criterion id", "proofCriterionIdBytes").min(1),
40
+ statement: line("criterion statement"),
41
+ how: limited("criterion how", "proofHowBytes").min(1),
42
+ verdict: z.enum(CRITERION_VERDICTS),
43
+ evidence: z.array(criterionEvidenceSchema).max(PROOF_COUNTS.evidencePerCriterion),
44
+ });
45
+ export const proofCheckSchema = z.strictObject({
46
+ command: line("check command"),
47
+ summary: line("check summary"),
48
+ exitCode: z.int().min(0).max(255),
49
+ });
50
+ /** A screenshot the proof claims; its path is where the file lived in the worktree, its bytes are checked by the
51
+ * caller. */
52
+ export const proofScreenshotSchema = z.strictObject({
53
+ path: line("screenshot path"),
54
+ caption: line("screenshot caption"),
55
+ });
56
+ export const PROOF_VERSION = 2;
57
+ /** The proof as stored and as every reader returns it: every list present, nothing unknown. */
58
+ export const proofSchema = versioned(PROOF_VERSION, {
59
+ criteria: z.array(proofCriterionSchema).max(PROOF_COUNTS.criteria),
60
+ checks: z.array(proofCheckSchema).max(PROOF_COUNTS.checks),
61
+ changed: z.array(line("changed path")).max(PROOF_COUNTS.changed),
62
+ caveats: z.array(line("caveat")).max(PROOF_COUNTS.caveats),
63
+ screenshots: z.array(proofScreenshotSchema).max(PROOF_COUNTS.screenshots),
64
+ });
65
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
66
+ /** The listed fields of `body`, a null or missing one left out (the old parser read null text as missing). */
67
+ function pick(body, fields) {
68
+ const out = {};
69
+ for (const field of fields) {
70
+ if (body[field] !== undefined && body[field] !== null)
71
+ out[field] = body[field];
72
+ }
73
+ return out;
74
+ }
75
+ const each = (value, fields) => (Array.isArray(value) ? value.map(one => (isRecord(one) ? pick(one, fields) : one)) : value);
76
+ /**
77
+ * A version 1 proof — what a builder writes (`{ "version": 1, "screenshots": [...] }`) and every proof stored through
78
+ * 0.9.34 — as version 2. Version 1 read only the fields it knew and ignored any others, and read a missing or null
79
+ * list (a criterion's `evidence` too) as empty; this keeps exactly that. Values themselves are never rewritten.
80
+ */
81
+ export function upgradeProofV1(body) {
82
+ const list = (field) => (body[field] === undefined || body[field] === null ? [] : body[field]);
83
+ const criteria = list("criteria");
84
+ return {
85
+ version: PROOF_VERSION,
86
+ criteria: Array.isArray(criteria)
87
+ ? criteria.map(one => {
88
+ if (!isRecord(one))
89
+ return one;
90
+ const evidence = one["evidence"] === undefined || one["evidence"] === null ? [] : one["evidence"];
91
+ return { ...pick(one, ["id", "statement", "how", "verdict"]), evidence: each(evidence, ["kind", "ref"]) };
92
+ })
93
+ : criteria,
94
+ checks: each(list("checks"), ["command", "summary", "exitCode"]),
95
+ changed: list("changed"),
96
+ caveats: list("caveats"),
97
+ screenshots: each(list("screenshots"), ["path", "caption"]),
98
+ };
99
+ }
100
+ export const PROOF_UPGRADES = { 1: upgradeProofV1 };
101
+ /** The payload as version 2 (upgraded from version 1 when needed), for the plain-code rules; null for a non-object. */
102
+ export function proofPayloadBody(input) {
103
+ if (!isRecord(input))
104
+ return null;
105
+ return input["version"] === 1 ? upgradeProofV1(input) : input;
106
+ }
107
+ /** Read a proof payload: version 2 as itself, version 1 upgraded, a newer one refused plainly. */
108
+ export function readProofPayload(input) {
109
+ return readVersioned(proofSchema, input, PROOF_UPGRADES);
110
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The machine verification receipt (`verification-receipt.json`): one schema for what `sealVerificationReceipt`
3
+ * writes and what `verificationEvidence` reads back. Only the machine writes it, from typed values, so every receipt
4
+ * ever sealed already has exactly this shape; the bindings that need the store — the run, its candidate, the approved
5
+ * command, the retained log and a reused gate's source — are checked in plain code after parsing.
6
+ *
7
+ * Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
8
+ * reused by an unchanged observation follow-up. A direct receipt is still sealed as version 1, so its bytes (and the
9
+ * digests bound to them) read the same in every Toolroll that reads receipts.
10
+ */
11
+ import { z } from "zod";
12
+ import { type ContractResult } from "./contract.js";
13
+ export declare const VERIFICATION_FAILURES: readonly ["spawn-failed", "timed-out", "dependency-missing", "setup-stale", "setup-failed", "own-install-failed", "tracked-files-changed", "setup-changed-files", "checkout-moved", "cleanliness-unavailable", "dependency-still-missing", "retry-spawn-failed", "retry-timed-out", "custody-lost"];
14
+ /**
15
+ * What the repository's approved verification command did: not configured; configured but the run could not attempt
16
+ * it (`attemptFailed`, which downgrades to `short` rather than `refuted`: "we could not check" is not "the claim is
17
+ * false"); or ran, with its exit code.
18
+ */
19
+ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<{
20
+ configured: z.ZodLiteral<false>;
21
+ }, z.core.$strict>, z.ZodObject<{
22
+ configured: z.ZodLiteral<true>;
23
+ ran: z.ZodLiteral<false>;
24
+ attemptFailed: z.ZodLiteral<true>;
25
+ failure: z.ZodOptional<z.ZodEnum<{
26
+ "checkout-moved": "checkout-moved";
27
+ "cleanliness-unavailable": "cleanliness-unavailable";
28
+ "custody-lost": "custody-lost";
29
+ "dependency-missing": "dependency-missing";
30
+ "dependency-still-missing": "dependency-still-missing";
31
+ "own-install-failed": "own-install-failed";
32
+ "retry-spawn-failed": "retry-spawn-failed";
33
+ "retry-timed-out": "retry-timed-out";
34
+ "setup-changed-files": "setup-changed-files";
35
+ "setup-failed": "setup-failed";
36
+ "setup-stale": "setup-stale";
37
+ "spawn-failed": "spawn-failed";
38
+ "timed-out": "timed-out";
39
+ "tracked-files-changed": "tracked-files-changed";
40
+ }>>;
41
+ }, z.core.$strict>, z.ZodObject<{
42
+ configured: z.ZodLiteral<true>;
43
+ ran: z.ZodLiteral<true>;
44
+ exitCode: z.ZodInt;
45
+ setupReplayed: z.ZodOptional<z.ZodLiteral<true>>;
46
+ }, z.core.$strict>]>;
47
+ export type VerifyCommandFacts = z.infer<typeof verifyCommandFactsSchema>;
48
+ /** The approved verification grant the gate ran, exactly as the store held it. */
49
+ export declare const receiptCommandSchema: z.ZodObject<{
50
+ id: z.ZodInt;
51
+ repo: z.ZodString;
52
+ command: z.ZodString;
53
+ timeoutMs: z.ZodInt;
54
+ digest: z.ZodString;
55
+ recoverySetupDigest: z.ZodNullable<z.ZodString>;
56
+ approvedBy: z.ZodString;
57
+ approvedAt: z.ZodString;
58
+ revokedAt: z.ZodNullable<z.ZodString>;
59
+ revokedBy: z.ZodNullable<z.ZodString>;
60
+ }, z.core.$strict>;
61
+ /** The retained check log the receipt binds, by artifact id and content. */
62
+ export declare const receiptLogSchema: z.ZodObject<{
63
+ artifactId: z.ZodInt;
64
+ sha256: z.ZodString;
65
+ bytesStored: z.ZodInt;
66
+ bytesOriginal: z.ZodInt;
67
+ truncated: z.ZodBoolean;
68
+ redacted: z.ZodBoolean;
69
+ captureStatus: z.ZodNullable<z.ZodEnum<{
70
+ failed: "failed";
71
+ ok: "ok";
72
+ }>>;
73
+ }, z.core.$strict>;
74
+ export declare const RECEIPT_VERSION = 2;
75
+ /** The receipt as read: a reused gate names its source run and digest; a direct one names neither. */
76
+ export declare const verificationReceiptSchema: z.ZodObject<{
77
+ version: z.ZodLiteral<2>;
78
+ run: z.ZodInt;
79
+ head: z.ZodString;
80
+ base: z.ZodNullable<z.ZodString>;
81
+ scopeDigest: z.ZodNullable<z.ZodString>;
82
+ reusedFrom: z.ZodOptional<z.ZodObject<{
83
+ run: z.ZodInt;
84
+ digest: z.ZodString;
85
+ }, z.core.$strict>>;
86
+ executedHere: z.ZodOptional<z.ZodLiteral<false>>;
87
+ command: z.ZodObject<{
88
+ id: z.ZodInt;
89
+ repo: z.ZodString;
90
+ command: z.ZodString;
91
+ timeoutMs: z.ZodInt;
92
+ digest: z.ZodString;
93
+ recoverySetupDigest: z.ZodNullable<z.ZodString>;
94
+ approvedBy: z.ZodString;
95
+ approvedAt: z.ZodString;
96
+ revokedAt: z.ZodNullable<z.ZodString>;
97
+ revokedBy: z.ZodNullable<z.ZodString>;
98
+ }, z.core.$strict>;
99
+ result: z.ZodUnion<readonly [z.ZodObject<{
100
+ configured: z.ZodLiteral<false>;
101
+ }, z.core.$strict>, z.ZodObject<{
102
+ configured: z.ZodLiteral<true>;
103
+ ran: z.ZodLiteral<false>;
104
+ attemptFailed: z.ZodLiteral<true>;
105
+ failure: z.ZodOptional<z.ZodEnum<{
106
+ "checkout-moved": "checkout-moved";
107
+ "cleanliness-unavailable": "cleanliness-unavailable";
108
+ "custody-lost": "custody-lost";
109
+ "dependency-missing": "dependency-missing";
110
+ "dependency-still-missing": "dependency-still-missing";
111
+ "own-install-failed": "own-install-failed";
112
+ "retry-spawn-failed": "retry-spawn-failed";
113
+ "retry-timed-out": "retry-timed-out";
114
+ "setup-changed-files": "setup-changed-files";
115
+ "setup-failed": "setup-failed";
116
+ "setup-stale": "setup-stale";
117
+ "spawn-failed": "spawn-failed";
118
+ "timed-out": "timed-out";
119
+ "tracked-files-changed": "tracked-files-changed";
120
+ }>>;
121
+ }, z.core.$strict>, z.ZodObject<{
122
+ configured: z.ZodLiteral<true>;
123
+ ran: z.ZodLiteral<true>;
124
+ exitCode: z.ZodInt;
125
+ setupReplayed: z.ZodOptional<z.ZodLiteral<true>>;
126
+ }, z.core.$strict>]>;
127
+ log: z.ZodObject<{
128
+ artifactId: z.ZodInt;
129
+ sha256: z.ZodString;
130
+ bytesStored: z.ZodInt;
131
+ bytesOriginal: z.ZodInt;
132
+ truncated: z.ZodBoolean;
133
+ redacted: z.ZodBoolean;
134
+ captureStatus: z.ZodNullable<z.ZodEnum<{
135
+ failed: "failed";
136
+ ok: "ok";
137
+ }>>;
138
+ }, z.core.$strict>;
139
+ }, z.core.$strict>;
140
+ export type VerificationReceipt = z.infer<typeof verificationReceiptSchema>;
141
+ /** A version 1 receipt read as version 2: the same fields. Reuse is a version 2 field; a version 1 receipt naming it
142
+ * is refused by its reader, which still knows the version it was sealed as. */
143
+ export declare function upgradeDirectReceipt(body: Record<string, unknown>): Record<string, unknown>;
144
+ export declare const RECEIPT_UPGRADES: {
145
+ readonly 1: typeof upgradeDirectReceipt;
146
+ };
147
+ /** Read a receipt: version 2 as itself, version 1 as a direct gate, a newer one refused plainly. */
148
+ export declare function readVerificationReceipt(input: unknown): ContractResult<VerificationReceipt>;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The machine verification receipt (`verification-receipt.json`): one schema for what `sealVerificationReceipt`
3
+ * writes and what `verificationEvidence` reads back. Only the machine writes it, from typed values, so every receipt
4
+ * ever sealed already has exactly this shape; the bindings that need the store — the run, its candidate, the approved
5
+ * command, the retained log and a reused gate's source — are checked in plain code after parsing.
6
+ *
7
+ * Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
8
+ * reused by an unchanged observation follow-up. A direct receipt is still sealed as version 1, so its bytes (and the
9
+ * digests bound to them) read the same in every Toolroll that reads receipts.
10
+ */
11
+ import { z } from "zod";
12
+ import { readVersioned, versioned } from "./contract.js";
13
+ export const VERIFICATION_FAILURES = [
14
+ "spawn-failed",
15
+ "timed-out",
16
+ "dependency-missing",
17
+ "setup-stale",
18
+ "setup-failed",
19
+ "own-install-failed",
20
+ "tracked-files-changed",
21
+ "setup-changed-files",
22
+ "checkout-moved",
23
+ "cleanliness-unavailable",
24
+ "dependency-still-missing",
25
+ "retry-spawn-failed",
26
+ "retry-timed-out",
27
+ "custody-lost",
28
+ ];
29
+ /**
30
+ * What the repository's approved verification command did: not configured; configured but the run could not attempt
31
+ * it (`attemptFailed`, which downgrades to `short` rather than `refuted`: "we could not check" is not "the claim is
32
+ * false"); or ran, with its exit code.
33
+ */
34
+ export const verifyCommandFactsSchema = z.union([
35
+ z.strictObject({ configured: z.literal(false) }),
36
+ z.strictObject({ configured: z.literal(true), ran: z.literal(false), attemptFailed: z.literal(true), failure: z.enum(VERIFICATION_FAILURES).optional() }),
37
+ z.strictObject({ configured: z.literal(true), ran: z.literal(true), exitCode: z.int().min(0).max(255), setupReplayed: z.literal(true).optional() }),
38
+ ]);
39
+ /** The approved verification grant the gate ran, exactly as the store held it. */
40
+ export const receiptCommandSchema = z.strictObject({
41
+ id: z.int(),
42
+ repo: z.string(),
43
+ command: z.string(),
44
+ timeoutMs: z.int(),
45
+ digest: z.string(),
46
+ recoverySetupDigest: z.string().nullable(),
47
+ approvedBy: z.string(),
48
+ approvedAt: z.string(),
49
+ revokedAt: z.string().nullable(),
50
+ revokedBy: z.string().nullable(),
51
+ });
52
+ /** The retained check log the receipt binds, by artifact id and content. */
53
+ export const receiptLogSchema = z.strictObject({
54
+ artifactId: z.int(),
55
+ sha256: z.string(),
56
+ bytesStored: z.int(),
57
+ bytesOriginal: z.int(),
58
+ truncated: z.boolean(),
59
+ redacted: z.boolean(),
60
+ captureStatus: z.enum(["ok", "failed"]).nullable(),
61
+ });
62
+ export const RECEIPT_VERSION = 2;
63
+ const receiptFields = {
64
+ run: z.int(),
65
+ head: z.string(),
66
+ base: z.string().nullable(),
67
+ scopeDigest: z.string().nullable(),
68
+ };
69
+ /** The receipt as read: a reused gate names its source run and digest; a direct one names neither. */
70
+ export const verificationReceiptSchema = versioned(RECEIPT_VERSION, {
71
+ ...receiptFields,
72
+ reusedFrom: z.strictObject({ run: z.int(), digest: z.string() }).optional(),
73
+ executedHere: z.literal(false).optional(),
74
+ command: receiptCommandSchema,
75
+ result: verifyCommandFactsSchema,
76
+ log: receiptLogSchema,
77
+ });
78
+ /** A version 1 receipt read as version 2: the same fields. Reuse is a version 2 field; a version 1 receipt naming it
79
+ * is refused by its reader, which still knows the version it was sealed as. */
80
+ export function upgradeDirectReceipt(body) {
81
+ return { ...body, version: RECEIPT_VERSION };
82
+ }
83
+ export const RECEIPT_UPGRADES = { 1: upgradeDirectReceipt };
84
+ /** Read a receipt: version 2 as itself, version 1 as a direct gate, a newer one refused plainly. */
85
+ export function readVerificationReceipt(input) {
86
+ return readVersioned(verificationReceiptSchema, input, RECEIPT_UPGRADES);
87
+ }
package/dist/demo.js CHANGED
@@ -38,6 +38,7 @@ import { fileTaskProposal, fileRoutineProposal } from "./proposal.js";
38
38
  import { storeEvidence, budgetedStatJson, imageDimensions } from "./evidence.js";
39
39
  import { parseProof, adjudicate } from "./proof.js";
40
40
  import { sealVerificationReceipt } from "./verification-evidence.js";
41
+ import { HANDOFF_VERSION, readHandoffArtifact } from "./contracts/handoff.js";
41
42
  import { parseExecutionPlanDocument, milestonesOf } from "./plan.js";
42
43
  import { maybeTriggerRepair } from "./dispose.js";
43
44
  import { deflateSync } from "node:zlib";
@@ -84,8 +85,21 @@ index 11aa0b2..c44d1f7 100644
84
85
  + expect(settle(1005, 0.031)).toBe(31.16);
85
86
  + });
86
87
  `;
88
+ /** A demo run's handoff artifact: the run's own identity around the story's conclusion and lists, written through
89
+ * the handoff schema like a real one. */
90
+ function demoHandoffBytes(store, runId, told) {
91
+ const run = store.getRun(runId);
92
+ const head = run.headRevision ?? run.baseRevision ?? "0".repeat(40);
93
+ const read = readHandoffArtifact({
94
+ version: HANDOFF_VERSION, taskId: store.externalIdFor(run.taskRef) ?? String(run.taskRef), runId, provider: run.provider,
95
+ sessionId: null, branch: run.branch ?? "", worktree: run.worktree ?? "", base: run.baseRevision ?? head, head, ...told,
96
+ freshness: { stampedAt: run.startedAt, currentAsOf: head },
97
+ });
98
+ if (!read.ok)
99
+ throw new Error(`demo handoff: ${read.issues.map(issue => issue.line).join("; ")}`);
100
+ return Buffer.from(JSON.stringify(read.value, null, 2), "utf8");
101
+ }
87
102
  const DEMO_HANDOFF = {
88
- schema: 1,
89
103
  outcome: "built",
90
104
  committed: true,
91
105
  conclusion: "Fixed the payout rounding drift: settle() now rounds at cent precision instead of accumulating half-cent errors. Added boundary tests against the ledger fixtures. All 214 tests pass.",
@@ -123,7 +137,6 @@ index 2b3c4d5..8e9f0a1 100644
123
137
  };
124
138
  `;
125
139
  const DEMO_FAILED_HANDOFF = {
126
- schema: 1,
127
140
  outcome: "built",
128
141
  committed: true,
129
142
  conclusion: "Removed LEGACY_PAYOUT from settlement and config. The admin override still reads it, so one reference remains.",
@@ -193,7 +206,6 @@ index a1b2c3d..d4e5f6a 100644
193
206
  +export const EMPTY_STATE_ILLUSTRATION = "quiet-inbox";
194
207
  `;
195
208
  const DEMO_COPY_HANDOFF = {
196
- schema: 1,
197
209
  outcome: "built",
198
210
  committed: true,
199
211
  conclusion: "Rewrote the inbox's empty-state copy so it explains why the list is empty instead of just saying so.",
@@ -605,7 +617,7 @@ export function seedDemo(store, repos, evidenceRoot, now) {
605
617
  filesTruncated: false,
606
618
  };
607
619
  storeEvidence(store, evidenceRoot, doneRun, "diff-stat", "diff-stat.json", budgetedStatJson(stat), "parsed from git diff --numstat -z [demo: synthetic]", hoursAgo(8.5));
608
- storeEvidence(store, evidenceRoot, doneRun, "handoff", "handoff.json", Buffer.from(JSON.stringify(DEMO_HANDOFF, null, 2), "utf8"), "composed at completion [demo: synthetic]", hoursAgo(8.4));
620
+ storeEvidence(store, evidenceRoot, doneRun, "handoff", "handoff.json", demoHandoffBytes(store, doneRun, DEMO_HANDOFF), "composed at completion [demo: synthetic]", hoursAgo(8.4));
609
621
  // The evidence bundle (Priority 2): a validated proof, its claimed
610
622
  // screenshot stored as immutable image evidence, the plane's own re-run
611
623
  // check, and the closed verdict — computed once, exactly as the real
@@ -669,7 +681,7 @@ export function seedDemo(store, repos, evidenceRoot, now) {
669
681
  filesTruncated: false,
670
682
  };
671
683
  storeEvidence(store, evidenceRoot, failedRun, "diff-stat", "diff-stat.json", budgetedStatJson(failedStat), "parsed from git diff --numstat -z [demo: synthetic]", hoursAgo(5.6));
672
- storeEvidence(store, evidenceRoot, failedRun, "handoff", "handoff.json", Buffer.from(JSON.stringify(DEMO_FAILED_HANDOFF, null, 2), "utf8"), "composed at completion [demo: synthetic]", hoursAgo(5.6));
684
+ storeEvidence(store, evidenceRoot, failedRun, "handoff", "handoff.json", demoHandoffBytes(store, failedRun, DEMO_FAILED_HANDOFF), "composed at completion [demo: synthetic]", hoursAgo(5.6));
673
685
  storeEvidence(store, evidenceRoot, failedRun, "proof", "proof.json", Buffer.from(JSON.stringify(DEMO_FAILED_PROOF, null, 2), "utf8"), "agent-authored proof (validated, re-serialized) [demo: synthetic]", hoursAgo(5.6));
674
686
  const failedAdjudicated = adjudicate({
675
687
  proofArtifactPresent: true,
@@ -734,7 +746,7 @@ export function seedDemo(store, repos, evidenceRoot, now) {
734
746
  filesTruncated: false,
735
747
  };
736
748
  storeEvidence(store, evidenceRoot, copyReviewRun, "diff-stat", "diff-stat.json", budgetedStatJson(copyStat), "parsed from git diff --numstat -z [demo: synthetic]", hoursAgo(3.6));
737
- storeEvidence(store, evidenceRoot, copyReviewRun, "handoff", "handoff.json", Buffer.from(JSON.stringify(DEMO_COPY_HANDOFF, null, 2), "utf8"), "composed at completion [demo: synthetic]", hoursAgo(3.5));
749
+ storeEvidence(store, evidenceRoot, copyReviewRun, "handoff", "handoff.json", demoHandoffBytes(store, copyReviewRun, DEMO_COPY_HANDOFF), "composed at completion [demo: synthetic]", hoursAgo(3.5));
738
750
  storeEvidence(store, evidenceRoot, copyReviewRun, "proof", "proof.json", Buffer.from(JSON.stringify(DEMO_COPY_PROOF, null, 2), "utf8"), "agent-authored proof (validated, re-serialized) [demo: synthetic]", hoursAgo(3.5));
739
751
  const copyReviewProofParse = parseProof(JSON.stringify(DEMO_COPY_PROOF));
740
752
  const copyReviewAdjudicated = adjudicate({
@@ -815,10 +827,15 @@ export function seedDemo(store, repos, evidenceRoot, now) {
815
827
  };
816
828
  storeEvidence(store, evidenceRoot, reviewedRun, "diff-stat", "diff-stat.json", budgetedStatJson(reviewedStat), "parsed from git diff --numstat -z [demo: synthetic]", hoursAgo(4.6));
817
829
  const DEMO_REPAIR_HANDOFF = {
818
- summary: "Added a TODO comment marking the per-account lock — ran out of turns before wiring the actual guard.",
819
- filesTouched: ["src/payout-limiter.ts"],
830
+ outcome: "built",
831
+ committed: true,
832
+ conclusion: "Added a TODO comment marking the per-account lock — ran out of turns before wiring the actual guard.",
833
+ changes: ["Marked the per-account lock in src/payout-limiter.ts."],
834
+ verification: [],
835
+ followUps: [],
836
+ decisionsIncorporated: [],
820
837
  };
821
- storeEvidence(store, evidenceRoot, reviewedRun, "handoff", "handoff.json", Buffer.from(JSON.stringify(DEMO_REPAIR_HANDOFF, null, 2), "utf8"), "composed at completion [demo: synthetic]", hoursAgo(4.5));
838
+ storeEvidence(store, evidenceRoot, reviewedRun, "handoff", "handoff.json", demoHandoffBytes(store, reviewedRun, DEMO_REPAIR_HANDOFF), "composed at completion [demo: synthetic]", hoursAgo(4.5));
822
839
  const DEMO_REPAIR_PROOF = {
823
840
  version: 1,
824
841
  criteria: [
@@ -1389,7 +1406,7 @@ export function createDemoLead(input) {
1389
1406
  };
1390
1407
  const shotPath = `evidence/${plan.kind}.png`;
1391
1408
  const png = demoScreenshot(plan.screenshot.accent);
1392
- const handoff = { schema: 1, outcome: "built", committed: true, conclusion: plan.conclusion, changes: diffs.map(one => `Changed ${one.path}.`), verification: [plan.checks[0]], followUps: [], decisionsIncorporated: [] };
1409
+ const handoff = { outcome: "built", committed: true, conclusion: plan.conclusion, changes: diffs.map(one => `Changed ${one.path}.`), verification: [plan.checks[0]], followUps: [], decisionsIncorporated: [] };
1393
1410
  const proof = {
1394
1411
  version: 1,
1395
1412
  criteria: [
@@ -1404,7 +1421,7 @@ export function createDemoLead(input) {
1404
1421
  const synthetic = "[demo: scripted]";
1405
1422
  storeEvidence(store, evidenceRoot, run, "terminal-diff", "terminal-diff.patch", Buffer.from(diffs.map(one => one.patch).join(""), "utf8"), `git diff --no-ext-diff --no-textconv --no-color ${base.slice(0, 8)}..HEAD (exit 0) ${synthetic}`, at);
1406
1423
  storeEvidence(store, evidenceRoot, run, "diff-stat", "diff-stat.json", budgetedStatJson(stat), `parsed from git diff --numstat -z ${synthetic}`, at);
1407
- storeEvidence(store, evidenceRoot, run, "handoff", "handoff.json", Buffer.from(JSON.stringify(handoff, null, 2), "utf8"), `composed at completion ${synthetic}`, at);
1424
+ storeEvidence(store, evidenceRoot, run, "handoff", "handoff.json", demoHandoffBytes(store, run, handoff), `composed at completion ${synthetic}`, at);
1408
1425
  storeEvidence(store, evidenceRoot, run, "proof", "proof.json", Buffer.from(JSON.stringify(proof, null, 2), "utf8"), `agent-authored proof (validated, re-serialized) ${synthetic}`, at);
1409
1426
  storeEvidence(store, evidenceRoot, run, "screenshot", `screenshot-${plan.kind}.png`, png, `agent-claimed screenshot at ${shotPath} (validated png) ${synthetic}`, at);
1410
1427
  const log = `=== Attempt summary ===\n- Project check · attempt 1: (exit 0)\n\n=== Project check · attempt 1 ===\n$ ${DEMO_CHECK_COMMAND}\n(exit 0)\n\n--- stdout ---\n${plan.checkOutput}\n--- stderr ---\n`;