toolroll 0.9.20 → 0.9.21
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.
- package/CHANGELOG.md +14 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -21
- package/dist/browser-workspace.d.ts +41 -1
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +57 -6
- package/dist/chat-delivery-state.d.ts +15 -0
- package/dist/chat-delivery-state.js +27 -2
- package/dist/chat-delivery.js +24 -4
- package/dist/chat-flow.d.ts +26 -3
- package/dist/chat-flow.js +217 -5
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +2 -2
- package/dist/evidence.d.ts +13 -1
- package/dist/evidence.js +10 -1
- package/dist/flow-engine.d.ts +7 -0
- package/dist/flow-engine.js +71 -27
- package/dist/flow-gallery-ui.d.ts +1 -1
- package/dist/flow-gallery-ui.js +8 -7
- package/dist/flow-gallery.d.ts +8 -0
- package/dist/flow-gallery.js +37 -3
- package/dist/flow-send.d.ts +80 -0
- package/dist/flow-send.js +249 -0
- package/dist/flow-share.js +7 -3
- package/dist/flow-steps.js +1 -1
- package/dist/flows-cli.js +11 -3
- package/dist/flows-ui.js +20 -4
- package/dist/flows.d.ts +27 -1
- package/dist/flows.js +104 -18
- package/dist/mate-tools.js +33 -10
- package/dist/operate.js +41 -0
- package/dist/project-delete.js +4 -0
- package/dist/project-demo.d.ts +12 -0
- package/dist/project-demo.js +71 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.js +4 -1
- package/dist/report-summary.d.ts +9 -0
- package/dist/report-summary.js +9 -0
- package/dist/result-shots.d.ts +2 -0
- package/dist/result-shots.js +24 -5
- package/dist/scout-net.d.ts +22 -0
- package/dist/scout-net.js +214 -0
- package/dist/scout-report.d.ts +67 -0
- package/dist/scout-report.js +108 -1
- package/dist/scout.d.ts +42 -0
- package/dist/scout.js +225 -16
- package/dist/serve.js +43 -5
- package/dist/slack-chat.js +3 -3
- package/dist/store.d.ts +53 -0
- package/dist/store.js +95 -4
- package/dist/teams-chat.js +1 -1
- package/dist/telegram-flow.d.ts +16 -2
- package/dist/telegram-flow.js +58 -0
- package/dist/telegram.js +46 -10
- package/package.json +1 -1
package/dist/flows-ui.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parseSortDecision, sortChip } from "./flow-sort.js";
|
|
|
6
6
|
import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
|
|
7
7
|
import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
|
|
8
8
|
import { flowFingerprint } from "./flow-live.js";
|
|
9
|
+
import { flowChoiceAt, readFlowSend } from "./flow-send.js";
|
|
9
10
|
import { FLOW_FILE_MAX_BYTES } from "./flow-share.js";
|
|
10
11
|
import { googleConnected } from "./google-mail.js";
|
|
11
12
|
import { mailboxReady } from "./mailbox.js";
|
|
@@ -20,7 +21,7 @@ export function flowsListHtml(store, flows, projects, csrf, canCreate, problem)
|
|
|
20
21
|
const rows = flows.map(flow => {
|
|
21
22
|
const cards = store.flowCards(flow.id, false);
|
|
22
23
|
const definition = flowDefinitionOf(flow);
|
|
23
|
-
const waiting = cards.filter(card => definition?.stages.find(one => one.id === card.stage)?.kind
|
|
24
|
+
const waiting = cards.filter(card => ["approval", "choose"].includes(definition?.stages.find(one => one.id === card.stage)?.kind ?? "")).length;
|
|
24
25
|
const buttons = store.flowTriggers(flow.id).filter(one => one.state === "active").flatMap(one => { const config = triggerConfigOf(one); return config?.kind === "button" ? [{ id: one.id, label: config.label }] : []; });
|
|
25
26
|
const trouble = troubleWords(flowInsights(store, flow, new Date(), 7));
|
|
26
27
|
return `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${e(flow.name)}</a></h2><span class="flow-counts">${e(projectName(flow.repo))} · ${cards.length} card${cards.length === 1 ? "" : "s"} in progress${waiting > 0 ? ` · ${waiting} waiting for a decision` : ""}${trouble === null ? "" : ` · ${e(trouble)}`}</span></div>` +
|
|
@@ -68,7 +69,8 @@ const historyText = (event, title, sorts = new Set()) => {
|
|
|
68
69
|
case "approved": return `Approved${who}${note}`;
|
|
69
70
|
case "sent-back": return `Sent back to ${title(event.toStage)}${who}${note}`;
|
|
70
71
|
case "fail": return `Moved to ${title(event.toStage)} after a problem${note}`;
|
|
71
|
-
|
|
72
|
+
// "Ignore" on a choice closes the card (flow-send.ts): said as such, with what was chosen and where.
|
|
73
|
+
case "cancelled": return event.note?.startsWith("Ignored") ? `Ignored${who}${event.note.slice("Ignored".length)}` : `Cancelled${who}`;
|
|
72
74
|
case "ok": return event.fromStage !== null && sorts.has(event.fromStage) ? `Sorted into ${title(event.toStage)}` : `Moved on to ${title(event.toStage)}${note}`;
|
|
73
75
|
default: return `Moved to ${title(event.toStage)}${who}${note}`;
|
|
74
76
|
}
|
|
@@ -121,21 +123,34 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
const asker = asked === null || asked.state !== "open" ? null : store.getTeammate(asked.teammate);
|
|
126
|
+
// "Person chooses" (flow-send.ts): what was sent, and the options, for the person it waits on; the latest send otherwise.
|
|
127
|
+
const choosing = card.state === "active" && stage?.kind === "choose" ? flowChoiceAt(store, card.id, card.entry) : null;
|
|
128
|
+
const latest = store.flowSends(card.id)[0] ?? null;
|
|
129
|
+
const sentContent = latest === null ? null : readFlowSend(latest.contentJson);
|
|
130
|
+
// On the card itself: where it came from (the card's title is right above), and only the links the panel doesn't already have.
|
|
131
|
+
const own = new Set([`/flows/${flow.id}?card=${card.id}`, ...(task === null ? [] : [`/t/${encodeURIComponent(task)}`])]);
|
|
132
|
+
const sentView = (content) => ({ title: content.from === undefined ? "What was done" : `From ${content.from}`, summary: content.summary,
|
|
133
|
+
links: content.links.map(one => ({ label: one.label, href: "url" in one ? one.url : one.path })).filter(one => !own.has(one.href)) });
|
|
124
134
|
return {
|
|
125
135
|
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state, waiting: card.waiting,
|
|
126
136
|
task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}` },
|
|
127
137
|
createdBy: card.createdBy, updatedAt: card.updatedAt,
|
|
128
138
|
canDecide,
|
|
129
|
-
|
|
139
|
+
// What a "Send to me" sent shows once, in its own box (sent), not again here; a report zone's items and full report
|
|
140
|
+
// (`<id>.items`, `<id>.report`) are for later zones, and its summary is shown.
|
|
141
|
+
outputs: Object.entries(card.outputs).filter(([id]) => !id.includes(".") && stages.find(one => one.id === id)?.kind !== "send").map(([id, text]) => ({ stage: id, title: title(id), text })),
|
|
130
142
|
history: [...moves, ...owned].sort((a, b) => b.at.localeCompare(a.at)).slice(0, 30),
|
|
131
143
|
source: card.source,
|
|
132
144
|
owner: card.owner, watchers, watching: watchers.includes(viewer.name),
|
|
133
|
-
mine: card.owner === viewer.name || watchers.includes(viewer.name) || canDecide,
|
|
145
|
+
mine: card.owner === viewer.name || watchers.includes(viewer.name) || canDecide || choosing?.person === viewer.name,
|
|
134
146
|
comments: discussion.filter(one => one.kind === "comment").map(one => ({ id: one.id, author: one.author, body: one.body, mentions: one.mentions, at: one.at })),
|
|
135
147
|
sorted: decision === null ? null : { chip: sortChip(decision), confident: decision.confident },
|
|
136
148
|
draft: shown === null || card.state !== "active" || card.outputs[shown.id] === undefined ? null : { zone: shown.id, title: shown.title, text: card.outputs[shown.id] },
|
|
137
149
|
deadline,
|
|
138
150
|
handoff,
|
|
151
|
+
choose: choosing === null || sentContent === null || latest?.entry !== card.entry ? null : { entry: card.entry, ...sentView(sentContent), person: choosing.person, mine: choosing.person === viewer.name,
|
|
152
|
+
options: (sentContent.options ?? []).filter(one => choosing.stage.options?.[one.choice]?.label === one.label), reply: sentContent.reply === true },
|
|
153
|
+
sent: sentContent === null || (choosing !== null && latest?.entry === card.entry) ? null : { ...sentView(sentContent), person: latest.person, at: latest.createdAt },
|
|
139
154
|
question: asked === null || asked.state !== "open" || asker === null ? null
|
|
140
155
|
: { id: asked.id, from: labelOf(asker), question: asked.question, options: asked.options, askedOf: asked.askedOf, mine: asked.askedOf === viewer.name,
|
|
141
156
|
call: pending === null ? null : { why: pending.why, rule: pending.result ?? "" } },
|
|
@@ -188,6 +203,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
188
203
|
live: flowFingerprint(store, flow.id),
|
|
189
204
|
canEdit: viewer.approver,
|
|
190
205
|
approvers: store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, flow.repo)),
|
|
206
|
+
projects: [flow.repo, ...setup.repos.filter(one => one !== flow.repo)].map(path => ({ path, name: projectName(path) })),
|
|
191
207
|
kinds: FLOW_STAGE_KINDS.map(kind => ({ kind, label: FLOW_KIND_WORDS[kind].label, about: FLOW_KIND_WORDS[kind].about })),
|
|
192
208
|
colors: [...FLOW_COLORS],
|
|
193
209
|
};
|
package/dist/flows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const FLOW_STAGE_KINDS: readonly ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
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
2
|
export type FlowStageKind = (typeof FLOW_STAGE_KINDS)[number];
|
|
3
3
|
export declare const FLOW_COLORS: readonly ["slate", "blue", "violet", "amber", "green", "rose"];
|
|
4
4
|
export type FlowColor = (typeof FLOW_COLORS)[number];
|
|
@@ -60,6 +60,15 @@ export type FlowWait = {
|
|
|
60
60
|
/** How a Pull request zone merges once checks pass. */
|
|
61
61
|
export declare const FLOW_MERGE_METHODS: readonly ["squash", "merge", "rebase"];
|
|
62
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;
|
|
63
72
|
/** A time limit on a zone (v91): after this long, the person it waits on is reminded, and a Holding or
|
|
64
73
|
* "Person decides" zone can move the card on (`to`). */
|
|
65
74
|
export type FlowLimit = {
|
|
@@ -109,6 +118,10 @@ export type FlowStage = {
|
|
|
109
118
|
teammate?: string;
|
|
110
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). */
|
|
111
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;
|
|
112
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. */
|
|
113
126
|
merge?: FlowMergeMethod;
|
|
114
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). */
|
|
@@ -156,6 +169,10 @@ export declare function validateFlowDefinition(input: unknown, options?: {
|
|
|
156
169
|
}): FlowDefinition;
|
|
157
170
|
/** The zones a card can reach from `start` without passing through a zone `stop` picks (those zones aren't included). */
|
|
158
171
|
export declare function reachableWithout(stages: readonly FlowStage[], start: string, stop: (stage: FlowStage) => boolean): Set<string>;
|
|
172
|
+
/** The zones a choose zone's options lead to (an option that ends the card leads to none). */
|
|
173
|
+
export declare function choiceTargets(stage: Pick<FlowStage, "options">): string[];
|
|
174
|
+
/** Where a reply to a choice goes, as its {{note}}: the zone's reply path, else its first option that leads somewhere; null when none does. */
|
|
175
|
+
export declare function replyTarget(stage: Pick<FlowStage, "options" | "onFail">): string | null;
|
|
159
176
|
/** What a card's work is held to: the zones' steps and paths, never where they sit on the canvas. */
|
|
160
177
|
export declare function flowDigest(definition: FlowDefinition): string;
|
|
161
178
|
type FlowFillCard = {
|
|
@@ -243,6 +260,15 @@ export type FlowStepInput = {
|
|
|
243
260
|
teammate?: string;
|
|
244
261
|
/** v96: a teammate step sends what it writes back to whoever asked. */
|
|
245
262
|
reply?: boolean;
|
|
263
|
+
/** choose: 2 to 4 buttons, each with its words and the step it goes to ("end" ignores the card); where a reply goes instead
|
|
264
|
+
* (the reply is its {{note}}; the nearest earlier build or research step when left out). ifNoReply (with remindAfter) moves it on. */
|
|
265
|
+
options?: {
|
|
266
|
+
label?: string;
|
|
267
|
+
goesTo?: string;
|
|
268
|
+
}[];
|
|
269
|
+
ifReplied?: string;
|
|
270
|
+
/** task: the project it builds in (path), when not the flow's own. */
|
|
271
|
+
repo?: string;
|
|
246
272
|
next?: string;
|
|
247
273
|
ifFails?: string;
|
|
248
274
|
ifNotSure?: string;
|
package/dist/flows.js
CHANGED
|
@@ -35,16 +35,27 @@
|
|
|
35
35
|
* waits for CI, and moves on when it passes or down the
|
|
36
36
|
* failure path, naming the failing check, when it fails. It
|
|
37
37
|
* can merge once checks pass, only after a person approved.
|
|
38
|
+
* - send — "Send to me": sends the card's person (its owner, else the
|
|
39
|
+
* flow's) what the step before produced — its summary, links
|
|
40
|
+
* and images — in each chat app they paired and on the card,
|
|
41
|
+
* then moves on.
|
|
42
|
+
* - choose — "Person chooses": the same content, with 2 to 4 buttons the
|
|
43
|
+
* flow names (each leads to a zone, or ends the card as
|
|
44
|
+
* Ignored). A reply instead is the {{note}} for where replies go.
|
|
38
45
|
* - done — the end.
|
|
39
46
|
*
|
|
40
47
|
* The engine is deterministic and model-free: it runs in the worker's pass
|
|
41
48
|
* beside routines, and no card ever skips an approval the task itself needs.
|
|
42
49
|
*/
|
|
43
50
|
import { createHash } from "node:crypto";
|
|
44
|
-
export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
51
|
+
export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "send", "choose", "done"];
|
|
45
52
|
export const FLOW_COLORS = ["slate", "blue", "violet", "amber", "green", "rose"];
|
|
46
53
|
/** How a Pull request zone merges once checks pass. */
|
|
47
54
|
export const FLOW_MERGE_METHODS = ["squash", "merge", "rebase"];
|
|
55
|
+
/** Where a choose option that ends the card leads. */
|
|
56
|
+
export const FLOW_END = "end";
|
|
57
|
+
/** How many options a choose zone offers. */
|
|
58
|
+
export const CHOICES_MIN = 2, CHOICES_MAX = 4;
|
|
48
59
|
/** What each kind is called and does, in the words the canvas uses. */
|
|
49
60
|
export const FLOW_KIND_WORDS = {
|
|
50
61
|
inbox: { label: "Holding", about: "Cards wait here until someone moves them." },
|
|
@@ -62,6 +73,8 @@ export const FLOW_KIND_WORDS = {
|
|
|
62
73
|
sort: { label: "Sort", about: "Jev reads the card in under a second and sends it where its answer leads. Cards it isn't sure about take the not-sure path." },
|
|
63
74
|
wait: { label: "Wait", about: "Waits for a reply to the card's email, or for a set time. A reply moves the card on; if none comes in time, it takes the no-reply path." },
|
|
64
75
|
teammate: { label: "Teammate handles it", about: "An AI teammate reads the card, decides where it goes within its rules, and writes what the next zones send. When its rules say to ask, it asks you first." },
|
|
76
|
+
send: { label: "Send to me", about: "Sends the card's owner what the step before produced: its summary, links and any screenshots, in each chat app they use and on the card. Then moves on." },
|
|
77
|
+
choose: { label: "Person chooses", about: "Sends the card's owner what was done with 2 to 4 buttons you name, in their chat apps and here. Each leads to a zone or ignores the card; a reply instead goes where replies go, as the note." },
|
|
65
78
|
done: { label: "Done", about: "The end of the flow." },
|
|
66
79
|
};
|
|
67
80
|
const ID = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
@@ -264,8 +277,8 @@ function validateLimit(input, kind, title) {
|
|
|
264
277
|
if (minutes === null)
|
|
265
278
|
throw new Error(`Zone ${title}: say how long a card may wait, from 1 minute to 30 days.`);
|
|
266
279
|
const to = text(raw["to"], 32);
|
|
267
|
-
if (to !== null && kind !== "inbox" && kind !== "approval")
|
|
268
|
-
throw new Error(`Zone ${title}: only Holding and "Person
|
|
280
|
+
if (to !== null && kind !== "inbox" && kind !== "approval" && kind !== "choose")
|
|
281
|
+
throw new Error(`Zone ${title}: only Holding, "Person decides" and "Person chooses" zones move a card on when it waits too long; other zones remind.`);
|
|
269
282
|
return { minutes, to };
|
|
270
283
|
}
|
|
271
284
|
/** Who decides at an approval zone: its named person, the flow's owner, or null for anyone who approves on the project. */
|
|
@@ -314,8 +327,10 @@ export function validateFlowDefinition(input, options = {}) {
|
|
|
314
327
|
...((kind === "approval" || kind === "teammate") && stage["teammate"] !== undefined && stage["teammate"] !== null && stage["teammate"] !== "" ? { teammate: validateTeammate(stage["teammate"], title) } : {}),
|
|
315
328
|
...(kind === "teammate" ? validateRoutes(stage, title) : {}),
|
|
316
329
|
...(kind === "teammate" && stage["reply"] === true ? { reply: true } : {}),
|
|
330
|
+
...(kind === "choose" ? { options: validateChoices(stage["options"], title) } : {}),
|
|
331
|
+
...(kind === "task" && stage["repo"] !== undefined && stage["repo"] !== null && stage["repo"] !== "" ? { repo: validateRepo(stage["repo"], title) } : {}),
|
|
317
332
|
...(() => { const limit = validateLimit(stage["limit"], kind, title); return limit === null ? {} : { limit }; })(),
|
|
318
|
-
next: kind === "sort" ? null : text(stage["next"], 32),
|
|
333
|
+
next: kind === "sort" || kind === "choose" ? null : text(stage["next"], 32),
|
|
319
334
|
onFail: text(stage["onFail"], 32),
|
|
320
335
|
};
|
|
321
336
|
});
|
|
@@ -326,9 +341,13 @@ export function validateFlowDefinition(input, options = {}) {
|
|
|
326
341
|
ids.add(stage.id);
|
|
327
342
|
}
|
|
328
343
|
for (const stage of stages) {
|
|
329
|
-
for (const target of [stage.next, stage.onFail, stage.limit?.to ?? null, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? [])])
|
|
344
|
+
for (const target of [stage.next, stage.onFail, stage.limit?.to ?? null, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? []), ...choiceTargets(stage)])
|
|
330
345
|
if (target !== null && !ids.has(target))
|
|
331
346
|
throw new Error(`Zone ${stage.title} points at a zone that no longer exists.`);
|
|
347
|
+
if (stage.options !== undefined && ids.has(FLOW_END) && stage.options.some(one => one.to === FLOW_END))
|
|
348
|
+
throw new Error(`Zone ${stage.title}: an option that ends the card can't be told apart from the zone called ${FLOW_END}. Rename that zone.`);
|
|
349
|
+
if (stage.options?.some(one => one.to === stage.id))
|
|
350
|
+
throw new Error(`Zone ${stage.title}: an option can't send cards back into the same zone.`);
|
|
332
351
|
if (stage.limit?.to === stage.id)
|
|
333
352
|
throw new Error(`Zone ${stage.title}: a card that waits too long can't move back into the same zone.`);
|
|
334
353
|
if (stage.routes?.some(one => one.to === stage.id))
|
|
@@ -380,10 +399,43 @@ export function reachableWithout(stages, start, stop) {
|
|
|
380
399
|
if (stage === undefined || seen.has(stage.id) || stop(stage))
|
|
381
400
|
continue;
|
|
382
401
|
seen.add(stage.id);
|
|
383
|
-
queue.push(...[stage.next, stage.onFail, stage.limit?.to ?? null, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? [])].filter((one) => one !== null));
|
|
402
|
+
queue.push(...[stage.next, stage.onFail, stage.limit?.to ?? null, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? []), ...choiceTargets(stage)].filter((one) => one !== null));
|
|
384
403
|
}
|
|
385
404
|
return seen;
|
|
386
405
|
}
|
|
406
|
+
/** The zones a choose zone's options lead to (an option that ends the card leads to none). */
|
|
407
|
+
export function choiceTargets(stage) {
|
|
408
|
+
return (stage.options ?? []).map(one => one.to).filter(to => to !== FLOW_END);
|
|
409
|
+
}
|
|
410
|
+
/** Where a reply to a choice goes, as its {{note}}: the zone's reply path, else its first option that leads somewhere; null when none does. */
|
|
411
|
+
export function replyTarget(stage) {
|
|
412
|
+
return stage.onFail ?? choiceTargets(stage)[0] ?? null;
|
|
413
|
+
}
|
|
414
|
+
/** A choose zone's options, checked: 2 to 4, each with short words of its own and where it leads. */
|
|
415
|
+
function validateChoices(input, title) {
|
|
416
|
+
const raw = Array.isArray(input) ? input : [];
|
|
417
|
+
if (raw.length < CHOICES_MIN || raw.length > CHOICES_MAX)
|
|
418
|
+
throw new Error(`Zone ${title}: give it ${CHOICES_MIN} to ${CHOICES_MAX} options.`);
|
|
419
|
+
const seen = new Set();
|
|
420
|
+
return raw.map(one => {
|
|
421
|
+
const row = (one ?? {});
|
|
422
|
+
const label = text(row["label"], 40);
|
|
423
|
+
const to = text(row["to"], 32);
|
|
424
|
+
if (label === null || to === null)
|
|
425
|
+
throw new Error(`Zone ${title}: each option needs a few words and where it leads.`);
|
|
426
|
+
if (seen.has(label.toLowerCase()))
|
|
427
|
+
throw new Error(`Zone ${title}: two options are called ${label}.`);
|
|
428
|
+
seen.add(label.toLowerCase());
|
|
429
|
+
return { label, to };
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/** A build zone's project, when it isn't the flow's: its path. Whether the flow's owner may file there is checked when work is filed. */
|
|
433
|
+
function validateRepo(value, title) {
|
|
434
|
+
const repo = text(value, 1000);
|
|
435
|
+
if (repo === null)
|
|
436
|
+
throw new Error(`Zone ${title}: choose the project it builds in.`);
|
|
437
|
+
return repo;
|
|
438
|
+
}
|
|
387
439
|
/** A teammate's handle on a zone (v92): which teammate works it. Whether it exists is the project's to say, when a card arrives. */
|
|
388
440
|
function validateTeammate(value, title) {
|
|
389
441
|
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(value))
|
|
@@ -440,7 +492,8 @@ export function flowDigest(definition) {
|
|
|
440
492
|
const terms = definition.stages.map(({ zone: _zone, ...rest }) => rest);
|
|
441
493
|
return createHash("sha256").update(JSON.stringify({ start: definition.start, terms })).digest("hex").slice(0, 32);
|
|
442
494
|
}
|
|
443
|
-
|
|
495
|
+
/** {{stage.<id>}} is an earlier zone's output; a report zone also fills {{stage.<id>.items}} and {{stage.<id>.report}}. */
|
|
496
|
+
const FILLED = /\{\{\s*(card\.title|card\.description|card\.email|note|stage\.([a-z0-9-]+(?:\.(?:items|report))?))\s*\}\}/g;
|
|
444
497
|
/** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
|
|
445
498
|
export function fillFlowText(template, card, encode = value => value) {
|
|
446
499
|
return template.replace(FILLED, (_match, key, stage) => encode(fillOne(key, stage, card))).trim();
|
|
@@ -524,7 +577,7 @@ function fillOne(key, stage, card) {
|
|
|
524
577
|
return stage === undefined ? "" : card.outputs[stage] ?? "";
|
|
525
578
|
}
|
|
526
579
|
}
|
|
527
|
-
const KIND_COLORS = { inbox: "slate", task: "blue", report: "violet", approval: "amber", check: "blue", "pull-request": "blue", update: "green", notify: "green", sort: "violet", draft: "violet", request: "blue", email: "green", tool: "blue", wait: "slate", teammate: "violet", done: "green" };
|
|
580
|
+
const KIND_COLORS = { inbox: "slate", task: "blue", report: "violet", approval: "amber", check: "blue", "pull-request": "blue", update: "green", notify: "green", sort: "violet", draft: "violet", request: "blue", email: "green", tool: "blue", wait: "slate", teammate: "violet", send: "green", choose: "amber", done: "green" };
|
|
528
581
|
/** A step id as the lead may write it (sort_by_hand, Sort-By-Hand) in the one form zones use. */
|
|
529
582
|
const idOf = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
530
583
|
const slugOf = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 28) || "zone";
|
|
@@ -617,18 +670,21 @@ export function flowFromSteps(input, previous = null) {
|
|
|
617
670
|
drafts.forEach(({ step, old, id, title, kind }, index) => {
|
|
618
671
|
const earlier = stages.slice();
|
|
619
672
|
const following = drafts.slice(index + 1).find(() => true) ?? null;
|
|
620
|
-
const next = kind === "done" || kind === "sort" ? null
|
|
673
|
+
const next = kind === "done" || kind === "sort" || kind === "choose" ? null
|
|
621
674
|
: typeof step.next === "string" && step.next.trim() !== "" ? find(step.next, title)
|
|
622
675
|
: following?.id ?? drafts.find(one => one.kind === "done").id;
|
|
623
676
|
const keptFail = old?.onFail !== null && old?.onFail !== undefined && drafts.some(one => one.id === old.onFail) ? old.onFail : null;
|
|
624
677
|
const worker = [...earlier].reverse().find(one => one.kind === "task" || one.kind === "report");
|
|
625
678
|
const notSure = kind === "sort" && typeof step.ifNotSure === "string" && step.ifNotSure.trim() !== "" ? step.ifNotSure
|
|
626
|
-
: kind === "wait" && typeof step.ifNoReply === "string" && step.ifNoReply.trim() !== "" ? step.ifNoReply
|
|
679
|
+
: kind === "wait" && typeof step.ifNoReply === "string" && step.ifNoReply.trim() !== "" ? step.ifNoReply
|
|
680
|
+
: kind === "choose" ? step.ifReplied : step.ifFails;
|
|
627
681
|
const onFail = kind === "done" ? null
|
|
628
682
|
: typeof notSure === "string" && notSure.trim() !== "" ? find(notSure, title)
|
|
629
683
|
: keptFail ?? (kind === "approval" ? worker?.id ?? (drafts[0].id === id ? null : drafts[0].id)
|
|
630
|
-
//
|
|
631
|
-
: kind === "
|
|
684
|
+
// A reply to a choice says what to change: it goes back to the work before it, as its note.
|
|
685
|
+
: kind === "choose" ? worker?.id ?? null
|
|
686
|
+
// Red CI goes back to the build before it, carrying the failing check.
|
|
687
|
+
: kind === "pull-request" ? [...earlier].reverse().find(one => one.kind === "task")?.id ?? null : null);
|
|
632
688
|
// "owner": whoever owns the flow when the card arrives.
|
|
633
689
|
const toOwner = kind === "approval" && (step.decider === undefined ? old?.toOwner === true : typeof step.decider === "string" && /^(owner|the owner|flow owner|the flow owner|the flow's owner)$/i.test(step.decider.trim()));
|
|
634
690
|
const approver = kind !== "approval" || toOwner ? null
|
|
@@ -636,7 +692,7 @@ export function flowFromSteps(input, previous = null) {
|
|
|
636
692
|
: step.decider === null || /^(anyone|any approver|anybody)$/i.test(step.decider.trim()) ? null : step.decider.trim();
|
|
637
693
|
stages.push({
|
|
638
694
|
id, title, kind,
|
|
639
|
-
zone: old?.zone ?? { x: 0, y: 0, w: 260, h: kind === "done" || kind === "notify" ? 220 : 300, color: KIND_COLORS[kind] },
|
|
695
|
+
zone: old?.zone ?? { x: 0, y: 0, w: 260, h: kind === "done" || kind === "notify" || kind === "send" ? 220 : 300, color: KIND_COLORS[kind] },
|
|
640
696
|
// A kept step on our default instructions gets the default again, so it reads any research step added before it.
|
|
641
697
|
instructions: kind === "teammate" ? step.instructions?.trim() || old?.instructions || "Read the card and decide what happens next."
|
|
642
698
|
: kind === "draft" ? step.instructions?.trim() || old?.instructions || DRAFT_DEFAULT
|
|
@@ -666,15 +722,24 @@ export function flowFromSteps(input, previous = null) {
|
|
|
666
722
|
...((kind === "approval" || kind === "teammate") && (step.teammate === undefined ? old?.teammate !== undefined : !/^(nobody|none|no one)$/i.test(step.teammate.trim())) ? { teammate: idOf(step.teammate ?? old.teammate) } : {}),
|
|
667
723
|
...(kind === "teammate" ? step.routes !== undefined ? step.routes.length === 0 ? {} : { routes: step.routes.map(one => ({ answer: String(one.answer ?? "").trim(), to: find(String(one.goesTo ?? ""), title) })) } : old?.routes === undefined ? {} : { routes: old.routes } : {}),
|
|
668
724
|
...(kind === "teammate" && (step.reply ?? old?.reply) === true ? { reply: true } : {}),
|
|
669
|
-
...(
|
|
725
|
+
...(kind === "choose" ? { options: choicesFromStep(step, old?.options ?? null, title, ref => find(ref, title)) } : {}),
|
|
726
|
+
...(kind === "task" && (step.repo ?? old?.repo) !== undefined && (step.repo ?? old?.repo).trim() !== "" ? { repo: (step.repo ?? old?.repo).trim() } : {}),
|
|
727
|
+
...(() => {
|
|
728
|
+
// "If no reply" on a choice is where it moves once the reminder comes.
|
|
729
|
+
const asked = kind === "choose" && step.thenMoveTo === undefined && typeof step.ifNoReply === "string" ? { ...step, thenMoveTo: step.ifNoReply } : step;
|
|
730
|
+
if (kind === "choose" && asked.thenMoveTo !== undefined && asked.remindAfter === undefined && old?.limit === undefined)
|
|
731
|
+
throw new Error(`Step ${title}: say how long to wait for a choice first, with remindAfter (like "2 days").`);
|
|
732
|
+
const limit = kind === "wait" || kind === "done" ? null : limitFromStep(asked, old?.limit ?? null, title, ref => find(ref, title));
|
|
733
|
+
return limit === null ? {} : { limit };
|
|
734
|
+
})(),
|
|
670
735
|
next, onFail,
|
|
671
736
|
});
|
|
672
737
|
});
|
|
673
738
|
// {{stage.<ref>}} in a step's words names a step as the lead wrote it (draftReply, Draft reply):
|
|
674
739
|
// it is rewritten to that step's id, the same way the steps themselves are named.
|
|
675
|
-
const refs = (text) => text.replace(/\{\{\s*stage\.([A-Za-z0-9_ -]{1,60}?)
|
|
740
|
+
const refs = (text) => text.replace(/\{\{\s*stage\.([A-Za-z0-9_ -]{1,60}?)(\.items|\.report)?\s*\}\}/g, (whole, ref, part) => {
|
|
676
741
|
const hit = drafts.find(one => one.id === ref) ?? drafts.find(one => one.id === idOf(ref) || one.id.replace(/-/g, "") === idOf(ref).replace(/-/g, "") || one.title.toLowerCase() === ref.trim().toLowerCase());
|
|
677
|
-
return hit === undefined ? whole : `{{stage.${hit.id}}}`;
|
|
742
|
+
return hit === undefined ? whole : `{{stage.${hit.id}${part ?? ""}}}`;
|
|
678
743
|
});
|
|
679
744
|
for (const stage of stages) {
|
|
680
745
|
if (stage.instructions !== null)
|
|
@@ -692,14 +757,14 @@ export function flowFromSteps(input, previous = null) {
|
|
|
692
757
|
// A new flow that branches (a sort, or a script's answers) is laid out in columns by step instead,
|
|
693
758
|
// each branch below the one before, so no arrow crosses a zone.
|
|
694
759
|
const placed = stages.filter(one => kept.get(one.id)?.zone === one.zone).map(one => one.zone);
|
|
695
|
-
const branches = previous === null && stages.some(one => (one.sort?.answers.length ?? 0) > 0 || (one.routes?.length ?? 0) > 0);
|
|
760
|
+
const branches = previous === null && stages.some(one => (one.sort?.answers.length ?? 0) > 0 || (one.routes?.length ?? 0) > 0 || choiceTargets(one).length > 0);
|
|
696
761
|
const columns = new Map();
|
|
697
762
|
if (branches) {
|
|
698
763
|
const queue = [stages[0].id];
|
|
699
764
|
columns.set(stages[0].id, 0);
|
|
700
765
|
while (queue.length > 0) {
|
|
701
766
|
const id = queue.shift(), stage = stages.find(one => one.id === id);
|
|
702
|
-
for (const to of [...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? []), stage.next, stage.onFail]) {
|
|
767
|
+
for (const to of [...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? []), ...choiceTargets(stage), stage.next, stage.onFail]) {
|
|
703
768
|
if (to === null || to === "" || columns.has(to))
|
|
704
769
|
continue;
|
|
705
770
|
columns.set(to, columns.get(id) + 1);
|
|
@@ -735,6 +800,19 @@ export function flowFromSteps(input, previous = null) {
|
|
|
735
800
|
});
|
|
736
801
|
return validateFlowDefinition({ version: 1, start: stages[0].id, stages });
|
|
737
802
|
}
|
|
803
|
+
/** A choose step's options: each with its words and the step it goes to ("end", or no step, ignores the card); kept ones carry over. */
|
|
804
|
+
function choicesFromStep(step, old, title, find) {
|
|
805
|
+
if (!Array.isArray(step.options) || step.options.length === 0) {
|
|
806
|
+
if (old !== null)
|
|
807
|
+
return old;
|
|
808
|
+
throw new Error(`Step ${title}: give it ${CHOICES_MIN} to ${CHOICES_MAX} options, each with a label and the step it goes to (or "end").`);
|
|
809
|
+
}
|
|
810
|
+
return step.options.map(one => {
|
|
811
|
+
const label = String(one?.label ?? "").trim();
|
|
812
|
+
const goes = typeof one?.goesTo === "string" ? one.goesTo.trim() : "";
|
|
813
|
+
return { label, to: goes === "" || /^(end|ignore|ignored|stop|close)$/i.test(goes) ? FLOW_END : find(goes) };
|
|
814
|
+
});
|
|
815
|
+
}
|
|
738
816
|
/** A sort step as the lead describes it: answers name the steps they go to; what it leaves out carries over from the zone it keeps. */
|
|
739
817
|
function sortFromStep(step, old, find) {
|
|
740
818
|
const answers = Array.isArray(step.answers) && step.answers.length > 0
|
|
@@ -767,6 +845,8 @@ export function flowTerms(definition, previous) {
|
|
|
767
845
|
}
|
|
768
846
|
if (stage.kind === "task" && stage.planning !== "auto")
|
|
769
847
|
lines.push(stage.planning === "required" ? "Plans first." : "Builds without a plan.");
|
|
848
|
+
if (stage.kind === "task" && stage.repo !== undefined)
|
|
849
|
+
lines.push(`Builds in another project: ${stage.repo}`);
|
|
770
850
|
if (stage.kind === "draft")
|
|
771
851
|
lines.push(`Claude writes: ${plain(stage.instructions ?? "")}`, `Then → ${to(stage.next)}`);
|
|
772
852
|
else if (stage.kind === "request" && stage.request !== undefined) {
|
|
@@ -783,6 +863,10 @@ export function flowTerms(definition, previous) {
|
|
|
783
863
|
lines.push(`The AI teammate ${stage.teammate ?? "(none)"} handles it${stage.instructions === null ? "" : `: ${plain(stage.instructions)}`}`, ...(stage.routes === undefined || stage.routes.length === 0 ? [`Then → ${to(stage.next)}`] : stage.routes.map(one => `${one.answer} → ${to(one.to)}`)), `It asks the flow's owner when its rules say to.${stage.reply === true ? " It sends what it writes back to whoever asked." : ""}${stage.onFail === null ? "" : ` If it can't → ${to(stage.onFail)}`}`);
|
|
784
864
|
else if (stage.kind === "notify")
|
|
785
865
|
lines.push(`Posts: ${plain(stage.message ?? "")}`, `Then → ${to(stage.next)}`);
|
|
866
|
+
else if (stage.kind === "send")
|
|
867
|
+
lines.push("Sends the card's owner (or the flow's) what the step before produced: its summary, links and any screenshots, in each chat app they use.", `Then → ${to(stage.next)}`);
|
|
868
|
+
else if (stage.kind === "choose")
|
|
869
|
+
lines.push("Sends the card's owner (or the flow's) what the step before produced, and asks them to choose:", ...(stage.options ?? []).map(one => `${one.label} → ${one.to === FLOW_END ? "ignores the card." : to(one.to)}`), `Or a reply with what they'd change → ${replyTarget(stage) === null ? "not possible." : to(replyTarget(stage))}`);
|
|
786
870
|
else if (stage.kind === "update")
|
|
787
871
|
lines.push(`Comments on the issue the card came from: ${plain(stage.message ?? "")}${stage.close === true ? " Then closes it (Linear: moves it to done)." : ""}`, `Then → ${to(stage.next)}${stage.onFail === null ? "" : ` If it can't → ${to(stage.onFail)}`}`);
|
|
788
872
|
else if (stage.kind === "sort" && stage.sort !== null) {
|
|
@@ -835,6 +919,8 @@ export function flowTerms(definition, previous) {
|
|
|
835
919
|
terms.push(`Pull request steps push the card's branch to GitHub and open a pull request under this project's pull request setup.${definition.stages.some(one => one.merge !== undefined) ? " A merge happens only after a person approves the card; nothing merges without one." : " Nothing merges on its own."}`);
|
|
836
920
|
if (definition.stages.some(one => one.kind === "draft"))
|
|
837
921
|
terms.push("Draft steps send each card's text to Claude through the lead chat's sign-in. Nothing a draft writes is sent until a later step sends it.");
|
|
922
|
+
if (definition.stages.some(one => one.kind === "task" && one.repo !== undefined))
|
|
923
|
+
terms.push("A build step in another project files its task there, only when the flow's owner may file work in that project.");
|
|
838
924
|
if (definition.stages.some(one => one.teammate !== undefined))
|
|
839
925
|
terms.push("AI teammates decide and act within their soul files' rules, reading each card through Claude on this computer's sign-in; they never approve code tasks or merges.");
|
|
840
926
|
return terms;
|
package/dist/mate-tools.js
CHANGED
|
@@ -46,6 +46,7 @@ import { agentsSummary, chosenWords, isRiskLevel, PHASES, postureWords, RISK_CHO
|
|
|
46
46
|
import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommandLine, toolStanding } from "./project-tools.js";
|
|
47
47
|
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps, stepsFor } from "./flows.js";
|
|
48
48
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
49
|
+
import { flowPersonOf } from "./flow-send.js";
|
|
49
50
|
import { flowInsights } from "./flow-insights.js";
|
|
50
51
|
import { describeTrigger, FLOW_TRIGGER_KINDS, triggerConfigOf } from "./flow-triggers.js";
|
|
51
52
|
export const MATE_MAX_PROPOSALS_PER_TURN = 5;
|
|
@@ -796,8 +797,11 @@ export const MATE_TOOLS = [
|
|
|
796
797
|
inputSchema: schema({ repo: REPO_ARG, flow: { type: "integer", minimum: 1 }, card: { type: "integer", minimum: 1 } }),
|
|
797
798
|
handle: (ctx, args) => {
|
|
798
799
|
const reachable = (repo) => ctx.who.repos.includes(repo) && ctx.store.accountCanAccess(ctx.who.name, repo);
|
|
799
|
-
const needsYou = (flow, definition,
|
|
800
|
-
const at = definition?.stages.find(one => one.id === stage);
|
|
800
|
+
const needsYou = (flow, definition, card) => {
|
|
801
|
+
const at = definition?.stages.find(one => one.id === card.stage);
|
|
802
|
+
// A "Person chooses" zone waits for the card's person; a "Person decides" one for its decider.
|
|
803
|
+
if (at?.kind === "choose")
|
|
804
|
+
return flowPersonOf(card, flow) === ctx.who.name;
|
|
801
805
|
if (at?.kind !== "approval")
|
|
802
806
|
return false;
|
|
803
807
|
const decider = deciderOf(at, flow);
|
|
@@ -832,15 +836,19 @@ export const MATE_TOOLS = [
|
|
|
832
836
|
...(stage.merge === undefined ? {} : { merge: stage.merge }),
|
|
833
837
|
...(stage.teammate === undefined ? {} : { teammate: stage.teammate }),
|
|
834
838
|
...(stage.kind === "teammate" && stage.routes !== undefined ? { routes: stage.routes.map(one => ({ answer: one.answer, goesTo: titleOf(one.to) })) } : {}),
|
|
839
|
+
...(stage.kind === "choose" ? { options: (stage.options ?? []).map((one, index) => ({ choice: index + 1, label: one.label, goesTo: one.to === "end" ? "end" : titleOf(one.to) })) } : {}),
|
|
840
|
+
...(stage.kind === "task" && stage.repo !== undefined ? { repo: repoIdOf(ctx.who, stage.repo) ?? "another project" } : {}),
|
|
835
841
|
...(stage.limit === undefined ? {} : { remindAfter: durationWords(stage.limit.minutes), ...(stage.limit.to === null ? {} : { thenMoveTo: titleOf(stage.limit.to) }) }),
|
|
836
|
-
next: titleOf(stage.next), ...(stage.kind === "sort" ? { ifNotSure: titleOf(stage.onFail) } : stage.kind === "wait" ? { ifNoReply: titleOf(stage.onFail) } : { ifFails: titleOf(stage.onFail) }),
|
|
842
|
+
next: titleOf(stage.next), ...(stage.kind === "sort" ? { ifNotSure: titleOf(stage.onFail) } : stage.kind === "wait" ? { ifNoReply: titleOf(stage.onFail) } : stage.kind === "choose" ? { ifReplied: titleOf(stage.onFail) } : { ifFails: titleOf(stage.onFail) }),
|
|
837
843
|
})),
|
|
838
844
|
cards: [...active, ...finished].map(card => ({
|
|
839
845
|
card: card.id, title: card.title, ...(card.description === null ? {} : { description: card.description.slice(0, 300) }),
|
|
840
|
-
at: titleOf(card.stage) ?? card.stage, state: card.state, needsYou: card.state === "active" && needsYou(flow, definition, card
|
|
846
|
+
at: titleOf(card.stage) ?? card.stage, state: card.state, needsYou: card.state === "active" && needsYou(flow, definition, card),
|
|
841
847
|
// Names never reach the model; a decision says whose it is in its own terms.
|
|
842
|
-
waiting: card.state
|
|
843
|
-
|
|
848
|
+
waiting: card.state === "active" && definition.stages.find(one => one.id === card.stage)?.kind === "choose" && card.waiting !== null
|
|
849
|
+
? (flowPersonOf(card, flow) === ctx.who.name ? "Waiting for you to choose (operation choose: choice by number, or note to reply)" : "Waiting for someone else to choose")
|
|
850
|
+
: card.state !== "active" || definition.stages.find(one => one.id === card.stage)?.kind !== "approval" || card.waiting === null ? card.waiting
|
|
851
|
+
: needsYou(flow, definition, card) ? "Waiting for you to approve or send it back" : "Waiting for someone else to decide",
|
|
844
852
|
task: card.task ?? card.primaryTask, ...(card.note === null ? {} : { lastNote: card.note.slice(0, 300) }),
|
|
845
853
|
// Names never reach the model: people read as you or a teammate.
|
|
846
854
|
owner: card.owner === null ? null : card.owner === ctx.who.name ? "you" : "a teammate", following: ctx.store.flowCardWatchers(card.id).includes(ctx.who.name),
|
|
@@ -867,7 +875,7 @@ export const MATE_TOOLS = [
|
|
|
867
875
|
const definition = flowDefinitionOf(flow);
|
|
868
876
|
const cards = ctx.store.flowCards(flow.id, false);
|
|
869
877
|
return { flow: flow.id, name: flow.name, project: repoIdOf(ctx.who, flow.repo), steps: definition === null ? "can't be read" : definition.stages.map(one => one.title).join(" → "),
|
|
870
|
-
cards: cards.length, needYou: cards.filter(card => needsYou(flow, definition, card
|
|
878
|
+
cards: cards.length, needYou: cards.filter(card => needsYou(flow, definition, card)).length, triggers: ctx.store.flowTriggers(flow.id).length };
|
|
871
879
|
}),
|
|
872
880
|
templates: FLOW_TEMPLATES.map(one => ({ template: one.id, about: one.about })),
|
|
873
881
|
// Scripts belong to a project, not a flow: they exist (and can be saved) before any flow does.
|
|
@@ -878,9 +886,9 @@ export const MATE_TOOLS = [
|
|
|
878
886
|
},
|
|
879
887
|
{
|
|
880
888
|
name: "propose_flow",
|
|
881
|
-
description: "Draft a flow change as a card the operator confirms. To make a flow, create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about; comment (with @name to ping someone), assign and follow here too. For triage, spam, lead, effort or exception routing, start from the matching template, with the sort as the first step. Use remindAfter and thenMoveTo for follow-ups and decisions that stall. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight, review what went wrong every morning), offer the matching starter; each is one yes, and none merges without a person. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a kit sets up the teammate and its flow in one card. Teammates: propose_teammate adds one (from a template: support, sales, ops, triage), changes one rule section (edit_section), pauses, resumes or removes it, passes it a note, or answers its question for the operator; put it to work with a flow here. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates use the project's tools under a rule per action (do it, ask first: the operator approves each exact call, never, or up to a limit on a number); propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. forget or edit_memory when the operator says a teammate has something wrong; a rule change it suggests is a question for the operator, answered only as they say. propose_teammate add_routine puts a card on its desk on a schedule; propose_teammate undo undoes a tool call only when its action has an undo set (tool_rule undoWith). Actions: starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning, plane-review reviews the plane's last 24 hours every morning and turns each problem worth fixing into a researched fix and a pull request) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a teammate, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). AI teammates (get_teammates): an approval step with teammate (its short name) is decided by that teammate within its rules, and it hands hard ones to the step's decider; a 'teammate' step (teammate, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain); plane-review: at (HH:MM, default 07:30), timeZone — every day it reads the plane's last 24 hours and makes one card per problem worth fixing, a returning problem joining its card); pause_trigger, resume_trigger, remove_trigger with trigger. Read get_flows first except to create.",
|
|
889
|
+
description: "Draft a flow change as a card the operator confirms. To make a flow, create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about; comment (with @name to ping someone), assign and follow here too. For triage, spam, lead, effort or exception routing, start from the matching template, with the sort as the first step. Use remindAfter and thenMoveTo for follow-ups and decisions that stall. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight, review what went wrong every morning), offer the matching starter; each is one yes, and none merges without a person. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a kit sets up the teammate and its flow in one card. Teammates: propose_teammate adds one (from a template: support, sales, ops, triage), changes one rule section (edit_section), pauses, resumes or removes it, passes it a note, or answers its question for the operator; put it to work with a flow here. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates use the project's tools under a rule per action (do it, ask first: the operator approves each exact call, never, or up to a limit on a number); propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. forget or edit_memory when the operator says a teammate has something wrong; a rule change it suggests is a question for the operator, answered only as they say. propose_teammate add_routine puts a card on its desk on a schedule; propose_teammate undo undoes a tool call only when its action has an undo set (tool_rule undoWith). Actions: starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning, plane-review reviews the plane's last 24 hours every morning and turns each problem worth fixing into a researched fix and a pull request) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a teammate, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). AI teammates (get_teammates): an approval step with teammate (its short name) is decided by that teammate within its rules, and it hands hard ones to the step's decider; a 'teammate' step (teammate, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain); plane-review: at (HH:MM, default 07:30), timeZone — every day it reads the plane's last 24 hours and makes one card per problem worth fixing, a returning problem joining its card); pause_trigger, resume_trigger, remove_trigger with trigger. A 'send' step ('Send to me') sends the card's owner what the step before produced (summary, links, screenshots) in their chat apps, then moves on; a 'choose' step sends the same with 2 to 4 options (label, goesTo: a step, or 'end' to ignore the card) as buttons, ifReplied (where a reply goes as {{note}}; the build before by default), and remindAfter with ifNoReply. choose (card, and choice by number or a note to reply) answers one for the operator. A 'task' step may build in another project: repo (from list_repos). Read get_flows first except to create.",
|
|
882
890
|
inputSchema: schema({
|
|
883
|
-
operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit", "starter"] },
|
|
891
|
+
operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "choose", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit", "starter"] },
|
|
884
892
|
repo: REPO_ARG, flow: { type: "integer", minimum: 1 }, card: { type: "integer", minimum: 1 }, kit: { type: "string", enum: KITS.map(one => one.id) },
|
|
885
893
|
starter: { type: "string", enum: [...STARTER_IDS] },
|
|
886
894
|
name: { type: "string", maxLength: 80 }, template: { type: "string", enum: FLOW_TEMPLATES.map(one => one.id) },
|
|
@@ -901,9 +909,12 @@ export const MATE_TOOLS = [
|
|
|
901
909
|
waitFor: { type: "string", enum: ["reply", "time", "hours"] }, wait: { type: "string", maxLength: 40 }, ifNoReply: { type: "string", maxLength: 60 },
|
|
902
910
|
from: { type: "string", maxLength: 5 }, until: { type: "string", maxLength: 5 }, merge: { type: "string", enum: ["squash", "merge", "rebase"] },
|
|
903
911
|
remindAfter: { type: "string", maxLength: 40 }, thenMoveTo: { type: "string", maxLength: 60 }, teammate: { type: "string", maxLength: 40 },
|
|
912
|
+
options: { type: "array", minItems: 2, maxItems: 4, items: { type: "object", additionalProperties: false, properties: { label: { type: "string", maxLength: 40 }, goesTo: { type: "string", maxLength: 60 } } } },
|
|
913
|
+
ifReplied: { type: "string", maxLength: 60 }, repo: REPO_ARG,
|
|
904
914
|
next: { type: "string", maxLength: 60 }, ifFails: { type: "string", maxLength: 60 }, ifNotSure: { type: "string", maxLength: 60 },
|
|
905
915
|
} } },
|
|
906
916
|
title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength: 2000 },
|
|
917
|
+
choice: { type: "integer", minimum: 1, maximum: 4 },
|
|
907
918
|
trigger: { type: "integer", minimum: 1 }, owner: { type: "string", maxLength: 64 },
|
|
908
919
|
script: { type: "object", additionalProperties: false, properties: { name: { type: "string", maxLength: 40 }, about: { type: "string", maxLength: 160 }, body: { type: "string", maxLength: 1600 }, timeoutMinutes: { type: "integer", minimum: 1, maximum: 60 },
|
|
909
920
|
language: { type: "string", enum: ["python", "node", "shell"] }, file: { type: "string", maxLength: 200 } } },
|
|
@@ -921,7 +932,15 @@ export const MATE_TOOLS = [
|
|
|
921
932
|
handle: (ctx, args) => {
|
|
922
933
|
const pick = (keys) => Object.fromEntries(keys.filter(key => args[key] !== undefined).map(key => [key, args[key]]));
|
|
923
934
|
// "me" in a step means the operator; the drawing stores their name.
|
|
924
|
-
|
|
935
|
+
// A build step in another project names it as list_repos does (r2); the drawing keeps its path.
|
|
936
|
+
const steps = () => stepsFor(Array.isArray(args["steps"]) ? args["steps"] : [], ctx.who.name).map(step => {
|
|
937
|
+
if (step === null || typeof step !== "object" || step.repo === undefined)
|
|
938
|
+
return step;
|
|
939
|
+
const path = repoPathOf(ctx.who, step.repo);
|
|
940
|
+
if (path === null)
|
|
941
|
+
throw new Error("A build step's repo is a project from list_repos.");
|
|
942
|
+
return { ...step, repo: path };
|
|
943
|
+
});
|
|
925
944
|
const flowOf = () => {
|
|
926
945
|
const flow = Number.isSafeInteger(args["flow"]) ? ctx.store.getFlow(Number(args["flow"])) : null;
|
|
927
946
|
return flow !== null && ctx.who.repos.includes(flow.repo) ? flow : null;
|
|
@@ -988,6 +1007,10 @@ export const MATE_TOOLS = [
|
|
|
988
1007
|
operation = "flow_card_send_back";
|
|
989
1008
|
input = pick(["card", "note"]);
|
|
990
1009
|
break;
|
|
1010
|
+
case "choose":
|
|
1011
|
+
operation = "flow_card_choose";
|
|
1012
|
+
input = pick(["card", "choice", "note"]);
|
|
1013
|
+
break;
|
|
991
1014
|
case "cancel_card":
|
|
992
1015
|
operation = "flow_card_cancel";
|
|
993
1016
|
input = pick(["card"]);
|