toolroll 0.9.34 → 0.9.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +7 -0
  3. package/dist/assignment.d.ts +1 -0
  4. package/dist/assignment.js +2 -2
  5. package/dist/browser-workspace.d.ts +3 -3
  6. package/dist/builder.js +4 -7
  7. package/dist/chat-acceptance.d.ts +1 -1
  8. package/dist/contracts/contract.d.ts +33 -5
  9. package/dist/contracts/contract.js +143 -16
  10. package/dist/contracts/flow-propose.d.ts +325 -0
  11. package/dist/contracts/flow-propose.js +41 -0
  12. package/dist/contracts/flow.d.ts +19400 -0
  13. package/dist/contracts/flow.js +359 -0
  14. package/dist/contracts/handoff.d.ts +91 -0
  15. package/dist/contracts/handoff.js +82 -0
  16. package/dist/contracts/proof.d.ts +136 -0
  17. package/dist/contracts/proof.js +110 -0
  18. package/dist/contracts/verification-receipt.d.ts +148 -0
  19. package/dist/contracts/verification-receipt.js +87 -0
  20. package/dist/demo.js +28 -11
  21. package/dist/desktop-update.d.ts +2 -2
  22. package/dist/evidence.d.ts +3 -40
  23. package/dist/evidence.js +5 -1
  24. package/dist/flow-gallery.d.ts +3 -2
  25. package/dist/flow-share.d.ts +10 -32
  26. package/dist/flow-share.js +65 -83
  27. package/dist/flow-starters.d.ts +2 -1
  28. package/dist/flow-triggers.d.ts +15 -72
  29. package/dist/flow-triggers.js +94 -83
  30. package/dist/flows.d.ts +37 -218
  31. package/dist/flows.js +350 -385
  32. package/dist/mate-tools.js +14 -44
  33. package/dist/mcp-connect.d.ts +3 -0
  34. package/dist/mcp-connect.js +16 -4
  35. package/dist/operate.js +31 -2
  36. package/dist/plan.d.ts +5 -5
  37. package/dist/plan.js +5 -25
  38. package/dist/process-recovery-eligibility.d.ts +2 -2
  39. package/dist/process-recovery-eligibility.js +13 -8
  40. package/dist/proof.d.ts +15 -91
  41. package/dist/proof.js +90 -249
  42. package/dist/result-evidence-readers.d.ts +42 -0
  43. package/dist/result-evidence-readers.js +91 -0
  44. package/dist/serve.js +8 -99
  45. package/dist/setup-guide.d.ts +2 -2
  46. package/dist/surface.js +8 -1
  47. package/dist/task-review-brief.d.ts +75 -0
  48. package/dist/task-review-brief.js +179 -0
  49. package/dist/text-limits.d.ts +70 -0
  50. package/dist/text-limits.js +70 -0
  51. package/dist/verification-evidence.js +24 -12
  52. package/package.json +1 -1
@@ -33,13 +33,16 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, write
33
33
  import { join } from "node:path";
34
34
  import { scanForSecrets } from "./evidence.js";
35
35
  import { flowCardText, flowDefinitionOf } from "./flow-engine.js";
36
+ import { FlowContractError } from "./flows.js";
37
+ import { parseContract } from "./contracts/contract.js";
38
+ import { FLOW_ALIASES, FLOW_TRIGGER_KINDS, SECRETS_MAX, triggerConfigSchema, triggerInputSchema } from "./contracts/flow.js";
36
39
  import { describeSchedule, firstFireAt, nextFireAt, parseSchedule, WEEKDAYS } from "./routine.js";
37
40
  import { takeReply } from "./flow-replies.js";
38
41
  import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./mailbox.js";
39
42
  import { readFlowSecrets } from "./flow-secrets.js";
40
43
  import { tmpdir } from "node:os";
41
44
  import { envValue } from "./names.js";
42
- export const FLOW_TRIGGER_KINDS = ["button", "schedule", "github", "linear", "flow", "webhook", "email", "chat", "plane-review"];
45
+ export { FLOW_TRIGGER_KINDS };
43
46
  export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
44
47
  /** What each kind is called on the canvas. */
