toolroll 0.9.19 → 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 +24 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -21
- package/dist/browser-workspace.d.ts +43 -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/flow-triggers.d.ts +7 -0
- package/dist/flow-triggers.js +17 -5
- package/dist/flows-cli.js +11 -3
- package/dist/flows-ui.js +22 -6
- package/dist/flows.d.ts +27 -1
- package/dist/flows.js +104 -18
- package/dist/invoke.d.ts +3 -0
- package/dist/invoke.js +1 -0
- package/dist/mate-tools.js +33 -10
- package/dist/operate.js +41 -0
- package/dist/plane-review.d.ts +15 -0
- package/dist/plane-review.js +62 -20
- 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 +14 -0
- package/dist/provider.js +8 -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 +160 -0
- package/dist/scout-report.js +171 -1
- package/dist/scout.d.ts +42 -0
- package/dist/scout.js +380 -69
- 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-cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { envelopeJson } from './envelope.js';
|
|
13
|
-
import { addCardToFlow, advanceFlows, flowDefinitionOf } from './flow-engine.js';
|
|
13
|
+
import { addCardToFlow, advanceFlows, crossProjectProblem, flowDefinitionOf } from './flow-engine.js';
|
|
14
14
|
import { saveScript } from './flow-scripts.js';
|
|
15
15
|
import { exportFlow, fetchFlowFile, FLOW_FILE_MAX_BYTES, FlowFileError, importFlow, parseFlowFile, planFlowImport } from './flow-share.js';
|
|
16
16
|
import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, switchOnStarter } from './flow-starters.js';
|
|
@@ -213,6 +213,9 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
213
213
|
const flowName = name || template?.label || '';
|
|
214
214
|
if (flowName === '')
|
|
215
215
|
return fail('usage', 'Name the flow with --name.', EXIT.usage);
|
|
216
|
+
const elsewhere = crossProjectProblem(store, definition, { repo: project, owner: who });
|
|
217
|
+
if (elsewhere !== null)
|
|
218
|
+
return refuse(project, 'invalid-steps', elsewhere);
|
|
216
219
|
const terms = flowTerms(definition, null);
|
|
217
220
|
if (template?.trigger !== undefined) {
|
|
218
221
|
const draft = { id: 0, repo: project, name: flowName, definitionJson: JSON.stringify(definition), revision: 1, state: 'active', createdBy: who, createdAt: now.toISOString(), updatedBy: who, updatedAt: now.toISOString(), owner: who };
|
|
@@ -377,6 +380,9 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
377
380
|
const redrawn = flowDigest(next) !== flowDigest(definition);
|
|
378
381
|
if (!redrawn && name === flow.name)
|
|
379
382
|
return fail('no-change', 'That\'s the flow as it is now.');
|
|
383
|
+
const elsewhere = crossProjectProblem(store, next, flow, who);
|
|
384
|
+
if (elsewhere !== null)
|
|
385
|
+
return refuse(flow.repo, 'invalid-steps', elsewhere);
|
|
380
386
|
if (!flags.has('yes'))
|
|
381
387
|
return preview(flow.repo, `Change the ${flow.name} flow`, [...(name === flow.name ? [] : [`Renames it to ${name}.`]), ...(redrawn ? flowTerms(next, definition) : [])], { flowId: flow.id, revision: flow.revision, definition: next });
|
|
382
388
|
if (!store.saveFlow(flow.id, { name, definitionJson: JSON.stringify(next), sawRevision: flow.revision, by: who }, now))
|
|
@@ -454,7 +460,7 @@ function listed(store, flow) {
|
|
|
454
460
|
const cards = store.flowCards(flow.id, false);
|
|
455
461
|
return { id: flow.id, name: flow.name, repo: flow.repo, project: projectName(flow.repo), zones: definition?.stages.map(one => one.title) ?? [],
|
|
456
462
|
triggers: store.flowTriggers(flow.id).filter(one => one.state === 'active').length, cardsWaiting: cards.length,
|
|
457
|
-
needDecision: cards.filter(card => definition?.stages.find(one => one.id === card.stage)?.kind
|
|
463
|
+
needDecision: cards.filter(card => ['approval', 'choose'].includes(definition?.stages.find(one => one.id === card.stage)?.kind ?? '')).length };
|
|
458
464
|
}
|
|
459
465
|
/** One zone and every path out of it, by zone id. */
|
|
460
466
|
function zoneOf(stage, flow) {
|
|
@@ -468,6 +474,8 @@ function zoneOf(stage, flow) {
|
|
|
468
474
|
...(stage.script === null ? {} : { script: stage.script }),
|
|
469
475
|
...(stage.merge === undefined ? {} : { merge: stage.merge }),
|
|
470
476
|
...(stage.teammate === undefined ? {} : { teammate: stage.teammate }),
|
|
477
|
+
...(stage.options === undefined ? {} : { options: stage.options.map(one => ({ label: one.label, to: one.to })) }),
|
|
478
|
+
...(stage.repo === undefined ? {} : { repo: stage.repo }),
|
|
471
479
|
};
|
|
472
480
|
}
|
|
473
481
|
function cardOf(definition, card) {
|
|
@@ -499,7 +507,7 @@ function showLines(flow) {
|
|
|
499
507
|
...('answers' in zone && zone.answers !== undefined ? zone.answers.map(one => ` ${one.answer} → ${title(one.to)}`) : []),
|
|
500
508
|
...('routes' in zone && zone.routes !== undefined ? zone.routes.map(one => ` ${one.answer} → ${title(one.to)}`) : []),
|
|
501
509
|
...(zone.next === null ? [] : [` next → ${title(zone.next)}`]),
|
|
502
|
-
...(zone.ifFails === null ? [] : [` ${zone.kind === 'approval' ? 'sent back' : zone.kind === 'sort' ? 'not sure' : zone.kind === 'wait' ? 'no reply' : 'if it fails'} → ${title(zone.ifFails)}`]),
|
|
510
|
+
...(zone.ifFails === null ? [] : [` ${zone.kind === 'approval' ? 'sent back' : zone.kind === 'sort' ? 'not sure' : zone.kind === 'wait' ? 'no reply' : zone.kind === 'choose' ? 'a reply' : 'if it fails'} → ${title(zone.ifFails)}`]),
|
|
503
511
|
]),
|
|
504
512
|
'', 'Triggers',
|
|
505
513
|
...(flow.triggers.length === 0 ? [' none — add one with toolroll flows trigger add'] : flow.triggers.map(one => ` #${one.id} ${one.state === 'active' ? '' : `(${one.state}) `}${one.words} → ${title(one.zone)}${one.lastOutcome === null ? '' : ` · last: ${one.lastOutcome}`}`)),
|
package/dist/flows-ui.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
2
|
-
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
2
|
+
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
3
3
|
import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
|
|
4
4
|
import { flowInsights, troubleWords } from "./flow-insights.js";
|
|
5
5
|
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 ?? "" } },
|
|
@@ -152,7 +167,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
152
167
|
return {
|
|
153
168
|
id: trigger.id, kind: trigger.kind, words: config === null ? "This trigger can't be read." : describeTrigger(config, store),
|
|
154
169
|
...(config === null ? { name: "Trigger", detail: "Can't be read" } : triggerHeadline(config, store)),
|
|
155
|
-
zone: title(config?.zone ?? definition?.start ?? ""), zoneId: config?.zone ?? definition?.start ?? "", state: trigger.state, status: trigger.lastOutcome, statusAt: trigger.lastAt, failing: trigger.failures > 0,
|
|
170
|
+
zone: title(config?.zone ?? definition?.start ?? ""), zoneId: config?.zone ?? definition?.start ?? "", state: trigger.state, status: trigger.lastOutcome, statusLink: planeStatusLink(config, trigger.lastOutcome), statusAt: trigger.lastAt, failing: trigger.failures > 0,
|
|
156
171
|
button: config?.kind === "button" ? { label: config.label, questions: config.questions } : null,
|
|
157
172
|
hook: config !== null && takesDeliveries(config) ? { ready: hookReady(trigger, setup.dir), needsSecret: config.kind === "linear" } : null,
|
|
158
173
|
checkable: ((config?.kind === "github" || config?.kind === "linear") && config.delivery === "poll") || config?.kind === "email" || (config?.kind === "schedule" && config.script !== undefined) || config?.kind === "plane-review",
|
|
@@ -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/invoke.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export type AgentOutcome = {
|
|
|
39
39
|
sessionId: string | null;
|
|
40
40
|
/** The agent's spoken conclusion — diagnostics, never the handoff. */
|
|
41
41
|
finalMessage: string | null;
|
|
42
|
+
/** Claude's `structured_output` alone (see ParsedEnvelope) — the only
|
|
43
|
+
* field a structured handback may be read from. */
|
|
44
|
+
structuredOutput?: string | null;
|
|
42
45
|
/** The harness's own account of the ending, when its dialect has one. */
|
|
43
46
|
ending?: AgentEnding | null;
|
|
44
47
|
usage: ProviderUsage;
|
package/dist/invoke.js
CHANGED
|
@@ -550,6 +550,7 @@ export async function invokeAgent(store, runId, spec, invocation, options) {
|
|
|
550
550
|
notFound: result.notFound,
|
|
551
551
|
sessionId: envelope.sessionId,
|
|
552
552
|
finalMessage: envelope.finalMessage,
|
|
553
|
+
structuredOutput: envelope.structuredOutput ?? null,
|
|
553
554
|
ending: envelope.ending ?? null,
|
|
554
555
|
usage: {
|
|
555
556
|
tokensIn: envelope.tokensIn,
|