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,359 @@
1
+ /**
2
+ * Flows' one contract (docs/plans/zod-revamp.md, item 3): one schema per zone kind, as a discriminated union on `kind`,
3
+ * in each of the three vocabularies a flow is written in —
4
+ *
5
+ * - the saved drawing (`flowDefinitionSchema`): zones as the canvas and the store hold them, references by id;
6
+ * - steps (`flowStepSchema`): the lead's, `toolroll flows create/edit`'s, starters', kits' and the gallery's words,
7
+ * references by id or name (`goesTo`, `ifFails`), what a kept step leaves out carried over;
8
+ * - the flow file (`flowFileSchema`, docs/flow-file.schema.json): steps with an id and a place on the canvas;
9
+ *
10
+ * plus triggers as they are given (`triggerInputSchema`) and as they are saved (`triggerConfigSchema`). Types are
11
+ * `z.infer`, limits come from TEXT_LIMITS, and every object is strict, so an unknown key is refused by name. What JSON
12
+ * Schema cannot say — paths to zones that exist, no path back into its own zone, a merge only after a decision, a web
13
+ * address's host, tool arguments as JSON, time zones, schedules, secrets, people and scripts — is checked in plain code
14
+ * after parsing (flows.ts, flow-triggers.ts, flow-share.ts), each with a path-named error.
15
+ */
16
+ import { z } from "zod";
17
+ import { TEXT_LIMITS } from "../text-limits.js";
18
+ import { limited, toModelSchema, versioned } from "./contract.js";
19
+ export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "send", "choose", "done"];
20
+ export const FLOW_COLORS = ["slate", "blue", "violet", "amber", "green", "rose"];
21
+ /** How a Pull request zone merges once checks pass. */
22
+ export const FLOW_MERGE_METHODS = ["squash", "merge", "rebase"];
23
+ export const FLOW_TRIGGER_KINDS = ["button", "schedule", "github", "linear", "flow", "webhook", "email", "chat", "plane-review"];
24
+ export const FLOW_PLANNING = ["auto", "required", "skip"];
25
+ export const FLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
26
+ export const FLOW_WAIT_FOR = ["reply", "time", "hours"];
27
+ export const FLOW_RUN_IN = ["folder", "copy"];
28
+ export const SCRIPT_LANGUAGES = ["shell", "python", "node"];
29
+ /** Where a choose option that ends the card leads. */
30
+ export const FLOW_END = "end";
31
+ /** How many zones a flow has, options a choose zone offers, answers a sort or script picks from, and so on. */
32
+ export const FLOW_ZONES_MAX = 24;
33
+ export const CHOICES_MIN = 2, CHOICES_MAX = 4;
34
+ export const SORT_ANSWERS_MIN = 2, SORT_ANSWERS_MAX = 12, SORT_NOTES_MAX = 3, SORT_LEVELS_MIN = 2, SORT_LEVELS_MAX = 10;
35
+ export const ROUTES_MAX = 12, SECRETS_MAX = 10, HEADERS_MAX = 10;
36
+ /** The longest a zone waits or a limit runs: 30 days. */
37
+ export const LONGEST_WAIT_MINUTES = 30 * 24 * 60;
38
+ /** "Sure enough to act alone", as a fraction (0.5–0.99) or a percentage (50–99). */
39
+ export const SURE_AT_MIN = 0.5, SURE_AT_MAX = 0.99, SURE_AT_PERCENT_MAX = 99;
40
+ /** A zone's id: short, lowercase, dashes. */
41
+ export const ZONE_ID = /^[a-z0-9][a-z0-9-]{0,31}$/;
42
+ /** A project script's name: short, lowercase, dashes — how zones and chat refer to it. */
43
+ export const SCRIPT_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
44
+ /** A saved secret's name, in capitals. */
45
+ export const SECRET_NAME = /^[A-Z][A-Z0-9_]{0,39}$/;
46
+ /** A flow file parameter's id. */
47
+ export const PARAMETER_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
48
+ /** A time of day as zones save it, and as people write it. */
49
+ export const CLOCK = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
50
+ export const CLOCK_WRITTEN = /^\s*([01]?[0-9]|2[0-3]):([0-5][0-9])\s*$/;
51
+ /** No control characters (tabs and line breaks are fine). */
52
+ const PLAIN = /^[^\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]*$/;
53
+ /** No control characters and no hidden direction marks: text a flow file or trigger brings in from outside. */
54
+ const VISIBLE = /^[^\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f​-‏‪-‮⁦-⁩]*$/;
55
+ /** Words, within a TEXT_LIMITS bound, with no control characters. */
56
+ const words = (field, key) => limited(field, key).regex(PLAIN, { error: "can't contain control characters" });
57
+ const visible = (field, key) => limited(field, key).regex(VISIBLE, { error: "can't contain hidden characters" });
58
+ const zoneId = z.string().regex(ZONE_ID, { error: "must be a short id: lowercase letters, numbers and dashes" });
59
+ const scriptName = z.string().regex(SCRIPT_NAME, { error: "must be a script's name: lowercase letters, numbers and dashes" });
60
+ const secretName = z.string().regex(SECRET_NAME, { error: "must be a saved secret's name in capitals, like API_TOKEN" });
61
+ const minutes = z.int().min(1).max(LONGEST_WAIT_MINUTES);
62
+ // ------------------------------------------------------------------ the saved drawing
63
+ /** Where a zone sits on the canvas. */
64
+ export const flowZoneSchema = z.strictObject({
65
+ x: z.int().min(-20000).max(20000), y: z.int().min(-20000).max(20000), w: z.int().min(220).max(1200), h: z.int().min(160).max(1600), color: z.enum(FLOW_COLORS),
66
+ });
67
+ const stageRef = zoneId;
68
+ /** One answer a sort zone can pick: its name, what it means (what Jev reads), and the zone it sends the card to. */
69
+ export const flowSortAnswerSchema = z.strictObject({ answer: words("answer", "flowAnswer").min(1), means: words("means", "flowSortMeans").min(1), to: stageRef });
70
+ /** Something else a sort zone notes on the card: a score on a scale of levels, or a yes/no. */
71
+ export const flowSortNoteSchema = z.discriminatedUnion("kind", [
72
+ z.strictObject({ id: zoneId, kind: z.literal("score"), question: words("question", "flowSortQuestion").min(1), levels: z.array(words("level", "flowSortLevel").min(1)).min(SORT_LEVELS_MIN).max(SORT_LEVELS_MAX) }),
73
+ z.strictObject({ id: zoneId, kind: z.literal("yes-no"), question: words("question", "flowSortQuestion").min(1), levels: z.null() }),
74
+ ]);
75
+ /** sort: the question, its answers, how sure Jev must be to act alone (0.5–0.99), and what else it notes. */
76
+ export const flowSortSchema = z.strictObject({
77
+ question: words("question", "flowSortQuestion").min(1),
78
+ answers: z.array(flowSortAnswerSchema).min(SORT_ANSWERS_MIN).max(SORT_ANSWERS_MAX),
79
+ sureAt: z.number().min(SURE_AT_MIN).max(SURE_AT_MAX),
80
+ notes: z.array(flowSortNoteSchema).max(SORT_NOTES_MAX),
81
+ });
82
+ /** request: what is called. The address's scheme and host are fixed; fill-ins go in its path and query (encoded), headers and body. */
83
+ export const flowRequestSchema = z.strictObject({
84
+ method: z.enum(FLOW_METHODS), url: words("url", "flowUrl").min(1),
85
+ headers: z.object({}).catchall(words("header", "flowHeader").min(1)), body: words("body", "flowBody").min(1).nullable(),
86
+ });
87
+ /** email: who it goes to, the subject and the text — all with fill-ins. */
88
+ export const flowEmailSchema = z.strictObject({ to: words("to", "flowEmailTo").min(1), subject: words("subject", "flowEmailSubject").min(1), body: words("body", "flowBody").min(1) });
89
+ /** tool: which of the project's tools (MCP servers), which of its functions, and the arguments as JSON with fill-ins. */
90
+ export const flowToolSchema = z.strictObject({ server: words("server", "flowToolServer").min(1), name: words("name", "flowToolName").min(1), args: words("args", "flowToolArgs").min(1) });
91
+ /** wait: for a reply to the card's email (next: replied, onFail: no reply in time), for a set time (then next), or until
92
+ * the clock is between `from` and `to` (like 22:00–06:00), in the computer's time zone unless one is named. */
93
+ export const flowWaitSchema = z.strictObject({
94
+ for: z.enum(FLOW_WAIT_FOR),
95
+ /** reply, time: how long (a minute to 30 days); hours: 0. */
96
+ minutes: z.int().min(0).max(LONGEST_WAIT_MINUTES),
97
+ from: z.string().regex(CLOCK, { error: "must be a time like 22:00" }).optional(),
98
+ to: z.string().regex(CLOCK, { error: "must be a time like 06:00" }).optional(),
99
+ timeZone: z.string().min(1).max(TEXT_LIMITS.flowTimeZone).optional(),
100
+ });
101
+ /** A choose zone's option: its button's words, and the zone it leads to (FLOW_END closes the card as Ignored). */
102
+ export const flowChoiceSchema = z.strictObject({ label: words("label", "flowChoice").min(1), to: z.union([stageRef, z.literal(FLOW_END)]) });
103
+ /** A script's or teammate's answer and the zone it leads to. */
104
+ export const flowRouteSchema = z.strictObject({ answer: z.string().min(1).max(TEXT_LIMITS.flowAnswer).regex(/^[^\n]*$/, { error: "must be one line" }), to: stageRef });
105
+ /** A time limit on a zone: after this long, the person it waits on is reminded; a Holding, "Person decides" or "Person
106
+ * chooses" zone can also move the card on (`to`). */
107
+ const limitRemind = z.strictObject({ minutes, to: z.null() });
108
+ const limitMove = z.strictObject({ minutes, to: stageRef.nullable() });
109
+ /**
110
+ * The 18 zones, given how long instructions may be (written now: TEXT_LIMITS.flowInstructions; saved earlier: any
111
+ * length). Every zone carries the same first fields (null where a kind has no use for them) and its own after them, in
112
+ * the order the store has always saved them: a zone's digest is its JSON, so the order is part of the contract.
113
+ */
114
+ function zoneSchemas(instructions) {
115
+ const text = instructions.min(1).regex(PLAIN, { error: "can't contain control characters" });
116
+ const said = text.nullable(), nil = z.null();
117
+ const id = zoneId, title = words("title", "flowTitle").min(1), zone = flowZoneSchema, message = words("message", "flowMessage").min(1);
118
+ const next = stageRef.nullable(), onFail = stageRef.nullable();
119
+ const routes = z.array(flowRouteSchema).min(1).max(ROUTES_MAX).optional();
120
+ const remind = limitRemind.optional(), move = limitMove.optional();
121
+ return [
122
+ z.strictObject({ id, title, kind: z.literal("inbox"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, limit: move, next, onFail }),
123
+ z.strictObject({ id, title, kind: z.literal("task"), zone, instructions: text, planning: z.enum(FLOW_PLANNING), approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, repo: words("repo", "flowRepo").min(1).optional(), limit: remind, next, onFail }),
124
+ z.strictObject({ id, title, kind: z.literal("report"), zone, instructions: text, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, limit: remind, next, onFail }),
125
+ z.strictObject({ id, title, kind: z.literal("approval"), zone, instructions: said, planning: nil, approver: words("approver", "flowDecider").min(1).nullable(), toOwner: z.literal(true).optional(), message: message.nullable(), close: nil, script: nil, sort: nil, teammate: zoneId.optional(), limit: move, next, onFail }),
126
+ z.strictObject({ id, title, kind: z.literal("check"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: scriptName, runIn: z.enum(FLOW_RUN_IN).optional(), routes, secrets: z.array(secretName).min(1).max(SECRETS_MAX).optional(), sort: nil, limit: remind, next, onFail }),
127
+ z.strictObject({ id, title, kind: z.literal("pull-request"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, merge: z.enum(FLOW_MERGE_METHODS).optional(), limit: remind, next, onFail }),
128
+ z.strictObject({ id, title, kind: z.literal("update"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: z.boolean(), script: nil, sort: nil, limit: remind, next, onFail }),
129
+ z.strictObject({ id, title, kind: z.literal("notify"), zone, instructions: said, planning: nil, approver: nil, message, close: nil, script: nil, sort: nil, limit: remind, next, onFail }),
130
+ z.strictObject({ id, title, kind: z.literal("sort"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: flowSortSchema, limit: remind, next: nil, onFail }),
131
+ z.strictObject({ id, title, kind: z.literal("draft"), zone, instructions: text, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, limit: remind, next, onFail }),
132
+ z.strictObject({ id, title, kind: z.literal("request"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, request: flowRequestSchema, limit: remind, next, onFail }),
133
+ z.strictObject({ id, title, kind: z.literal("email"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, email: flowEmailSchema, limit: remind, next, onFail }),
134
+ z.strictObject({ id, title, kind: z.literal("tool"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, tool: flowToolSchema, limit: remind, next, onFail }),
135
+ z.strictObject({ id, title, kind: z.literal("wait"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, wait: flowWaitSchema, next, onFail }),
136
+ z.strictObject({ id, title, kind: z.literal("teammate"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, teammate: zoneId, routes, reply: z.literal(true).optional(), limit: remind, next, onFail }),
137
+ z.strictObject({ id, title, kind: z.literal("send"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, limit: remind, next, onFail }),
138
+ z.strictObject({ id, title, kind: z.literal("choose"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, options: z.array(flowChoiceSchema).min(CHOICES_MIN).max(CHOICES_MAX), limit: move, next: nil, onFail }),
139
+ z.strictObject({ id, title, kind: z.literal("done"), zone, instructions: said, planning: nil, approver: nil, message: message.nullable(), close: nil, script: nil, sort: nil, next: nil, onFail: nil }),
140
+ ];
141
+ }
142
+ export const FLOW_DEFINITION_VERSION = 1;
143
+ const definitionOf = (instructions) => versioned(FLOW_DEFINITION_VERSION, {
144
+ start: stageRef,
145
+ stages: z.array(z.discriminatedUnion("kind", zoneSchemas(instructions))).min(1).max(FLOW_ZONES_MAX),
146
+ });
147
+ /** A flow's drawing as it is written now: what the canvas saves and steps become. */
148
+ export const flowDefinitionSchema = definitionOf(limited("instructions", "flowInstructions"));
149
+ /** A drawing saved earlier: the same, except instructions keep the length they were saved with (a limit is for writing). */
150
+ export const savedFlowDefinitionSchema = definitionOf(z.string());
151
+ export const flowStageSchema = flowDefinitionSchema.shape.stages.element;
152
+ // ------------------------------------------------------------------ steps (and the flow file's zones)
153
+ /** A short field: bounded by its TEXT_LIMITS entry. Control and hidden characters in what a step or trigger says are
154
+ * refused after parsing (the saved drawing's schema and the trigger reader name them), not restated per field here. */
155
+ const short = (key) => z.string().max(TEXT_LIMITS[key]);
156
+ const ref = short("flowRef");
157
+ const duration = z.union([z.string().max(TEXT_LIMITS.flowDuration), minutes]);
158
+ const clockWritten = z.string().regex(CLOCK_WRITTEN, { error: "must be a time like 22:00" });
159
+ /** A step's route: an answer and the step it goes to. */
160
+ export const flowStepRouteSchema = z.strictObject({ answer: z.string().min(1).max(TEXT_LIMITS.flowAnswer), goesTo: ref });
161
+ /** A sort step's answer: its name, what it means (a few words Jev reads) and the step it goes to. */
162
+ export const flowStepAnswerSchema = z.strictObject({ answer: short("flowAnswer").min(1), means: short("flowSortMeans").optional(), goesTo: ref });
163
+ /** Something else a sort step notes: a score (levels lowest first) or a yes/no. */
164
+ export const flowStepNoteSchema = z.strictObject({
165
+ id: zoneId.optional(), question: short("flowSortQuestion").min(1), kind: z.enum(["score", "yes-no"]).optional(),
166
+ levels: z.array(short("flowSortLevel")).min(SORT_LEVELS_MIN).max(SORT_LEVELS_MAX).optional(),
167
+ });
168
+ /** A choose step's button: its words, and the step it goes to ("end", or none, ignores the card). */
169
+ export const flowStepOptionSchema = z.strictObject({ label: short("flowChoice").min(1), goesTo: ref.optional() });
170
+ /** Every field a step may give, each once; the kinds below say which are theirs. */
171
+ const STEP_FIELDS = {
172
+ instructions: limited("instructions", "flowInstructions"),
173
+ planning: z.enum(FLOW_PLANNING),
174
+ /** approval: a sign-in name, "me", "owner" (the flow's owner), or "anyone" (or null). */
175
+ decider: short("flowDecider").nullable(),
176
+ message: limited("message", "flowMessage"),
177
+ close: z.boolean(),
178
+ script: scriptName,
179
+ runIn: z.enum(FLOW_RUN_IN),
180
+ routes: z.array(flowStepRouteSchema).max(ROUTES_MAX),
181
+ secrets: z.array(secretName).max(SECRETS_MAX),
182
+ question: short("flowSortQuestion"),
183
+ answers: z.array(flowStepAnswerSchema).min(SORT_ANSWERS_MIN).max(SORT_ANSWERS_MAX),
184
+ /** A percentage (80) or a fraction (0.8). */
185
+ sureAt: z.number().min(SURE_AT_MIN).max(SURE_AT_PERCENT_MAX),
186
+ alsoNote: z.array(flowStepNoteSchema).max(SORT_NOTES_MAX),
187
+ method: z.enum(FLOW_METHODS),
188
+ url: short("flowUrl"),
189
+ headers: z.object({}).catchall(short("flowHeader")),
190
+ body: limited("body", "flowBody"),
191
+ to: short("flowEmailTo"),
192
+ subject: short("flowEmailSubject"),
193
+ server: short("flowToolServer"),
194
+ tool: short("flowToolName"),
195
+ /** An object, or the same as JSON text. */
196
+ args: z.union([z.object({}).catchall(z.unknown()), short("flowToolArgs")]),
197
+ waitFor: z.enum(FLOW_WAIT_FOR),
198
+ /** Like "3 days" or "4 hours" (up to 30 days), or minutes. */
199
+ wait: duration,
200
+ from: clockWritten,
201
+ until: clockWritten,
202
+ timeZone: z.string().min(1).max(TEXT_LIMITS.flowTimeZone),
203
+ /** squash, merge or rebase; true is squash. */
204
+ merge: z.union([z.boolean(), z.enum(FLOW_MERGE_METHODS)]),
205
+ /** Like "2 days" ("none" removes it), or minutes. */
206
+ remindAfter: duration,
207
+ thenMoveTo: ref,
208
+ /** A teammate's short name; "nobody" takes one off. */
209
+ teammate: z.string().min(1).max(TEXT_LIMITS.flowTeammate),
210
+ reply: z.boolean(),
211
+ options: z.array(flowStepOptionSchema).min(CHOICES_MIN).max(CHOICES_MAX),
212
+ ifReplied: ref,
213
+ repo: short("flowRepo"),
214
+ next: ref,
215
+ ifFails: ref,
216
+ ifNotSure: ref,
217
+ ifNoReply: ref,
218
+ };
219
+ function optional(keys) {
220
+ return Object.fromEntries(keys.map(key => [key, STEP_FIELDS[key].optional()]));
221
+ }
222
+ /** What each kind of step takes, besides its id, title and kind. */
223
+ const STEP_KINDS = {
224
+ inbox: ["next", "ifFails", "remindAfter", "thenMoveTo"],
225
+ task: ["instructions", "planning", "repo", "next", "ifFails", "remindAfter"],
226
+ report: ["instructions", "next", "ifFails", "remindAfter"],
227
+ approval: ["decider", "teammate", "next", "ifFails", "remindAfter", "thenMoveTo"],
228
+ check: ["script", "runIn", "routes", "secrets", "next", "ifFails", "remindAfter"],
229
+ "pull-request": ["merge", "next", "ifFails", "remindAfter"],
230
+ update: ["message", "close", "next", "ifFails", "remindAfter"],
231
+ notify: ["message", "next", "ifFails", "remindAfter"],
232
+ sort: ["question", "answers", "sureAt", "alsoNote", "ifNotSure", "remindAfter"],
233
+ draft: ["instructions", "next", "ifFails", "remindAfter"],
234
+ request: ["method", "url", "headers", "body", "next", "ifFails", "remindAfter"],
235
+ email: ["to", "subject", "body", "next", "ifFails", "remindAfter"],
236
+ tool: ["server", "tool", "args", "next", "ifFails", "remindAfter"],
237
+ wait: ["waitFor", "wait", "from", "until", "timeZone", "next", "ifNoReply"],
238
+ teammate: ["teammate", "instructions", "routes", "reply", "next", "ifFails", "remindAfter"],
239
+ send: ["next", "ifFails", "remindAfter"],
240
+ choose: ["options", "ifReplied", "remindAfter", "ifNoReply", "thenMoveTo"],
241
+ done: [],
242
+ };
243
+ const stepOf = (kind) => z.strictObject({
244
+ /** Keeps an existing zone by its id, or names a new one so other steps can point at it. */
245
+ id: z.string().min(1).max(TEXT_LIMITS.flowStepId).optional(),
246
+ title: short("flowTitle").min(1),
247
+ kind: z.literal(kind),
248
+ ...optional(STEP_KINDS[kind]),
249
+ });
250
+ const STEPS = {
251
+ inbox: stepOf("inbox"), task: stepOf("task"), report: stepOf("report"), approval: stepOf("approval"), check: stepOf("check"), "pull-request": stepOf("pull-request"),
252
+ update: stepOf("update"), notify: stepOf("notify"), sort: stepOf("sort"), draft: stepOf("draft"), request: stepOf("request"), email: stepOf("email"),
253
+ tool: stepOf("tool"), wait: stepOf("wait"), teammate: stepOf("teammate"), send: stepOf("send"), choose: stepOf("choose"), done: stepOf("done"),
254
+ };
255
+ const stepList = [STEPS.inbox, STEPS.task, STEPS.report, STEPS.approval, STEPS.check, STEPS["pull-request"], STEPS.update, STEPS.notify, STEPS.sort, STEPS.draft, STEPS.request, STEPS.email, STEPS.tool, STEPS.wait, STEPS.teammate, STEPS.send, STEPS.choose, STEPS.done];
256
+ /** A step as the lead, `toolroll flows create/edit`, starters, kits and the gallery describe it, one schema per kind. */
257
+ export const flowStepSchema = z.discriminatedUnion("kind", stepList);
258
+ /** A flow's steps, in order: what `flowFromSteps` reads. */
259
+ export const flowStepsSchema = z.strictObject({ steps: z.array(flowStepSchema).min(1).max(FLOW_ZONES_MAX) });
260
+ /** Where a flow file's zone sits on the canvas. */
261
+ const fileAt = z.strictObject({ x: z.number().optional(), y: z.number().optional(), w: z.number().optional(), h: z.number().optional(), color: z.enum(FLOW_COLORS).optional() }).describe("Where the zone sits on the canvas.");
262
+ const fileZone = (step) => step.extend({ id: zoneId, at: fileAt.optional() });
263
+ /** A flow file's zone: a step with its id and its place on the canvas. */
264
+ export const flowFileZoneSchema = z.discriminatedUnion("kind", [
265
+ fileZone(STEPS.inbox), fileZone(STEPS.task), fileZone(STEPS.report), fileZone(STEPS.approval), fileZone(STEPS.check), fileZone(STEPS["pull-request"]),
266
+ fileZone(STEPS.update), fileZone(STEPS.notify), fileZone(STEPS.sort), fileZone(STEPS.draft), fileZone(STEPS.request), fileZone(STEPS.email),
267
+ fileZone(STEPS.tool), fileZone(STEPS.wait), fileZone(STEPS.teammate), fileZone(STEPS.send), fileZone(STEPS.choose), fileZone(STEPS.done),
268
+ ]);
269
+ /** The keys people and models write for the ones a step means: refusals suggest them (contract.ts). */
270
+ export const FLOW_ALIASES = {
271
+ aliases: {
272
+ onFail: ["ifFails", "ifNotSure", "ifNoReply", "ifReplied"], ifFails: ["ifNotSure", "ifNoReply", "ifReplied"], to: ["goesTo", "thenMoveTo"], goto: ["goesTo"], target: ["goesTo"],
273
+ approver: ["decider"], toOwner: ["decider"], prompt: ["instructions"], description: ["instructions"], text: ["message", "instructions", "body"], name: ["title"],
274
+ sort: ["question"], notes: ["alsoNote"], choices: ["options"], buttons: ["options"], limit: ["remindAfter"], minutes: ["wait"], until: ["thenMoveTo"],
275
+ stages: ["steps", "zones"], zone: ["at"], label: ["title"],
276
+ },
277
+ };
278
+ // ------------------------------------------------------------------ triggers
279
+ const zoneRef = { zone: z.string().max(TEXT_LIMITS.flowRef).optional() };
280
+ const delivery = z.enum(["poll", "webhook"]);
281
+ /** A trigger's settings as they are given: the console, `toolroll flows trigger add`, a flow file, a template, the lead. */
282
+ const TRIGGER_INPUTS = {
283
+ button: z.strictObject({ kind: z.literal("button"), label: short("triggerButton").min(1), questions: z.union([z.array(short("triggerQuestion")).max(6), limited("questions", "triggerDescription")]).optional(), ...zoneRef }),
284
+ schedule: z.strictObject({
285
+ kind: z.literal("schedule"), schedule: short("triggerSchedule").min(1), title: short("triggerTitle").optional(), description: limited("description", "triggerDescription").optional(),
286
+ script: scriptName.optional(), secrets: z.union([z.array(secretName).max(SECRETS_MAX), z.string().max(TEXT_LIMITS.triggerSecrets)]).optional(), ...zoneRef,
287
+ }),
288
+ github: z.strictObject({
289
+ kind: z.literal("github"), repo: short("triggerGithubRepo").optional(), watch: z.enum(["issues", "pulls", "checks"]).optional(), label: short("triggerLabel").optional(),
290
+ branch: short("triggerBranch").optional(), from: z.enum(["team", "anyone"]).optional(), delivery: delivery.optional(), ...zoneRef,
291
+ }),
292
+ linear: z.strictObject({ kind: z.literal("linear"), team: short("triggerTeam").optional(), state: short("triggerState").optional(), label: short("triggerLabel").optional(), delivery: delivery.optional(), ...zoneRef }),
293
+ flow: z.strictObject({ kind: z.literal("flow"), flow: z.int().min(1), when: z.string().max(TEXT_LIMITS.flowRef).optional(), ...zoneRef }),
294
+ webhook: z.strictObject({ kind: z.literal("webhook"), title: short("triggerWebhookTitle").optional(), titleField: short("triggerWebhookField").optional(), bodyField: short("triggerWebhookField").optional(), ...zoneRef }),
295
+ email: z.strictObject({ kind: z.literal("email"), folder: short("triggerFolder").optional(), sender: short("triggerSender").optional(), subject: short("triggerSubject").optional(), ...zoneRef }),
296
+ "plane-review": z.strictObject({ kind: z.literal("plane-review"), at: z.string().regex(/^\d{1,2}:\d{2}$/, { error: "must be a time like 07:30" }).optional(), timeZone: z.string().max(TEXT_LIMITS.flowTimeZone).optional(), ...zoneRef }),
297
+ };
298
+ export const triggerInputSchema = z.discriminatedUnion("kind", [TRIGGER_INPUTS.button, TRIGGER_INPUTS.schedule, TRIGGER_INPUTS.github, TRIGGER_INPUTS.linear, TRIGGER_INPUTS.flow, TRIGGER_INPUTS.webhook, TRIGGER_INPUTS.email, TRIGGER_INPUTS["plane-review"]]);
299
+ /** A flow file's trigger: one that names nothing on the installation it was made on (no chat channel, no other flow). */
300
+ export const flowFileTriggerSchema = z.discriminatedUnion("kind", [TRIGGER_INPUTS.button, TRIGGER_INPUTS.schedule, TRIGGER_INPUTS.github, TRIGGER_INPUTS.linear, TRIGGER_INPUTS.webhook, TRIGGER_INPUTS.email, TRIGGER_INPUTS["plane-review"]]);
301
+ /** The lead's trigger settings: no webhook (its address is made on the console), and no delivery setting; another flow is `follow`. */
302
+ export const leadTriggerSchema = z.discriminatedUnion("kind", [
303
+ TRIGGER_INPUTS.button, TRIGGER_INPUTS.schedule, TRIGGER_INPUTS.github.omit({ delivery: true }), TRIGGER_INPUTS.linear.omit({ delivery: true }),
304
+ TRIGGER_INPUTS.flow.omit({ flow: true }).extend({ follow: z.int().min(1).describe("The other flow's id (get_flows).") }), TRIGGER_INPUTS.email, TRIGGER_INPUTS["plane-review"],
305
+ ]);
306
+ const ZONE_OR_NULL = { zone: zoneId.nullable() };
307
+ /** A trigger as it is saved (`configJson`), one schema per kind. */
308
+ export const triggerConfigSchema = z.discriminatedUnion("kind", [
309
+ z.strictObject({ kind: z.literal("button"), label: z.string().min(1).max(TEXT_LIMITS.triggerButton), questions: z.array(z.string().min(1).max(TEXT_LIMITS.triggerQuestion)).min(1).max(6), ...ZONE_OR_NULL }),
310
+ /** `script`: run this project script on the schedule and make a card of each item it prints, instead of one card titled `title`. */
311
+ z.strictObject({ kind: z.literal("schedule"), schedule: z.string().min(1), title: z.string().min(1).max(TEXT_LIMITS.triggerTitle), description: z.string().max(TEXT_LIMITS.triggerDescription).nullable(), ...ZONE_OR_NULL, script: scriptName.optional(), secrets: z.array(secretName).min(1).max(SECRETS_MAX).optional() }),
312
+ z.strictObject({ kind: z.literal("github"), repo: z.string().min(1).max(TEXT_LIMITS.triggerGithubRepo), watch: z.enum(["issues", "pulls", "checks"]), label: z.string().max(TEXT_LIMITS.triggerLabel).nullable(), branch: z.string().max(TEXT_LIMITS.triggerBranch).nullable(), from: z.enum(["team", "anyone"]), delivery, ...ZONE_OR_NULL }),
313
+ z.strictObject({ kind: z.literal("linear"), team: z.string().max(TEXT_LIMITS.triggerTeam).nullable(), state: z.string().max(TEXT_LIMITS.triggerState).nullable(), label: z.string().max(TEXT_LIMITS.triggerLabel).nullable(), delivery, ...ZONE_OR_NULL }),
314
+ z.strictObject({ kind: z.literal("flow"), flow: z.int().min(1), when: zoneId, ...ZONE_OR_NULL }),
315
+ z.strictObject({ kind: z.literal("webhook"), title: z.string().min(1).max(TEXT_LIMITS.triggerWebhookTitle), titleField: z.string().max(TEXT_LIMITS.triggerWebhookField).nullable(), bodyField: z.string().max(TEXT_LIMITS.triggerWebhookField).nullable(), ...ZONE_OR_NULL }),
316
+ /** `sender`: addresses or domains, comma-separated; `subject`: words the subject must contain. */
317
+ z.strictObject({ kind: z.literal("email"), folder: z.string().min(1).max(TEXT_LIMITS.triggerFolder), sender: z.string().max(TEXT_LIMITS.triggerSender).nullable(), subject: z.string().max(TEXT_LIMITS.triggerSubject).nullable(), ...ZONE_OR_NULL }),
318
+ /** A channel in a chat app; `binding` is the pairing of the person who connected it (whose chat answers there). */
319
+ z.strictObject({ kind: z.literal("chat"), app: z.enum(["slack", "discord", "teams", "telegram"]), installation: z.string().min(1), chat: z.string().min(1), binding: z.int(), ...ZONE_OR_NULL }),
320
+ /** `schedule`: always daily, in the routines' form ("daily:07:30@Europe/London"). */
321
+ z.strictObject({ kind: z.literal("plane-review"), schedule: z.string().min(1), ...ZONE_OR_NULL }),
322
+ ]);
323
+ // ------------------------------------------------------------------ the flow file
324
+ export const FLOW_FILE_FORMAT = "toolroll-flow";
325
+ export const FLOW_FILE_VERSION = 1;
326
+ /** Something the import asks for: `{{param.<id>}}` in a zone or trigger is replaced by its value. */
327
+ export const flowFileParameterSchema = z.strictObject({
328
+ id: z.string().regex(PARAMETER_ID, { error: "must be a short id: lowercase letters, numbers and dashes" }),
329
+ about: visible("about", "flowParameter").min(1).describe("The question the import asks."),
330
+ default: visible("default", "flowParameter").optional(),
331
+ optional: z.boolean().optional().describe("May be left empty (the field is then left out)."),
332
+ });
333
+ /** A project script a zone or schedule runs. Imported held until a person approves it. */
334
+ export const flowFileScriptSchema = z.strictObject({
335
+ name: scriptName, about: limited("about", "flowScriptAbout"), language: z.enum(SCRIPT_LANGUAGES).optional().describe("Shell when left out."), timeoutMinutes: z.int().min(1).max(60).optional().describe("15 when left out."),
336
+ body: limited("body", "flowScriptBody").optional(), file: limited("file", "flowScriptFile").optional().describe("A path inside the project, run instead of a body."),
337
+ });
338
+ /** A flow exported as readable JSON (*.toolroll-flow.json): docs/flow-file.schema.json is this schema. */
339
+ export const flowFileSchema = versioned(FLOW_FILE_VERSION, {
340
+ format: z.literal(FLOW_FILE_FORMAT),
341
+ name: visible("name", "flowName").min(1),
342
+ about: visible("about", "flowFileAbout").optional().describe("One line on what the flow does. Untrusted: shown in the import preview."),
343
+ needs: z.array(visible("need", "flowFileNeed").min(1)).max(40).optional().describe("What the flow needs to run: github, linear, openrouter, email, tool:<server>, secret:<NAME>, teammate:<handle>, script:<name>."),
344
+ parameters: z.array(flowFileParameterSchema).max(20).optional(),
345
+ zones: z.array(flowFileZoneSchema).min(1).max(FLOW_ZONES_MAX).describe("The zones in order; the first is where cards start."),
346
+ triggers: z.array(flowFileTriggerSchema).max(10).optional(),
347
+ scripts: z.array(flowFileScriptSchema).max(20).optional(),
348
+ });
349
+ /** docs/flow-file.schema.json: the flow file's schema as published for people and other tools, generated from
350
+ * flowFileSchema (`npx tsx scripts/flow-file-schema.ts`); flow.test.ts checks the file is exactly this. */
351
+ export function flowFileJsonSchema() {
352
+ return {
353
+ $schema: "https://json-schema.org/draft/2020-12/schema",
354
+ $id: "https://github.com/ap9000/toolroll/docs/flow-file.schema.json",
355
+ title: "Toolroll flow file",
356
+ description: "A flow exported from Toolroll (*.toolroll-flow.json): its zones in the lead's step vocabulary, one shape per kind, its triggers' settings and the scripts it runs. Never secrets, webhook addresses or hashes, tokens, people's names, chat bindings or cards. {{param.<id>}} in a zone or trigger is replaced by the value the import asks for. Unknown keys are refused by name. Toolroll reads files up to 256 KB and checks every zone, trigger and script again on import (src/flow-share.ts). Generated from src/contracts/flow.ts.",
357
+ ...toModelSchema(flowFileSchema),
358
+ };
359
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The handoff artifact (`handoff.json`, M6.10): one schema for the machine's own statement of where a finished run
3
+ * left the world — workspace identity, exact base and head, which answered decisions the brief carried, the agent's
4
+ * conclusion and lists, and a freshness stamp a successor proves against the branch. `storeHandoffArtifact` writes it
5
+ * and every reader reads it through `readHandoffArtifact`.
6
+ *
7
+ * The agent's own terminal handoff (`STANDING-ORDERS-DONE-*`, `parseHandoff` in decision.ts) is a different contract;
8
+ * this file records what the machine made of it.
9
+ */
10
+ import { z } from "zod";
11
+ import { type ContractResult } from "./contract.js";
12
+ export declare const ROUTE_PHASES: readonly ["plan", "build", "repair", "review"];
13
+ export declare const ROUTE_CHOICES: readonly ["recommended", "override", "pinned", "legacy", "fallback"];
14
+ /** The sealed route leg the run ran as (v47); absent on handoffs written before routes existed. */
15
+ export declare const handoffRouteSchema: z.ZodObject<{
16
+ digest: z.ZodString;
17
+ phase: z.ZodEnum<{
18
+ build: "build";
19
+ plan: "plan";
20
+ repair: "repair";
21
+ review: "review";
22
+ }>;
23
+ provider: z.ZodString;
24
+ model: z.ZodNullable<z.ZodString>;
25
+ chosen: z.ZodEnum<{
26
+ fallback: "fallback";
27
+ legacy: "legacy";
28
+ override: "override";
29
+ pinned: "pinned";
30
+ recommended: "recommended";
31
+ }>;
32
+ }, z.core.$strict>;
33
+ export declare const HANDOFF_VERSION = 1;
34
+ export declare const handoffSchema: z.ZodObject<{
35
+ version: z.ZodLiteral<1>;
36
+ taskId: z.ZodString;
37
+ runId: z.ZodInt;
38
+ provider: z.ZodString;
39
+ model: z.ZodOptional<z.ZodString>;
40
+ route: z.ZodOptional<z.ZodObject<{
41
+ digest: z.ZodString;
42
+ phase: z.ZodEnum<{
43
+ build: "build";
44
+ plan: "plan";
45
+ repair: "repair";
46
+ review: "review";
47
+ }>;
48
+ provider: z.ZodString;
49
+ model: z.ZodNullable<z.ZodString>;
50
+ chosen: z.ZodEnum<{
51
+ fallback: "fallback";
52
+ legacy: "legacy";
53
+ override: "override";
54
+ pinned: "pinned";
55
+ recommended: "recommended";
56
+ }>;
57
+ }, z.core.$strict>>;
58
+ sessionId: z.ZodNullable<z.ZodString>;
59
+ branch: z.ZodString;
60
+ worktree: z.ZodString;
61
+ base: z.ZodString;
62
+ head: z.ZodString;
63
+ outcome: z.ZodEnum<{
64
+ built: "built";
65
+ "no-change": "no-change";
66
+ }>;
67
+ committed: z.ZodBoolean;
68
+ decisionsIncorporated: z.ZodArray<z.ZodInt>;
69
+ conclusion: z.ZodString;
70
+ changes: z.ZodOptional<z.ZodArray<z.ZodString>>;
71
+ verification: z.ZodOptional<z.ZodArray<z.ZodString>>;
72
+ followUps: z.ZodOptional<z.ZodArray<z.ZodString>>;
73
+ freshness: z.ZodObject<{
74
+ stampedAt: z.ZodString;
75
+ currentAsOf: z.ZodString;
76
+ }, z.core.$strict>;
77
+ }, z.core.$strict>;
78
+ export type HandoffArtifact = z.infer<typeof handoffSchema>;
79
+ /**
80
+ * A handoff written before handoffs carried `version` (every handoff through 0.9.34, stamped `schema: 1`), as
81
+ * version 1. The shape is the same; readers then read only the fields they knew and ignored any others, so this keeps
82
+ * exactly the known fields. Anything not stamped `schema: 1` is left as it is, for the schema to refuse by path.
83
+ */
84
+ export declare function upgradeSchemaOneHandoff(body: Record<string, unknown>): Record<string, unknown>;
85
+ export declare const HANDOFF_UPGRADES: {
86
+ readonly 0: typeof upgradeSchemaOneHandoff;
87
+ };
88
+ /** Read a handoff artifact: version 1 as itself, a `schema: 1` one upgraded, a newer one refused plainly. */
89
+ export declare function readHandoffArtifact(input: unknown): ContractResult<HandoffArtifact>;
90
+ /** Read a stored handoff's bytes: JSON first, then the schema. */
91
+ export declare function parseHandoffArtifact(raw: string): ContractResult<HandoffArtifact>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The handoff artifact (`handoff.json`, M6.10): one schema for the machine's own statement of where a finished run
3
+ * left the world — workspace identity, exact base and head, which answered decisions the brief carried, the agent's
4
+ * conclusion and lists, and a freshness stamp a successor proves against the branch. `storeHandoffArtifact` writes it
5
+ * and every reader reads it through `readHandoffArtifact`.
6
+ *
7
+ * The agent's own terminal handoff (`STANDING-ORDERS-DONE-*`, `parseHandoff` in decision.ts) is a different contract;
8
+ * this file records what the machine made of it.
9
+ */
10
+ import { z } from "zod";
11
+ import { readVersioned, versioned } from "./contract.js";
12
+ export const ROUTE_PHASES = ["plan", "build", "repair", "review"];
13
+ export const ROUTE_CHOICES = ["recommended", "override", "pinned", "legacy", "fallback"];
14
+ /** The sealed route leg the run ran as (v47); absent on handoffs written before routes existed. */
15
+ export const handoffRouteSchema = z.strictObject({
16
+ digest: z.string(),
17
+ phase: z.enum(ROUTE_PHASES),
18
+ provider: z.string(),
19
+ model: z.string().nullable(),
20
+ chosen: z.enum(ROUTE_CHOICES),
21
+ });
22
+ export const HANDOFF_VERSION = 1;
23
+ export const handoffSchema = versioned(HANDOFF_VERSION, {
24
+ taskId: z.string(),
25
+ runId: z.int(),
26
+ provider: z.string(),
27
+ /** v47: the exact model that ran. Absent on handoffs written before routes existed. */
28
+ model: z.string().optional(),
29
+ route: handoffRouteSchema.optional(),
30
+ sessionId: z.string().nullable(),
31
+ branch: z.string(),
32
+ worktree: z.string(),
33
+ base: z.string(),
34
+ head: z.string(),
35
+ outcome: z.enum(["built", "no-change"]),
36
+ committed: z.boolean(),
37
+ /** Decision ids whose answers were in this run's brief — causality, not time. */
38
+ decisionsIncorporated: z.array(z.int()),
39
+ /** The agent's conclusion — agent-reported, and labeled so by its position here. */
40
+ conclusion: z.string(),
41
+ /** The agent's structured lists; absent on handoffs written before they existed, read as none. */
42
+ changes: z.array(z.string()).optional(),
43
+ verification: z.array(z.string()).optional(),
44
+ followUps: z.array(z.string()).optional(),
45
+ freshness: z.strictObject({
46
+ stampedAt: z.string(),
47
+ /** A successor proves this against the branch before trusting anything above. */
48
+ currentAsOf: z.string(),
49
+ }),
50
+ });
51
+ const HANDOFF_FIELDS = Object.keys(handoffSchema.shape).filter(field => field !== "version");
52
+ /**
53
+ * A handoff written before handoffs carried `version` (every handoff through 0.9.34, stamped `schema: 1`), as
54
+ * version 1. The shape is the same; readers then read only the fields they knew and ignored any others, so this keeps
55
+ * exactly the known fields. Anything not stamped `schema: 1` is left as it is, for the schema to refuse by path.
56
+ */
57
+ export function upgradeSchemaOneHandoff(body) {
58
+ if (body["schema"] !== 1)
59
+ return body;
60
+ const out = { version: HANDOFF_VERSION };
61
+ for (const field of HANDOFF_FIELDS) {
62
+ if (Object.prototype.hasOwnProperty.call(body, field) && body[field] !== undefined)
63
+ out[field] = body[field];
64
+ }
65
+ return out;
66
+ }
67
+ export const HANDOFF_UPGRADES = { 0: upgradeSchemaOneHandoff };
68
+ /** Read a handoff artifact: version 1 as itself, a `schema: 1` one upgraded, a newer one refused plainly. */
69
+ export function readHandoffArtifact(input) {
70
+ return readVersioned(handoffSchema, input, HANDOFF_UPGRADES);
71
+ }
72
+ /** Read a stored handoff's bytes: JSON first, then the schema. */
73
+ export function parseHandoffArtifact(raw) {
74
+ let body;
75
+ try {
76
+ body = JSON.parse(raw);
77
+ }
78
+ catch {
79
+ return { ok: false, issues: [{ path: "payload", kind: "invalid", line: "payload: not JSON" }] };
80
+ }
81
+ return readHandoffArtifact(body);
82
+ }
@@ -0,0 +1,58 @@
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 { type ContractResult } from "./contract.js";
9
+ /** How many paths a plan may say it touches. */
10
+ export declare const PLAN_TOUCHES = 32;
11
+ /** One acceptance criterion: the scope's own shape (scope.ts `AcceptanceCriterion`), `how` advisory and unsigned. */
12
+ export declare const acceptanceCriterionSchema: z.ZodObject<{
13
+ id: z.ZodString;
14
+ statement: z.ZodString;
15
+ how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
16
+ evidence: z.ZodArray<z.ZodEnum<{
17
+ "changed-path": "changed-path";
18
+ check: "check";
19
+ "manual-review": "manual-review";
20
+ screenshot: "screenshot";
21
+ }>>;
22
+ }, z.core.$strict>;
23
+ export declare const PLAN_VERSION = 1;
24
+ export declare const planSchema: z.ZodObject<{
25
+ version: z.ZodLiteral<1>;
26
+ goal: z.ZodString;
27
+ outOfScope: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
+ touches: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
+ acceptance: z.ZodArray<z.ZodObject<{
30
+ id: z.ZodString;
31
+ statement: z.ZodString;
32
+ how: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ evidence: z.ZodArray<z.ZodEnum<{
34
+ "changed-path": "changed-path";
35
+ check: "check";
36
+ "manual-review": "manual-review";
37
+ screenshot: "screenshot";
38
+ }>>;
39
+ }, z.core.$strict>>;
40
+ plan: z.ZodString;
41
+ amendment: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ }, z.core.$strict>;
43
+ export type PlanPayload = z.infer<typeof planSchema>;
44
+ /** What the planner's `--json-schema` is: the plan schema, exactly. */
45
+ export declare const PLAN_MODEL_SCHEMA: Readonly<Record<string, unknown>>;
46
+ /**
47
+ * A plan written before plans carried `version` (every plan through 0.9.33), as version 1. The old parser read only
48
+ * the fields it knew and ignored any others, read a null goal, plan or acceptance as missing and null touches as
49
+ * none; this keeps exactly that, so every plan that parsed then parses now. Values themselves are never rewritten.
50
+ */
51
+ export declare function upgradeUnversionedPlan(body: Record<string, unknown>): Record<string, unknown>;
52
+ export declare const PLAN_UPGRADES: {
53
+ readonly 0: typeof upgradeUnversionedPlan;
54
+ };
55
+ /** The payload as version 1, upgraded when it was saved unversioned; null for anything that is not an object. */
56
+ export declare function planPayloadBody(input: unknown): Record<string, unknown> | null;
57
+ /** Read a plan payload: version 1 as itself, an unversioned one upgraded, a newer one refused plainly. */
58
+ export declare function readPlanPayload(input: unknown): ContractResult<PlanPayload>;