45
48
  export const FLOW_TRIGGER_WORDS = {
@@ -54,23 +57,30 @@ const HOOK_BYTES = 1_000_000;
54
57
  /** Authors GitHub says have write access to the repository. */
55
58
  const TEAM = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
56
59
  // ------------------------------------------------------------ the terms
57
- function words(input, key, cap, required) {
58
- const value = input[key];
59
- if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
60
- if (required)
61
- throw new Error(`Say the trigger's ${key}.`);
62
- return null;
60
+ const HIDDEN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/;
61
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
62
+ /**
63
+ * A trigger's settings read by their one schema (`label: required`, `unknown key 'flows'`). A setting given as null is
64
+ * left out, and a button's blank question lines are dropped, as every release has read them. Then what the schema can't
65
+ * say, named by its path: no hidden characters and nothing that looks like a key, anywhere in the settings.
66
+ */
67
+ export function readTriggerSettings(raw) {
68
+ const body = isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== null).map(([key, value]) => [key, key === "questions" && Array.isArray(value) ? value.filter(one => typeof one !== "string" || one.trim() !== "") : value])) : raw;
69
+ const read = parseContract(triggerInputSchema, body, FLOW_ALIASES);
70
+ if (!read.ok)
71
+ return read;
72
+ for (const [key, value] of Object.entries(read.value)) {
73
+ for (const [n, one] of (Array.isArray(value) ? value : [value]).entries()) {
74
+ if (typeof one !== "string")
75
+ continue;
76
+ const path = Array.isArray(value) ? `${key}[${n}]` : key;
77
+ if (HIDDEN.test(one))
78
+ return { ok: false, issues: [{ path, kind: "bad-value", line: `${path}: can't contain hidden characters` }] };
79
+ if (scanForSecrets(one).length > 0)
80
+ return { ok: false, issues: [{ path, kind: "bad-value", line: `${path}: looks like a key or password; keys never go in a trigger's settings` }] };
81
+ }
63
82
  }
64
- if (typeof value !== "string")
65
- throw new Error(`The trigger's ${key} must be plain text.`);
66
- const trimmed = value.trim();
67
- if (trimmed.length > cap)
68
- throw new Error(`The trigger's ${key} is up to ${cap} characters.`);
69
- if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/.test(trimmed))
70
- throw new Error(`The trigger's ${key} can't contain hidden characters.`);
71
- if (scanForSecrets(trimmed).length > 0)
72
- throw new Error("That looks like a key or password. Keys never go in a trigger's settings.");
73
- return trimmed;
83
+ return read;
74
84
  }
75
85
  /** A schedule said in words ("every 2 hours", "daily 09:00 Europe/London", "monday 09:00") or in the routines' own form. */
76
86
  export function scheduleFromWords(text) {
@@ -116,111 +126,98 @@ export function githubRepoOf(path) {
116
126
  return null;
117
127
  }
118
128
  }
