toolroll 0.9.34 → 0.9.37

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 (195) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -0
  3. package/dist/assignment-adapters.d.ts +12 -48
  4. package/dist/assignment-adapters.js +33 -43
  5. package/dist/assignment.d.ts +1 -0
  6. package/dist/assignment.js +2 -2
  7. package/dist/browser-workspace.d.ts +3 -3
  8. package/dist/build-review.d.ts +1 -1
  9. package/dist/build-review.js +2 -1
  10. package/dist/builder.js +4 -7
  11. package/dist/chat-acceptance.d.ts +2 -2
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-decide.d.ts +14 -1
  16. package/dist/chat-decide.js +12 -6
  17. package/dist/chat-delivery-state.d.ts +12 -70
  18. package/dist/chat-delivery-state.js +17 -135
  19. package/dist/chat-delivery.js +58 -30
  20. package/dist/chat-flow.js +8 -6
  21. package/dist/chat-inbox.js +1 -1
  22. package/dist/coding-context.d.ts +2 -39
  23. package/dist/coding-context.js +2 -1
  24. package/dist/coding-handoff.d.ts +6 -25
  25. package/dist/coding-handoff.js +11 -5
  26. package/dist/coding-types.d.ts +2 -26
  27. package/dist/coding-workspace.d.ts +2 -0
  28. package/dist/coding-workspace.js +50 -14
  29. package/dist/contracts/acceptance-terms.d.ts +14 -0
  30. package/dist/contracts/acceptance-terms.js +16 -0
  31. package/dist/contracts/chat-actions.d.ts +328 -0
  32. package/dist/contracts/chat-actions.js +133 -0
  33. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  34. package/dist/contracts/chat-callback-rows.js +46 -0
  35. package/dist/contracts/chat-content.d.ts +276 -0
  36. package/dist/contracts/chat-content.js +127 -0
  37. package/dist/contracts/chat-tables.d.ts +6 -0
  38. package/dist/contracts/chat-tables.js +136 -0
  39. package/dist/contracts/coding-context.d.ts +97 -0
  40. package/dist/contracts/coding-context.js +60 -0
  41. package/dist/contracts/coding-handoff.d.ts +44 -0
  42. package/dist/contracts/coding-handoff.js +47 -0
  43. package/dist/contracts/coding-workspace.d.ts +165 -0
  44. package/dist/contracts/coding-workspace.js +71 -0
  45. package/dist/contracts/contract.d.ts +40 -7
  46. package/dist/contracts/contract.js +155 -19
  47. package/dist/contracts/decision.d.ts +45 -0
  48. package/dist/contracts/decision.js +45 -0
  49. package/dist/contracts/discord-callback.d.ts +45 -0
  50. package/dist/contracts/discord-callback.js +34 -0
  51. package/dist/contracts/flow-card.d.ts +50 -0
  52. package/dist/contracts/flow-card.js +46 -0
  53. package/dist/contracts/flow-propose.d.ts +325 -0
  54. package/dist/contracts/flow-propose.js +41 -0
  55. package/dist/contracts/flow-send.d.ts +119 -0
  56. package/dist/contracts/flow-send.js +112 -0
  57. package/dist/contracts/flow.d.ts +19405 -0
  58. package/dist/contracts/flow.js +364 -0
  59. package/dist/contracts/gateway-tools.d.ts +374 -0
  60. package/dist/contracts/gateway-tools.js +94 -0
  61. package/dist/contracts/handoff.d.ts +91 -0
  62. package/dist/contracts/handoff.js +82 -0
  63. package/dist/contracts/lead-context.d.ts +165 -0
  64. package/dist/contracts/lead-context.js +73 -0
  65. package/dist/contracts/lead-tools.d.ts +1905 -0
  66. package/dist/contracts/lead-tools.js +265 -0
  67. package/dist/contracts/memory-pass.d.ts +158 -0
  68. package/dist/contracts/memory-pass.js +102 -0
  69. package/dist/contracts/project-knowledge.d.ts +126 -0
  70. package/dist/contracts/project-knowledge.js +93 -0
  71. package/dist/contracts/project-memory.d.ts +117 -0
  72. package/dist/contracts/project-memory.js +51 -0
  73. package/dist/contracts/project-skills.d.ts +121 -0
  74. package/dist/contracts/project-skills.js +76 -0
  75. package/dist/contracts/proof.d.ts +136 -0
  76. package/dist/contracts/proof.js +110 -0
  77. package/dist/contracts/review-findings.d.ts +46 -0
  78. package/dist/contracts/review-findings.js +48 -0
  79. package/dist/contracts/route.d.ts +484 -0
  80. package/dist/contracts/route.js +128 -0
  81. package/dist/contracts/scope.d.ts +105 -0
  82. package/dist/contracts/scope.js +114 -0
  83. package/dist/contracts/scout-report.d.ts +234 -0
  84. package/dist/contracts/scout-report.js +141 -0
  85. package/dist/contracts/slack-callback.d.ts +43 -0
  86. package/dist/contracts/slack-callback.js +35 -0
  87. package/dist/contracts/sort-answer.d.ts +83 -0
  88. package/dist/contracts/sort-answer.js +77 -0
  89. package/dist/contracts/stage-output.d.ts +127 -0
  90. package/dist/contracts/stage-output.js +166 -0
  91. package/dist/contracts/task-sizing.d.ts +24 -0
  92. package/dist/contracts/task-sizing.js +28 -0
  93. package/dist/contracts/teammate-turn.d.ts +43 -0
  94. package/dist/contracts/teammate-turn.js +51 -0
  95. package/dist/contracts/teams-callback.d.ts +14 -0
  96. package/dist/contracts/teams-callback.js +14 -0
  97. package/dist/contracts/telegram-callback.d.ts +120 -0
  98. package/dist/contracts/telegram-callback.js +97 -0
  99. package/dist/contracts/verification-receipt.d.ts +148 -0
  100. package/dist/contracts/verification-receipt.js +87 -0
  101. package/dist/decision.d.ts +11 -20
  102. package/dist/decision.js +86 -167
  103. package/dist/demo.js +28 -11
  104. package/dist/discord-chat.js +31 -20
  105. package/dist/evidence.d.ts +3 -40
  106. package/dist/evidence.js +5 -1
  107. package/dist/flow-engine.js +14 -17
  108. package/dist/flow-gallery.d.ts +3 -2
  109. package/dist/flow-items.d.ts +4 -13
  110. package/dist/flow-items.js +1 -15
  111. package/dist/flow-send.d.ts +6 -37
  112. package/dist/flow-send.js +18 -14
  113. package/dist/flow-share.d.ts +10 -32
  114. package/dist/flow-share.js +72 -84
  115. package/dist/flow-sort.d.ts +7 -26
  116. package/dist/flow-sort.js +25 -20
  117. package/dist/flow-starters.d.ts +2 -1
  118. package/dist/flow-steps.js +3 -2
  119. package/dist/flow-triggers.d.ts +15 -72
  120. package/dist/flow-triggers.js +94 -83
  121. package/dist/flows.d.ts +37 -220
  122. package/dist/flows.js +356 -387
  123. package/dist/invoke.js +4 -4
  124. package/dist/lead-context.d.ts +4 -2
  125. package/dist/lead-context.js +24 -17
  126. package/dist/mate-tools.d.ts +4 -0
  127. package/dist/mate-tools.js +111 -308
  128. package/dist/mcp-connect.d.ts +27 -4
  129. package/dist/mcp-connect.js +92 -17
  130. package/dist/mcp.js +84 -216
  131. package/dist/memory-pass.d.ts +17 -42
  132. package/dist/memory-pass.js +54 -26
  133. package/dist/operate.js +31 -2
  134. package/dist/phase-routing.d.ts +19 -72
  135. package/dist/phase-routing.js +34 -164
  136. package/dist/plan.d.ts +5 -5
  137. package/dist/plan.js +5 -25
  138. package/dist/process-recovery-eligibility.d.ts +2 -2
  139. package/dist/process-recovery-eligibility.js +13 -8
  140. package/dist/project-knowledge.d.ts +16 -35
  141. package/dist/project-knowledge.js +42 -21
  142. package/dist/project-memory.d.ts +2 -28
  143. package/dist/project-memory.js +28 -13
  144. package/dist/project-skills.d.ts +16 -38
  145. package/dist/project-skills.js +41 -32
  146. package/dist/project-tools.d.ts +1 -0
  147. package/dist/project-tools.js +4 -4
  148. package/dist/proof.d.ts +15 -91
  149. package/dist/proof.js +90 -249
  150. package/dist/provider.d.ts +2 -0
  151. package/dist/provider.js +4 -16
  152. package/dist/result-evidence-readers.d.ts +42 -0
  153. package/dist/result-evidence-readers.js +91 -0
  154. package/dist/result-shots.d.ts +1 -1
  155. package/dist/result-shots.js +2 -1
  156. package/dist/review-switch.d.ts +3 -8
  157. package/dist/reviewer.d.ts +8 -12
  158. package/dist/reviewer.js +11 -35
  159. package/dist/scope.d.ts +21 -43
  160. package/dist/scope.js +42 -128
  161. package/dist/scout-report.d.ts +9 -194
  162. package/dist/scout-report.js +113 -264
  163. package/dist/scout.js +15 -11
  164. package/dist/serve.js +17 -101
  165. package/dist/slack-chat.js +26 -20
  166. package/dist/slack-state.js +2 -1
  167. package/dist/store.d.ts +9 -32
  168. package/dist/store.js +18 -15
  169. package/dist/structured-output.js +2 -1
  170. package/dist/subscription-chat.d.ts +4 -0
  171. package/dist/subscription-chat.js +4 -1
  172. package/dist/surface.js +8 -1
  173. package/dist/task-review-brief.d.ts +69 -0
  174. package/dist/task-review-brief.js +179 -0
  175. package/dist/task-sizing.d.ts +3 -27
  176. package/dist/task-sizing.js +11 -26
  177. package/dist/task-text.d.ts +0 -5
  178. package/dist/task-text.js +1 -3
  179. package/dist/teammate-question.js +7 -5
  180. package/dist/teammate-work.js +9 -5
  181. package/dist/teammates.d.ts +13 -64
  182. package/dist/teammates.js +32 -33
  183. package/dist/teams-chat.js +20 -11
  184. package/dist/telegram-flow.js +4 -3
  185. package/dist/telegram-mate.d.ts +4 -14
  186. package/dist/telegram-mate.js +8 -7
  187. package/dist/telegram-team.d.ts +2 -19
  188. package/dist/telegram.d.ts +1 -1
  189. package/dist/telegram.js +52 -42
  190. package/dist/text-limits.d.ts +212 -1
  191. package/dist/text-limits.js +214 -1
  192. package/dist/tools-ui.d.ts +2 -1
  193. package/dist/tools-ui.js +7 -4
  194. package/dist/verification-evidence.js +24 -12
  195. package/package.json +1 -1