119
- /** A trigger's settings, checked whole against the flow it starts cards in. Throws in plain words. */
129
+ /** A trigger's settings, checked whole against the flow it starts cards in (readTriggerSettings, then the flow, its zones,
130
+ * the project's scripts and the schedule). Throws FlowContractError, each line naming its setting. */
120
131
  export function validateTriggerConfig(raw, context) {
121
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
122
- throw new Error("Choose what starts cards.");
123
- const input = raw;
124
- const kind = input["kind"];
125
- if (!FLOW_TRIGGER_KINDS.includes(kind))
126
- throw new Error("Choose what starts cards: a button, a schedule, GitHub, Linear, another flow, a webhook, an email inbox, a chat channel or a plane review.");
127
- const named = words(input, "zone", 60, false);
132
+ // Connecting a channel proves it's one the person is in: it happens in the channel, not here.
133
+ if (isRecord(raw) && raw["kind"] === "chat")
134
+ throw new FlowContractError([`kind: connect a chat channel from the channel itself: where Toolroll is in Slack, Discord, Teams or a Telegram group, send “flow ${context.flow.id}”`]);
135
+ const read = readTriggerSettings(raw);
136
+ if (!read.ok)
137
+ throw new FlowContractError(read.issues.map(one => one.line));
138
+ const input = read.value;
139
+ const refuse = (path, what) => { throw new FlowContractError([`${path}: ${what}`]); };
140
+ /** A setting's words, trimmed; blank is left out. */
141
+ const said = (value) => value === undefined || value.trim() === "" ? null : value.trim();
142
+ const named = said(input.zone);
128
143
  const zoneStage = named === null ? null : context.definition.stages.find(one => one.id === named || one.title.toLowerCase() === named.toLowerCase());
129
144
  if (named !== null && zoneStage === undefined)
130
- throw new Error(`This flow has no zone called ${named}.`);
145
+ refuse("zone", `this flow has no zone called ${named}`);
131
146
  const zone = zoneStage?.id ?? null;
132
- const choice = (key, options, fallback) => {
133
- const value = input[key];
134
- if (value === undefined || value === null || value === "")
135
- return fallback;
136
- if (!options.includes(value))
137
- throw new Error(`Choose the trigger's ${key}: ${options.join(" or ")}.`);
138
- return value;
139
- };
140
- switch (kind) {
147
+ switch (input.kind) {
141
148
  case "button": {
142
- const label = words(input, "label", 40, true);
143
- const asked = input["questions"];
144
- const questions = (Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split("\n") : [])
145
- .map(one => typeof one === "string" ? one.trim() : "").filter(one => one !== "");
149
+ const label = said(input.label) ?? refuse("label", "required");
150
+ const questions = (Array.isArray(input.questions) ? input.questions : typeof input.questions === "string" ? input.questions.split("\n") : []).map(one => one.trim()).filter(one => one !== "");
146
151
  if (questions.length > 6)
147
- throw new Error("A button asks at most 6 questions.");
148
- for (const question of questions)
149
- if (question.length > 80 || scanForSecrets(question).length > 0)
150
- throw new Error("Each question is a short line of plain text.");
152
+ refuse("questions", "at most 6 items");
153
+ questions.forEach((question, n) => { if (question.length > 80)
154
+ refuse(`questions[${n}]`, "at most 80 characters"); });
151
155
  return { kind: "button", label, questions: questions.length === 0 ? ["What needs doing?", "Details"] : questions, zone };
152
156
  }
153
157
  case "schedule": {
154
- const said = words(input, "schedule", 80, true);
155
- const schedule = scheduleFromWords(said);
156
- if (schedule === null)
157
- throw new Error("Say the schedule like “every 2 hours”, “daily 09:00 Europe/London”, “weekdays 09:00” or “monday 09:00”.");
158
+ const said_ = said(input.schedule) ?? refuse("schedule", "required");
159
+ const schedule = scheduleFromWords(said_) ?? refuse("schedule", "say it like “every 2 hours”, “daily 09:00 Europe/London”, “weekdays 09:00” or “monday 09:00”");
158
160
  // A script on a schedule (v90): each item it prints becomes a card.
159
- const script = words(input, "script", 40, false);
161
+ const script = said(input.script);
160
162
  if (script !== null) {
161
163
  if (context.store.flowScript(context.flow.repo, script) === null)
162
- throw new Error(`There's no script called ${script} in this project. Make it on the flow's Scripts panel first.`);
163
- const asked = input["secrets"];
164
- const secrets = asked === undefined || asked === null ? [] : Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split(/[\s,]+/).filter(one => one !== "") : null;
165
- if (secrets === null || secrets.length > 10 || secrets.some(one => typeof one !== "string" || !/^[A-Z][A-Z0-9_]{0,39}$/.test(one)))
166
- throw new Error("Name up to 10 saved secrets in capitals, like API_TOKEN.");
164
+ refuse("script", `there's no script called ${script} in this project; make it on the flow's Scripts panel first`);
165
+ const secrets = input.secrets === undefined ? [] : Array.isArray(input.secrets) ? input.secrets : input.secrets.split(/[\s,]+/).filter(one => one !== "");
166
+ if (secrets.length > SECRETS_MAX || secrets.some(one => !/^[A-Z][A-Z0-9_]{0,39}$/.test(one)))
167
+ refuse("secrets", "name up to 10 saved secrets in capitals, like API_TOKEN");
167
168
  return { kind: "schedule", schedule, title: `Items from ${script}`, description: null, zone, script, ...(secrets.length === 0 ? {} : { secrets: [...new Set(secrets)] }) };
168
169
  }
169
- return { kind: "schedule", schedule, title: words(input, "title", 200, true), description: words(input, "description", 2000, false), zone };
170
+ return { kind: "schedule", schedule, title: said(input.title) ?? refuse("title", "required"), description: said(input.description), zone };
170
171
  }
171
172
  case "github": {
172
- const repo = words(input, "repo", 140, false) ?? githubRepoOf(context.flow.repo);
173
+ const repo = said(input.repo) ?? githubRepoOf(context.flow.repo);
173
174
  if (repo === null || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo))
174
- throw new Error("Name the GitHub repository as owner/name.");
175
- const watch = choice("watch", ["issues", "pulls", "checks"], "issues");
176
- const label = watch === "checks" ? null : words(input, "label", 50, false);
177
- const branch = watch === "checks" ? words(input, "branch", 100, false) ?? "main" : null;
175
+ refuse("repo", "name the GitHub repository as owner/name");
176
+ const watch = input.watch ?? "issues";
177
+ const label = watch === "checks" ? null : said(input.label);
178
+ const branch = watch === "checks" ? said(input.branch) ?? "main" : null;
178
179
  if (branch !== null && !/^[A-Za-z0-9._/-]+$/.test(branch))
179
- throw new Error("That branch name isn't valid.");
180
- return { kind: "github", repo, watch, label, branch, from: choice("from", ["team", "anyone"], "team"), delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
180
+ refuse("branch", "that branch name isn't valid");
181
+ return { kind: "github", repo: repo, watch, label, branch, from: input.from ?? "team", delivery: input.delivery ?? "poll", zone };
181
182
  }
182
183
  case "linear": {
183
- const team = words(input, "team", 12, false), state = words(input, "state", 40, false), label = words(input, "label", 50, false);
184
+ const team = said(input.team), state = said(input.state), label = said(input.label);
184
185
  if (team !== null && !/^[A-Za-z0-9]+$/.test(team))
185
- throw new Error("A Linear team is its short key, like ENG.");
186
+ refuse("team", "a Linear team is its short key, like ENG");
186
187
  if (team === null && label === null)
187
- throw new Error("Name a Linear team or a label, so the trigger doesn't take every issue in the workspace.");
188
- return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
188
+ refuse("team", "name a Linear team or a label, so the trigger doesn't take every issue in the workspace");
189
+ return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery: input.delivery ?? "poll", zone };
189
190
  }
190
191
  case "flow": {
191
- const source = Number(input["flow"]);
192
- const flow = Number.isSafeInteger(source) ? context.store.getFlow(source) : null;
192
+ const flow = context.store.getFlow(input.flow);
193
193
  const sourceDefinition = flow === null ? null : flowDefinitionOf(flow);
194
194
  if (flow === null || flow.state !== "active" || sourceDefinition === null || !context.store.accountCanAccess(context.actor, flow.repo))
195
- throw new Error("Choose another flow in your projects.");
195
+ return refuse("flow", "choose another flow in your projects");
196
196
  if (flow.id === context.flow.id)
197
- throw new Error("A flow can't start cards in itself; move them with a zone's next step instead.");
198
- const when = words(input, "when", 60, false);
197
+ refuse("flow", "a flow can't start cards in itself; move them with a zone's next step instead");
198
+ const when = said(input.when);
199
199
  const stage = when === null ? sourceDefinition.stages.find(one => one.kind === "done") : sourceDefinition.stages.find(one => one.id === when || one.title.toLowerCase() === when.toLowerCase());
200
200
  if (stage === undefined)
201
- throw new Error(`${flow.name} has no zone called ${when ?? "Done"}.`);
201
+ return refuse("when", `${flow.name} has no zone called ${when ?? "Done"}`);
202
202
  return { kind: "flow", flow: flow.id, when: stage.id, zone };
203
203
  }
204
204
  case "webhook":
205
- return { kind: "webhook", title: words(input, "title", 120, false) ?? "Webhook", titleField: words(input, "titleField", 80, false), bodyField: words(input, "bodyField", 80, false), zone };
206
- case "chat":
207
- // Connecting a channel proves it's one the person is in: it happens in the channel, not here.
208
- throw new Error(`Connect a chat channel from the channel itself: where Toolroll is in Slack, Discord, Teams or a Telegram group, send “flow ${context.flow.id}”.`);
205
+ return { kind: "webhook", title: said(input.title) ?? "Webhook", titleField: said(input.titleField), bodyField: said(input.bodyField), zone };
209
206
  case "plane-review": {
210
- const at = words(input, "at", 5, false) ?? "07:30";
207
+ const at = said(input.at) ?? "07:30";
211
208
  const clock = /^(\d{1,2}):(\d{2})$/.exec(at);
212
- const zoneName = words(input, "timeZone", 60, false) ?? localTimeZone();
209
+ const zoneName = said(input.timeZone) ?? localTimeZone();
213
210
  const schedule = clock === null ? null : scheduleFromWords(`daily ${clock[1]}:${clock[2]} ${zoneName}`);
214
211
  if (schedule === null)
215
- throw new Error("Say the time it reviews the day as HH:MM, like 07:30, and a time zone like Europe/London.");
212
+ return refuse("at", "say the time it reviews the day as HH:MM, like 07:30, and a time zone like Europe/London");
216
213
  return { kind: "plane-review", schedule, zone };
217
214
  }
218
215
  case "email": {
219
- const folder = words(input, "folder", 100, false) ?? "INBOX";
220
- const sender = words(input, "sender", 300, false);
216
+ const folder = said(input.folder) ?? "INBOX";
217
+ const sender = said(input.sender);
221
218
  if (sender !== null && sendersOf(sender).length === 0)
222
- throw new Error("Say whom mail comes from as addresses or domains, like priya@example.com or example.com.");
223
- return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject: words(input, "subject", 100, false), zone };
219
+ refuse("sender", "say whom mail comes from as addresses or domains, like priya@example.com or example.com");
220
+ return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject: said(input.subject), zone };
224
221
  }
225
222
  }
226
223
  }
@@ -300,12 +297,26 @@ export function triggerHeadline(config, store) {
300
297
  }
301
298
  }
302
299
  export function triggerConfigOf(trigger) {
300
+ let raw;
303
301
  try {
304
- return JSON.parse(trigger.configJson);
302
+ raw = JSON.parse(trigger.configJson);
305
303
  }
306
304
  catch {
307
305
  return null;
308
306
  }
307
+ const read = readTriggerConfig(raw);
308
+ return read.ok ? read.value : null;
309
+ }
310
+ /** What a saved trigger leaves out, it had as these (every release has saved them whole; a hand-made row may not). */
311
+ const SAVED_DEFAULTS = {
312
+ button: { questions: ["What needs doing?", "Details"] }, schedule: { description: null }, github: { label: null, branch: null, from: "team", delivery: "poll" },
313
+ linear: { team: null, state: null, label: null, delivery: "poll" }, webhook: { title: "Webhook", titleField: null, bodyField: null }, email: { folder: "INBOX", sender: null, subject: null },
314
+ };
315
+ /** A saved trigger's config (`configJson`, which carries no version) read by its one schema, what it leaves out filled. */
316
+ export function readTriggerConfig(raw) {
317
+ const kind = isRecord(raw) ? raw["kind"] : undefined;
318
+ const body = isRecord(raw) ? { zone: null, ...SAVED_DEFAULTS[kind], ...raw } : raw;
319
+ return parseContract(triggerConfigSchema, body);
309
320
  }