@@ -0,0 +1,364 @@
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
+ /** How a sort answer is named to Jev: its words as a short key (the request's criteria, and the choice Jev returns). */
76
+ export const sortKeyOf = (answer) => answer.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "answer";
77
+ /** The key of a sort zone's question in its Jev request (a choice over its answers), and of each thing it also notes. */
78
+ export const SORT_ROUTE_KEY = "route";
79
+ export const sortNoteKeyOf = (noteId) => `note_${noteId.replace(/-/g, "_")}`;
80
+ /** sort: the question, its answers, how sure Jev must be to act alone (0.5–0.99), and what else it notes. */
81
+ export const flowSortSchema = z.strictObject({
82
+ question: words("question", "flowSortQuestion").min(1),
83
+ answers: z.array(flowSortAnswerSchema).min(SORT_ANSWERS_MIN).max(SORT_ANSWERS_MAX),
84
+ sureAt: z.number().min(SURE_AT_MIN).max(SURE_AT_MAX),
85
+ notes: z.array(flowSortNoteSchema).max(SORT_NOTES_MAX),
86
+ });
87
+ /** request: what is called. The address's scheme and host are fixed; fill-ins go in its path and query (encoded), headers and body. */
88
+ export const flowRequestSchema = z.strictObject({
89
+ method: z.enum(FLOW_METHODS), url: words("url", "flowUrl").min(1),
90
+ headers: z.object({}).catchall(words("header", "flowHeader").min(1)), body: words("body", "flowBody").min(1).nullable(),
91
+ });
92
+ /** email: who it goes to, the subject and the text — all with fill-ins. */
93
+ export const flowEmailSchema = z.strictObject({ to: words("to", "flowEmailTo").min(1), subject: words("subject", "flowEmailSubject").min(1), body: words("body", "flowBody").min(1) });
94
+ /** tool: which of the project's tools (MCP servers), which of its functions, and the arguments as JSON with fill-ins. */
95
+ export const flowToolSchema = z.strictObject({ server: words("server", "flowToolServer").min(1), name: words("name", "flowToolName").min(1), args: words("args", "flowToolArgs").min(1) });
96
+ /** wait: for a reply to the card's email (next: replied, onFail: no reply in time), for a set time (then next), or until
97
+ * the clock is between `from` and `to` (like 22:00–06:00), in the computer's time zone unless one is named. */
98
+ export const flowWaitSchema = z.strictObject({
99
+ for: z.enum(FLOW_WAIT_FOR),
100
+ /** reply, time: how long (a minute to 30 days); hours: 0. */
101
+ minutes: z.int().min(0).max(LONGEST_WAIT_MINUTES),
102
+ from: z.string().regex(CLOCK, { error: "must be a time like 22:00" }).optional(),
103
+ to: z.string().regex(CLOCK, { error: "must be a time like 06:00" }).optional(),
104
+ timeZone: z.string().min(1).max(TEXT_LIMITS.flowTimeZone).optional(),
105
+ });
106
+ /** A choose zone's option: its button's words, and the zone it leads to (FLOW_END closes the card as Ignored). */
107
+ export const flowChoiceSchema = z.strictObject({ label: words("label", "flowChoice").min(1), to: z.union([stageRef, z.literal(FLOW_END)]) });
108
+ /** A script's or teammate's answer and the zone it leads to. */
109
+ export const flowRouteSchema = z.strictObject({ answer: z.string().min(1).max(TEXT_LIMITS.flowAnswer).regex(/^[^\n]*$/, { error: "must be one line" }), to: stageRef });
110
+ /** A time limit on a zone: after this long, the person it waits on is reminded; a Holding, "Person decides" or "Person
111
+ * chooses" zone can also move the card on (`to`). */
112
+ const limitRemind = z.strictObject({ minutes, to: z.null() });
113
+ const limitMove = z.strictObject({ minutes, to: stageRef.nullable() });
114
+ /**
115
+ * The 18 zones, given how long instructions may be (written now: TEXT_LIMITS.flowInstructions; saved earlier: any
116
+ * length). Every zone carries the same first fields (null where a kind has no use for them) and its own after them, in
117
+ * the order the store has always saved them: a zone's digest is its JSON, so the order is part of the contract.
118
+ */
119
+ function zoneSchemas(instructions) {
120
+ const text = instructions.min(1).regex(PLAIN, { error: "can't contain control characters" });
121
+ const said = text.nullable(), nil = z.null();
122
+ const id = zoneId, title = words("title", "flowTitle").min(1), zone = flowZoneSchema, message = words("message", "flowMessage").min(1);
123
+ const next = stageRef.nullable(), onFail = stageRef.nullable();
124
+ const routes = z.array(flowRouteSchema).min(1).max(ROUTES_MAX).optional();
125
+ const remind = limitRemind.optional(), move = limitMove.optional();
126
+ return [
127
+ 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 }),
128
+ 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 }),
129
+ 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 }),
130
+ 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 }),
131
+ 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 }),
132
+ 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 }),
133
+ 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 }),
134
+ 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 }),
135
+ 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 }),
136
+ 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 }),
137
+ 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 }),
138
+ 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 }),
139
+ 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 }),
140
+ 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 }),
141
+ 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 }),
142
+ 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 }),
143
+ 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 }),
144
+ 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 }),
145
+ ];
146
+ }
147
+ export const FLOW_DEFINITION_VERSION = 1;
148
+ const definitionOf = (instructions) => versioned(FLOW_DEFINITION_VERSION, {
149
+ start: stageRef,
150
+ stages: z.array(z.discriminatedUnion("kind", zoneSchemas(instructions))).min(1).max(FLOW_ZONES_MAX),
151
+ });
152
+ /** A flow's drawing as it is written now: what the canvas saves and steps become. */
153
+ export const flowDefinitionSchema = definitionOf(limited("instructions", "flowInstructions"));
154
+ /** A drawing saved earlier: the same, except instructions keep the length they were saved with (a limit is for writing). */
155
+ export const savedFlowDefinitionSchema = definitionOf(z.string());
156
+ export const flowStageSchema = flowDefinitionSchema.shape.stages.element;
157
+ // ------------------------------------------------------------------ steps (and the flow file's zones)
158
+ /** A short field: bounded by its TEXT_LIMITS entry. Control and hidden characters in what a step or trigger says are
159
+ * refused after parsing (the saved drawing's schema and the trigger reader name them), not restated per field here. */
160
+ const short = (key) => z.string().max(TEXT_LIMITS[key]);
161
+ const ref = short("flowRef");
162
+ const duration = z.union([z.string().max(TEXT_LIMITS.flowDuration), minutes]);
163
+ const clockWritten = z.string().regex(CLOCK_WRITTEN, { error: "must be a time like 22:00" });
164
+ /** A step's route: an answer and the step it goes to. */
165
+ export const flowStepRouteSchema = z.strictObject({ answer: z.string().min(1).max(TEXT_LIMITS.flowAnswer), goesTo: ref });
166
+ /** A sort step's answer: its name, what it means (a few words Jev reads) and the step it goes to. */
167
+ export const flowStepAnswerSchema = z.strictObject({ answer: short("flowAnswer").min(1), means: short("flowSortMeans").optional(), goesTo: ref });
168
+ /** Something else a sort step notes: a score (levels lowest first) or a yes/no. */
169
+ export const flowStepNoteSchema = z.strictObject({
170
+ id: zoneId.optional(), question: short("flowSortQuestion").min(1), kind: z.enum(["score", "yes-no"]).optional(),
171
+ levels: z.array(short("flowSortLevel")).min(SORT_LEVELS_MIN).max(SORT_LEVELS_MAX).optional(),
172
+ });
173
+ /** A choose step's button: its words, and the step it goes to ("end", or none, ignores the card). */
174
+ export const flowStepOptionSchema = z.strictObject({ label: short("flowChoice").min(1), goesTo: ref.optional() });
175
+ /** Every field a step may give, each once; the kinds below say which are theirs. */
176
+ const STEP_FIELDS = {
177
+ instructions: limited("instructions", "flowInstructions"),
178
+ planning: z.enum(FLOW_PLANNING),
179
+ /** approval: a sign-in name, "me", "owner" (the flow's owner), or "anyone" (or null). */
180
+ decider: short("flowDecider").nullable(),
181
+ message: limited("message", "flowMessage"),
182
+ close: z.boolean(),
183
+ script: scriptName,
184
+ runIn: z.enum(FLOW_RUN_IN),
185
+ routes: z.array(flowStepRouteSchema).max(ROUTES_MAX),
186
+ secrets: z.array(secretName).max(SECRETS_MAX),
187
+ question: short("flowSortQuestion"),
188
+ answers: z.array(flowStepAnswerSchema).min(SORT_ANSWERS_MIN).max(SORT_ANSWERS_MAX),
189
+ /** A percentage (80) or a fraction (0.8). */
190
+ sureAt: z.number().min(SURE_AT_MIN).max(SURE_AT_PERCENT_MAX),
191
+ alsoNote: z.array(flowStepNoteSchema).max(SORT_NOTES_MAX),
192
+ method: z.enum(FLOW_METHODS),
193
+ url: short("flowUrl"),
194
+ headers: z.object({}).catchall(short("flowHeader")),
195
+ body: limited("body", "flowBody"),
196
+ to: short("flowEmailTo"),
197
+ subject: short("flowEmailSubject"),
198
+ server: short("flowToolServer"),
199
+ tool: short("flowToolName"),
200
+ /** An object, or the same as JSON text. */
201
+ args: z.union([z.object({}).catchall(z.unknown()), short("flowToolArgs")]),
202
+ waitFor: z.enum(FLOW_WAIT_FOR),
203
+ /** Like "3 days" or "4 hours" (up to 30 days), or minutes. */
204
+ wait: duration,
205
+ from: clockWritten,
206
+ until: clockWritten,
207
+ timeZone: z.string().min(1).max(TEXT_LIMITS.flowTimeZone),
208
+ /** squash, merge or rebase; true is squash. */
209
+ merge: z.union([z.boolean(), z.enum(FLOW_MERGE_METHODS)]),
210
+ /** Like "2 days" ("none" removes it), or minutes. */
211
+ remindAfter: duration,
212
+ thenMoveTo: ref,
213
+ /** A teammate's short name; "nobody" takes one off. */
214
+ teammate: z.string().min(1).max(TEXT_LIMITS.flowTeammate),
215
+ reply: z.boolean(),
216
+ options: z.array(flowStepOptionSchema).min(CHOICES_MIN).max(CHOICES_MAX),
217
+ ifReplied: ref,
218
+ repo: short("flowRepo"),
219
+ next: ref,
220
+ ifFails: ref,
221
+ ifNotSure: ref,
222
+ ifNoReply: ref,
223
+ };
224
+ function optional(keys) {
225
+ return Object.fromEntries(keys.map(key => [key, STEP_FIELDS[key].optional()]));
226
+ }
227
+ /** What each kind of step takes, besides its id, title and kind. */
228
+ const STEP_KINDS = {
229
+ inbox: ["next", "ifFails", "remindAfter", "thenMoveTo"],
230
+ task: ["instructions", "planning", "repo", "next", "ifFails", "remindAfter"],
231
+ report: ["instructions", "next", "ifFails", "remindAfter"],
232
+ approval: ["decider", "teammate", "next", "ifFails", "remindAfter", "thenMoveTo"],
233
+ check: ["script", "runIn", "routes", "secrets", "next", "ifFails", "remindAfter"],
234
+ "pull-request": ["merge", "next", "ifFails", "remindAfter"],
235
+ update: ["message", "close", "next", "ifFails", "remindAfter"],
236
+ notify: ["message", "next", "ifFails", "remindAfter"],
237
+ sort: ["question", "answers", "sureAt", "alsoNote", "ifNotSure", "remindAfter"],
238
+ draft: ["instructions", "next", "ifFails", "remindAfter"],
239
+ request: ["method", "url", "headers", "body", "next", "ifFails", "remindAfter"],
240
+ email: ["to", "subject", "body", "next", "ifFails", "remindAfter"],
241
+ tool: ["server", "tool", "args", "next", "ifFails", "remindAfter"],
242
+ wait: ["waitFor", "wait", "from", "until", "timeZone", "next", "ifNoReply"],
243
+ teammate: ["teammate", "instructions", "routes", "reply", "next", "ifFails", "remindAfter"],
244
+ send: ["next", "ifFails", "remindAfter"],
245
+ choose: ["options", "ifReplied", "remindAfter", "ifNoReply", "thenMoveTo"],
246
+ done: [],
247
+ };
248
+ const stepOf = (kind) => z.strictObject({
249
+ /** Keeps an existing zone by its id, or names a new one so other steps can point at it. */
250
+ id: z.string().min(1).max(TEXT_LIMITS.flowStepId).optional(),
251
+ title: short("flowTitle").min(1),
252
+ kind: z.literal(kind),
253
+ ...optional(STEP_KINDS[kind]),
254
+ });
255
+ const STEPS = {
256
+ inbox: stepOf("inbox"), task: stepOf("task"), report: stepOf("report"), approval: stepOf("approval"), check: stepOf("check"), "pull-request": stepOf("pull-request"),
257
+ update: stepOf("update"), notify: stepOf("notify"), sort: stepOf("sort"), draft: stepOf("draft"), request: stepOf("request"), email: stepOf("email"),
258
+ tool: stepOf("tool"), wait: stepOf("wait"), teammate: stepOf("teammate"), send: stepOf("send"), choose: stepOf("choose"), done: stepOf("done"),
259
+ };
260
+ 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];
261
+ /** A step as the lead, `toolroll flows create/edit`, starters, kits and the gallery describe it, one schema per kind. */
262
+ export const flowStepSchema = z.discriminatedUnion("kind", stepList);
263
+ /** A flow's steps, in order: what `flowFromSteps` reads. */
264
+ export const flowStepsSchema = z.strictObject({ steps: z.array(flowStepSchema).min(1).max(FLOW_ZONES_MAX) });
265
+ /** Where a flow file's zone sits on the canvas. */
266
+ 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.");
267
+ const fileZone = (step) => step.extend({ id: zoneId, at: fileAt.optional() });
268
+ /** A flow file's zone: a step with its id and its place on the canvas. */
269
+ export const flowFileZoneSchema = z.discriminatedUnion("kind", [
270
+ fileZone(STEPS.inbox), fileZone(STEPS.task), fileZone(STEPS.report), fileZone(STEPS.approval), fileZone(STEPS.check), fileZone(STEPS["pull-request"]),
271
+ fileZone(STEPS.update), fileZone(STEPS.notify), fileZone(STEPS.sort), fileZone(STEPS.draft), fileZone(STEPS.request), fileZone(STEPS.email),
272
+ fileZone(STEPS.tool), fileZone(STEPS.wait), fileZone(STEPS.teammate), fileZone(STEPS.send), fileZone(STEPS.choose), fileZone(STEPS.done),
273
+ ]);
274
+ /** The keys people and models write for the ones a step means: refusals suggest them (contract.ts). */
275
+ export const FLOW_ALIASES = {
276
+ aliases: {
277
+ onFail: ["ifFails", "ifNotSure", "ifNoReply", "ifReplied"], ifFails: ["ifNotSure", "ifNoReply", "ifReplied"], to: ["goesTo", "thenMoveTo"], goto: ["goesTo"], target: ["goesTo"],
278
+ approver: ["decider"], toOwner: ["decider"], prompt: ["instructions"], description: ["instructions"], text: ["message", "instructions", "body"], name: ["title"],
279
+ sort: ["question"], notes: ["alsoNote"], choices: ["options"], buttons: ["options"], limit: ["remindAfter"], minutes: ["wait"], until: ["thenMoveTo"],
280
+ stages: ["steps", "zones"], zone: ["at"], label: ["title"],
281
+ },
282
+ };
283
+ // ------------------------------------------------------------------ triggers
284
+ const zoneRef = { zone: z.string().max(TEXT_LIMITS.flowRef).optional() };
285
+ const delivery = z.enum(["poll", "webhook"]);
286
+ /** A trigger's settings as they are given: the console, `toolroll flows trigger add`, a flow file, a template, the lead. */
287
+ const TRIGGER_INPUTS = {
288
+ 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 }),
289
+ schedule: z.strictObject({
290
+ kind: z.literal("schedule"), schedule: short("triggerSchedule").min(1), title: short("triggerTitle").optional(), description: limited("description", "triggerDescription").optional(),
291
+ script: scriptName.optional(), secrets: z.union([z.array(secretName).max(SECRETS_MAX), z.string().max(TEXT_LIMITS.triggerSecrets)]).optional(), ...zoneRef,
292
+ }),
293
+ github: z.strictObject({
294
+ kind: z.literal("github"), repo: short("triggerGithubRepo").optional(), watch: z.enum(["issues", "pulls", "checks"]).optional(), label: short("triggerLabel").optional(),
295
+ branch: short("triggerBranch").optional(), from: z.enum(["team", "anyone"]).optional(), delivery: delivery.optional(), ...zoneRef,
296
+ }),
297
+ linear: z.strictObject({ kind: z.literal("linear"), team: short("triggerTeam").optional(), state: short("triggerState").optional(), label: short("triggerLabel").optional(), delivery: delivery.optional(), ...zoneRef }),
298
+ flow: z.strictObject({ kind: z.literal("flow"), flow: z.int().min(1), when: z.string().max(TEXT_LIMITS.flowRef).optional(), ...zoneRef }),
299
+ webhook: z.strictObject({ kind: z.literal("webhook"), title: short("triggerWebhookTitle").optional(), titleField: short("triggerWebhookField").optional(), bodyField: short("triggerWebhookField").optional(), ...zoneRef }),
300
+ email: z.strictObject({ kind: z.literal("email"), folder: short("triggerFolder").optional(), sender: short("triggerSender").optional(), subject: short("triggerSubject").optional(), ...zoneRef }),
301
+ "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 }),
302
+ };
303
+ 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"]]);
304
+ /** A flow file's trigger: one that names nothing on the installation it was made on (no chat channel, no other flow). */
305
+ 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"]]);
306
+ /** The lead's trigger settings: no webhook (its address is made on the console), and no delivery setting; another flow is `follow`. */
307
+ export const leadTriggerSchema = z.discriminatedUnion("kind", [
308
+ TRIGGER_INPUTS.button, TRIGGER_INPUTS.schedule, TRIGGER_INPUTS.github.omit({ delivery: true }), TRIGGER_INPUTS.linear.omit({ delivery: true }),
309
+ 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"],
310
+ ]);
311
+ const ZONE_OR_NULL = { zone: zoneId.nullable() };
312
+ /** A trigger as it is saved (`configJson`), one schema per kind. */
313
+ export const triggerConfigSchema = z.discriminatedUnion("kind", [
314
+ 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 }),
315
+ /** `script`: run this project script on the schedule and make a card of each item it prints, instead of one card titled `title`. */
316
+ 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() }),
317
+ 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 }),
318
+ 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 }),
319
+ z.strictObject({ kind: z.literal("flow"), flow: z.int().min(1), when: zoneId, ...ZONE_OR_NULL }),
320
+ 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 }),
321
+ /** `sender`: addresses or domains, comma-separated; `subject`: words the subject must contain. */
322
+ 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 }),
323
+ /** A channel in a chat app; `binding` is the pairing of the person who connected it (whose chat answers there). */
324
+ 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 }),
325
+ /** `schedule`: always daily, in the routines' form ("daily:07:30@Europe/London"). */
326
+ z.strictObject({ kind: z.literal("plane-review"), schedule: z.string().min(1), ...ZONE_OR_NULL }),
327
+ ]);
328
+ // ------------------------------------------------------------------ the flow file
329
+ export const FLOW_FILE_FORMAT = "toolroll-flow";
330
+ export const FLOW_FILE_VERSION = 1;
331
+ /** Something the import asks for: `{{param.<id>}}` in a zone or trigger is replaced by its value. */
332
+ export const flowFileParameterSchema = z.strictObject({
333
+ id: z.string().regex(PARAMETER_ID, { error: "must be a short id: lowercase letters, numbers and dashes" }),
334
+ about: visible("about", "flowParameter").min(1).describe("The question the import asks."),
335
+ default: visible("default", "flowParameter").optional(),
336
+ optional: z.boolean().optional().describe("May be left empty (the field is then left out)."),
337
+ });
338
+ /** A project script a zone or schedule runs. Imported held until a person approves it. */
339
+ export const flowFileScriptSchema = z.strictObject({
340
+ 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."),
341
+ body: limited("body", "flowScriptBody").optional(), file: limited("file", "flowScriptFile").optional().describe("A path inside the project, run instead of a body."),
342
+ });
343
+ /** A flow exported as readable JSON (*.toolroll-flow.json): docs/flow-file.schema.json is this schema. */
344
+ export const flowFileSchema = versioned(FLOW_FILE_VERSION, {
345
+ format: z.literal(FLOW_FILE_FORMAT),
346
+ name: visible("name", "flowName").min(1),
347
+ about: visible("about", "flowFileAbout").optional().describe("One line on what the flow does. Untrusted: shown in the import preview."),
348
+ 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>."),
349
+ parameters: z.array(flowFileParameterSchema).max(20).optional(),
350
+ zones: z.array(flowFileZoneSchema).min(1).max(FLOW_ZONES_MAX).describe("The zones in order; the first is where cards start."),
351
+ triggers: z.array(flowFileTriggerSchema).max(10).optional(),
352
+ scripts: z.array(flowFileScriptSchema).max(20).optional(),
353
+ });
354
+ /** docs/flow-file.schema.json: the flow file's schema as published for people and other tools, generated from
355
+ * flowFileSchema (`npx tsx scripts/flow-file-schema.ts`); flow.test.ts checks the file is exactly this. */
356
+ export function flowFileJsonSchema() {
357
+ return {
358
+ $schema: "https://json-schema.org/draft/2020-12/schema",
359
+ $id: "https://github.com/ap9000/toolroll/docs/flow-file.schema.json",
360
+ title: "Toolroll flow file",
361
+ 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.",
362
+ ...toModelSchema(flowFileSchema),
363
+ };
364
+ }