310
321
  /** Whether this trigger takes deliveries at a secret address rather than being checked. */
311
322
  export function takesDeliveries(config) {
package/dist/flows.d.ts CHANGED
@@ -1,172 +1,57 @@
1
- export declare const FLOW_STAGE_KINDS: readonly ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "send", "choose", "done"];
2
- export type FlowStageKind = (typeof FLOW_STAGE_KINDS)[number];
3
- export declare const FLOW_COLORS: readonly ["slate", "blue", "violet", "amber", "green", "rose"];
4
- export type FlowColor = (typeof FLOW_COLORS)[number];
5
- export type FlowZone = {
6
- x: number;
7
- y: number;
8
- w: number;
9
- h: number;
10
- color: FlowColor;
11
- };
12
- /** One answer a sort zone can pick: its name, what it means (what Jev reads), and the zone it sends the card to. */
13
- export type FlowSortAnswer = {
14
- answer: string;
15
- means: string;
16
- to: string;
17
- };
18
- /** Something else a sort zone notes on the card: a score on a scale of levels, or a yes/no. */
19
- export type FlowSortNote = {
20
- id: string;
21
- kind: "score" | "yes-no";
22
- question: string;
23
- levels: string[] | null;
24
- };
25
- /** sort: the question, its answers, how sure Jev must be to act alone (0.5–0.99), and what else it notes. */
26
- export type FlowSort = {
27
- question: string;
28
- answers: FlowSortAnswer[];
29
- sureAt: number;
30
- notes: FlowSortNote[];
31
- };
32
- /** request: what is called. The address's scheme and host are fixed; fill-ins go in its path and query (encoded), headers and body. */
33
- export type FlowRequest = {
34
- method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
35
- url: string;
36
- headers: Record<string, string>;
37
- body: string | null;
38
- };
39
- /** email: who it goes to, the subject and the text — all with fill-ins. */
40
- export type FlowEmail = {
41
- to: string;
42
- subject: string;
43
- body: string;
44
- };
45
- /** tool: which of the project's tools (MCP servers), which of its functions, and the arguments as JSON with fill-ins. */
46
- export type FlowTool = {
47
- server: string;
48
- name: string;
49
- args: string;
50
- };
51
- /** wait (v91): for a reply to the card's email (next: replied, onFail: no reply in time), or for a set time (then next).
52
- * "hours": until the clock is between `from` and `to` (like 22:00–06:00), in the computer's time zone unless one is named. */
53
- export type FlowWait = {
54
- for: "reply" | "time" | "hours";
55
- minutes: number;
56
- from?: string;
57
- to?: string;
58
- timeZone?: string;
59
- };
60
- /** How a Pull request zone merges once checks pass. */
61
- export declare const FLOW_MERGE_METHODS: readonly ["squash", "merge", "rebase"];
62
- export type FlowMergeMethod = (typeof FLOW_MERGE_METHODS)[number];
63
- /** A choose zone's option: its button's words, and the zone it leads to (FLOW_END closes the card as Ignored). */
64
- export type FlowChoice = {
65
- label: string;
66
- to: string;
67
- };
68
- /** Where a choose option that ends the card leads. */
69
- export declare const FLOW_END = "end";
70
- /** How many options a choose zone offers. */
71
- export declare const CHOICES_MIN = 2, CHOICES_MAX = 4;
72
- /** A time limit on a zone (v91): after this long, the person it waits on is reminded, and a Holding or
73
- * "Person decides" zone can move the card on (`to`). */
74
- export type FlowLimit = {
75
- minutes: number;
76
- to: string | null;
77
- };
78
- export type FlowStage = {
79
- id: string;
80
- title: string;
81
- kind: FlowStageKind;
82
- zone: FlowZone;
83
- /** task/report: what the agent is asked to do. `{{card.title}}`,
84
- * `{{card.description}}`, `{{note}}` (the latest send-back note) and
85
- * `{{stage.<id>}}` (an earlier zone's report) are filled in. */
86
- instructions: string | null;
87
- /** task: plan first (required), let Toolroll decide (auto), or build directly (skip). */
88
- planning: "auto" | "required" | "skip" | null;
89
- /** approval: the one person who decides, or null for any approver on the project. */
90
- approver: string | null;
91
- /** approval: the flow's owner decides (v86), whoever that is when the card arrives; `approver` is then unused. */
92
- toOwner?: boolean;
93
- /** notify: the message; update: the comment left on the issue. Same fill-ins. */
94
- message: string | null;
95
- /** update: also close the issue (Linear: move it to the team's done state). */
96
- close: boolean | null;
97
- /** check: the project script (by name) run with no AI. */
98
- script: string | null;
99
- /** check (v90): where it runs — a clean folder, or a copy of the card's work (the default for zones made before v90);
100
- * the answers a script's "goto:" line may pick, each with the zone it leads to; and the flow secrets it gets as variables. */
101
- runIn?: "folder" | "copy";
102
- routes?: {
103
- answer: string;
104
- to: string;
105
- }[];
106
- secrets?: string[];
107
- /** sort: what Jev is asked and where each answer leads. Its onFail is where a card goes when Jev isn't sure. */
108
- sort: FlowSort | null;
109
- /** request, email, tool (v87): what the step sends, and to where. */
110
- request?: FlowRequest;
111
- email?: FlowEmail;
112
- tool?: FlowTool;
113
- /** wait (v91): what it waits for, and how long. */
114
- wait?: FlowWait;
115
- /** Any zone but Wait and Done (v91): how long a card may sit here before someone is reminded. */
116
- limit?: FlowLimit;
117
- /** v92: the AI teammate (by handle) who decides a "Person decides" zone (handing hard ones to its person), or handles a "Teammate handles it" zone. */
118
- teammate?: string;
119
- /** v96: a "Teammate handles it" zone sends what the teammate writes back to whoever asked (the person who added the card, its chat thread, or the teammate's manager). */
120
- reply?: boolean;
121
- /** choose: the buttons the person picks from, in order. Its onFail is where a reply goes, with the reply as {{note}}. */
122
- options?: FlowChoice[];
123
- /** task: the project it builds in, when not the flow's own (one the flow's owner may file in). */
124
- repo?: string;
125
- /** pull-request: merge once checks pass, this way. Only allowed after a "Person decides" zone, and a card merges only when a person approved it after it was built. */
126
- merge?: FlowMergeMethod;
127
- /** Where a card goes when this zone's step succeeds, and when it fails or is sent back (sort: when it isn't sure). */
128
- next: string | null;
129
- onFail: string | null;
130
- };
131
- export type FlowDefinition = {
132
- version: 1;
133
- start: string;
134
- stages: FlowStage[];
135
- };
1
+ import { type ContractIssue, type ContractResult } from "./contracts/contract.js";
2
+ import { CHOICES_MAX, CHOICES_MIN, FLOW_COLORS, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, LONGEST_WAIT_MINUTES, SCRIPT_LANGUAGES, SCRIPT_NAME, type FlowDefinition, type FlowStage, type FlowStageKind, type FlowStepFields, type FlowWait, type ScriptLanguage, type TriggerInput } from "./contracts/flow.js";
3
+ export { CHOICES_MAX, CHOICES_MIN, FLOW_COLORS, FLOW_END, FLOW_MERGE_METHODS, FLOW_STAGE_KINDS, LONGEST_WAIT_MINUTES, SCRIPT_LANGUAGES, SCRIPT_NAME };
4
+ export type { FlowChoice, FlowColor, FlowDefinition, FlowEmail, FlowLimit, FlowMergeMethod, FlowRequest, FlowSort, FlowSortAnswer, FlowSortNote, FlowStage, FlowStageKind, FlowStepInput, FlowTool, FlowWait, FlowZone, ScriptLanguage, } from "./contracts/flow.js";
136
5
  /** What each kind is called and does, in the words the canvas uses. */
137
6
  export declare const FLOW_KIND_WORDS: Record<FlowStageKind, {
138
7
  label: string;
139
8
  about: string;
140
9
  }>;
141
- /** A project script's name: short, lowercase, dashes — how zones and chat refer to it. */
142
- export declare const SCRIPT_NAME: RegExp;
143
- /** The languages a project script is written in (v90). */
144
- export declare const SCRIPT_LANGUAGES: readonly ["shell", "python", "node"];
145
- export type ScriptLanguage = (typeof SCRIPT_LANGUAGES)[number];
146
10
  export declare const LANGUAGE_WORDS: Record<ScriptLanguage, string>;
147
11
  /** How an answer is named to Jev: its words as a short key. */
148
12
  export declare const sortKeyOf: (answer: string) => string;
149
13
  /** The default for "sure enough to act alone". */
150
14
  export declare const SORT_SURE_AT = 0.8;
151
- /** The longest a zone waits or a limit runs: 30 days. */
152
- export declare const LONGEST_WAIT_MINUTES: number;
153
15
  /** "3 days", "4 hours", "90 minutes", "1 week" (or a number of minutes) as minutes; null when it isn't one. */
154
16
  export declare function durationMinutes(value: unknown): number | null;
155
17
  /** Minutes in the words people use: "3 days", "1 day 4 hours", "45 minutes". */
156
18
  export declare function durationWords(minutes: number): string;
157
19
  /** "22:00", "9:30" as "HH:MM"; null when it isn't a time of day. */
158
20
  export declare function clockTime(value: unknown): string | null;
21
+ export declare function knownTimeZone(zone: string): boolean;
159
22
  /** Whether a time falls inside an "hours" wait (22:00–06:00 runs past midnight), in its time zone or this computer's. */
160
23
  export declare function withinHours(wait: Pick<FlowWait, "from" | "to" | "timeZone">, now: Date): boolean;
161
24
  /** Who decides at an approval zone: its named person, the flow's owner, or null for anyone who approves on the project. */
162
25
  export declare function deciderOf(stage: Pick<FlowStage, "approver" | "toOwner">, flow: {
163
26
  owner: string;
164
27
  }): string | null;
165
- /** A flow as drawn on the canvas, checked whole. Throws in plain words. */
166
- /** `stored`: reading a flow saved earlier, which keeps working under rules added since (its cards say what's wrong). */
28
+ /** A flow refused by its contract: every problem, each naming its path (`stages[2].instructions: required`). */
29
+ export declare class FlowContractError extends Error {
30
+ readonly lines: readonly string[];
31
+ constructor(lines: readonly string[]);
32
+ }
33
+ /** What JSON Schema can't say about a drawing, each named by its path: paths lead to zones that exist and never back into
34
+ * their own zone, answers and options have names of their own, a web address's host is written out, tool arguments are a
35
+ * JSON object, an hours wait's times differ in a real time zone, and a merge comes after a person's decision. */
36
+ export declare function drawingProblems(definition: FlowDefinition): ContractIssue[];
37
+ /**
38
+ * Read a flow's drawing: the canvas's, a step list's once built, or one saved earlier (`stored`, which keeps working under
39
+ * rules added since: its instructions keep the length they were saved with). A version 1 drawing is read as every release
40
+ * has read it (canonicalStage), then the schema checks it, then drawingProblems; a newer version is refused plainly.
41
+ */
42
+ export declare function readFlowDefinition(input: unknown, options?: {
43
+ stored?: boolean;
44
+ }): ContractResult<FlowDefinition>;
45
+ /** A refusal of the drawing a step list or flow file became, in its words: `stages[2].sort.answers[0].to` is
46
+ * `steps[2].answers[0].goesTo` (each zone is the step at its index), and a zone's failure path is the step's own name for it. */
47
+ export declare function inStepWords(line: string, list: "steps" | "zones", kinds: readonly (string | undefined)[]): string;
48
+ /** A flow as drawn on the canvas, checked whole (readFlowDefinition). Throws FlowContractError, naming every path. */
167
49
  export declare function validateFlowDefinition(input: unknown, options?: {
168
50
  stored?: boolean;
169
51
  }): FlowDefinition;
52
+ /** A drawing's refusal as the canvas shows it: each line as it is, after the name of the zone it is about, so a person
53
+ * finds it on the canvas (`Build · stages[1].instructions: required`). */
54
+ export declare function withZoneNames(lines: readonly string[], input: unknown): string;
170
55
  /** The instructions 0.9.26 and earlier read from a saved zone: they re-check this length on every read, and a flow with a
171
56
  * longer one won't load. A saved zone keeps its first part under it, and the rest in `instructionsMore`, which they ignore. */
172
57
  export declare const FLOW_INSTRUCTIONS_STORED = 4000;
@@ -217,86 +102,21 @@ export declare function cardEmailOf(card: {
217
102
  title: string;
218
103
  description: string | null;
219
104
  }): string;
220
- /** A step as the lead describes it, in list order. A step that keeps an
221
- * existing zone (by `id`) carries over whatever it leaves out. */
222
- export type FlowStepInput = {
223
- id?: string;
224
- title?: string;
225
- kind?: FlowStageKind;
226
- instructions?: string;
227
- planning?: "auto" | "required" | "skip";
228
- decider?: string | null;
229
- message?: string;
230
- script?: string;
231
- close?: boolean;
232
- /** check (v90): "folder" (a clean folder) or "copy" (a copy of the card's work); the answers a "goto:" line picks, each with the step it goes to; saved secrets it gets. */
233
- runIn?: "folder" | "copy";
234
- routes?: {
235
- answer?: string;
236
- goesTo?: string;
237
- }[];
238
- secrets?: string[];
239
- /** sort: the question, each answer with what it means and the step it goes to, how sure Jev must be (a percentage), and up to 3 other things to note. */
240
- question?: string;
241
- answers?: {
242
- answer?: string;
243
- means?: string;
244
- goesTo?: string;
245
- }[];
246
- sureAt?: number;
247
- alsoNote?: {
248
- question?: string;
249
- kind?: "score" | "yes-no";
250
- levels?: string[];
251
- }[];
252
- /** request: method, address, headers and body; email: to, subject, body; tool: the tool (server), its function (tool) and arguments. */
253
- method?: FlowRequest["method"];
254
- url?: string;
255
- headers?: Record<string, string>;
256
- body?: string;
257
- to?: string;
258
- subject?: string;
259
- server?: string;
260
- tool?: string;
261
- args?: Record<string, unknown> | string;
262
- /** wait (v91): for a reply (the default) or a set time, and how long ("3 days"); where a card goes when no reply comes.
263
- * "hours": until the clock is between `from` and `until` ("22:00", "06:00"). */
264
- waitFor?: "reply" | "time" | "hours";
265
- wait?: string | number;
266
- ifNoReply?: string;
267
- from?: string;
268
- until?: string;
269
- /** pull-request: merge once checks pass (true is squash). Needs a decision before it. */
270
- merge?: boolean | FlowMergeMethod;
271
- /** Any step but wait and done (v91): remind after this long ("2 days"; "none" removes it), and on holding and approval steps, move the card to this step then. */
272
- remindAfter?: string | number;
273
- thenMoveTo?: string;
274
- /** v92: the AI teammate (by name) who decides an approval step (handing hard ones to its decider) or handles a teammate step. */
275
- teammate?: string;
276
- /** v96: a teammate step sends what it writes back to whoever asked. */
277
- reply?: boolean;
278
- /** choose: 2 to 4 buttons, each with its words and the step it goes to ("end" ignores the card); where a reply goes instead
279
- * (the reply is its {{note}}; the nearest earlier build or research step when left out). ifNoReply (with remindAfter) moves it on. */
280
- options?: {
281
- label?: string;
282
- goesTo?: string;
283
- }[];
284
- ifReplied?: string;
285
- /** task: the project it builds in (path), when not the flow's own. */
286
- repo?: string;
287
- next?: string;
288
- ifFails?: string;
289
- ifNotSure?: string;
290
- };
291
105
  /** What a draft zone asks for when the steps leave it out. */
292
106
  export declare const DRAFT_DEFAULT = "Write a short, friendly reply to the person who sent this card, in plain words.";
293
107
  /** Steps as the lead's flow tool and `toolroll flows` take them: "me" as a decider is the person asking, stored by name. */
294
108
  export declare function stepsFor(steps: unknown, name: string): unknown;
109
+ /** An edit's steps with what a kept step leaves out of its kind and name filled from the zone it keeps (by id), so each
110
+ * reads as the one schema its kind has. Anything else it leaves out carries over when the flow is drawn (flowFromSteps). */
111
+ export declare function withKeptSteps(steps: unknown, previous: FlowDefinition | null): unknown;
112
+ /** A flow's steps, read by their one schema: `steps[0].routes[0]: unknown key 'to' (did you mean goesTo?)`. */
113
+ export declare function readFlowSteps(input: unknown, previous?: FlowDefinition | null): ContractResult<FlowStepFields[]>;
295
114
  /**
296
115
  * A flow from an ordered list of steps: ids from names, each step leading
297
116
  * to the next, a Done zone at the end when none is listed, and a decision
298
117
  * sending work back to the nearest earlier step that does work. New zones
299
- * are laid out in rows; zones kept from `previous` keep their place.
118
+ * are laid out in rows; zones kept from `previous` keep their place. Throws
119
+ * FlowContractError, each line naming the step's path (`steps[2].next: ...`).
300
120
  */
301
121
  export declare function flowFromSteps(input: unknown, previous?: FlowDefinition | null): FlowDefinition;
302
122
  /** What confirming a new or changed flow means, in the canvas's words: each step's job and path, what changed, and that the usual approvals still apply. */
@@ -310,6 +130,5 @@ export declare const FLOW_TEMPLATES: readonly {
310
130
  label: string;
311
131
  about: string;
312
132
  definition: FlowDefinition;
313
- trigger?: Record<string, unknown>;
133
+ trigger?: TriggerInput;
314
134
  }[];
315
- export {};