toolroll 0.9.34 → 0.9.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/flow-share.d.ts
CHANGED
|
@@ -1,39 +1,11 @@
|
|
|
1
|
-
import { type ScriptDraft } from "./flow-scripts.js";
|
|
2
1
|
import { type FlowDefinition } from "./flows.js";
|
|
2
|
+
import { FLOW_FILE_FORMAT, FLOW_FILE_VERSION, type FlowFile, type FlowFileParameter, type FlowFileScript } from "./contracts/flow.js";
|
|
3
3
|
import type { FlowRow, Store } from "./store.js";
|
|
4
|
-
export
|
|
5
|
-
export
|
|
4
|
+
export { FLOW_FILE_FORMAT, FLOW_FILE_VERSION };
|
|
5
|
+
export type { FlowFile, FlowFileParameter, FlowFileScript };
|
|
6
6
|
/** The largest flow file Toolroll reads or fetches. */
|
|
7
7
|
export declare const FLOW_FILE_MAX_BYTES: number;
|
|
8
8
|
export declare const FLOW_FILE_SUFFIX = ".toolroll-flow.json";
|
|
9
|
-
/** Something the import asks for: `{{param.<id>}}` in a zone or trigger is replaced by its value. */
|
|
10
|
-
export type FlowFileParameter = {
|
|
11
|
-
id: string;
|
|
12
|
-
about: string;
|
|
13
|
-
default?: string;
|
|
14
|
-
optional?: boolean;
|
|
15
|
-
};
|
|
16
|
-
export type FlowFileScript = {
|
|
17
|
-
name: string;
|
|
18
|
-
about: string;
|
|
19
|
-
language: ScriptDraft["language"];
|
|
20
|
-
timeoutMinutes: number;
|
|
21
|
-
body?: string;
|
|
22
|
-
file?: string;
|
|
23
|
-
};
|
|
24
|
-
export type FlowFile = {
|
|
25
|
-
format: typeof FLOW_FILE_FORMAT;
|
|
26
|
-
version: typeof FLOW_FILE_VERSION;
|
|
27
|
-
name: string;
|
|
28
|
-
about: string;
|
|
29
|
-
needs: string[];
|
|
30
|
-
parameters: FlowFileParameter[];
|
|
31
|
-
/** The zones in the lead's step vocabulary, the start first. */
|
|
32
|
-
zones: Record<string, unknown>[];
|
|
33
|
-
/** Each trigger's settings, as `toolroll flows trigger add` takes them. */
|
|
34
|
-
triggers: Record<string, unknown>[];
|
|
35
|
-
scripts: FlowFileScript[];
|
|
36
|
-
};
|
|
37
9
|
export type FlowExported = {
|
|
38
10
|
file: FlowFile;
|
|
39
11
|
json: string;
|
|
@@ -42,9 +14,15 @@ export type FlowExported = {
|
|
|
42
14
|
};
|
|
43
15
|
/** A flow as a file: its zones and paths, its triggers' settings and the scripts it runs, nothing of this installation's. */
|
|
44
16
|
export declare function exportFlow(store: Store, flow: FlowRow, dir: string | null): FlowExported;
|
|
17
|
+
/** A flow file refused: why, in plain words, each problem naming its path in the file (`zones[0].routes[0]: ...`). */
|
|
45
18
|
export declare class FlowFileError extends Error {
|
|
46
19
|
}
|
|
47
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Read a flow file: its size, its JSON and format, then its one schema (flowFileSchema: every zone, trigger, script
|
|
22
|
+
* and parameter, strict about unknown keys — `zones[0].routes[0]: unknown key 'to' (did you mean goesTo?)`), and a
|
|
23
|
+
* newer version refused plainly. Then what the schema can't say: no keys in a trigger, ids and names used once, each
|
|
24
|
+
* {{param.x}} declared, every script valid and the zones a flow. Throws FlowFileError in plain words.
|
|
25
|
+
*/
|
|
48
26
|
export declare function parseFlowFile(text: string): FlowFile;
|
|
49
27
|
/** --param k=v lines (or a form's fields) as values; refuses parameters the file doesn't ask for. */
|
|
50
28
|
export declare function parameterValues(file: FlowFile, given: Record<string, string>): Record<string, string>;
|
package/dist/flow-share.js
CHANGED
|
@@ -21,29 +21,31 @@ import { flowDefinitionOf } from "./flow-engine.js";
|
|
|
21
21
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
22
22
|
import { scriptDigest, validateScript } from "./flow-scripts.js";
|
|
23
23
|
import { addFlowTriggerTo, describeTrigger, HOOK_PATH, readHooksBase, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
|
|
24
|
-
import { durationMinutes, durationWords, flowTerms, LANGUAGE_WORDS, validateFlowDefinition } from "./flows.js";
|
|
24
|
+
import { durationMinutes, durationWords, FlowContractError, flowTerms, inStepWords, LANGUAGE_WORDS, validateFlowDefinition } from "./flows.js";
|
|
25
|
+
import { stageReferenceProblems } from "./contracts/stage-output.js";
|
|
26
|
+
import { readVersioned } from "./contracts/contract.js";
|
|
27
|
+
import { FLOW_ALIASES, FLOW_FILE_FORMAT, FLOW_FILE_VERSION, flowFileSchema, PARAMETER_ID } from "./contracts/flow.js";
|
|
25
28
|
import { parseSchedule } from "./routine.js";
|
|
26
|
-
export
|
|
27
|
-
export const FLOW_FILE_VERSION = 1;
|
|
29
|
+
export { FLOW_FILE_FORMAT, FLOW_FILE_VERSION };
|
|
28
30
|
/** The largest flow file Toolroll reads or fetches. */
|
|
29
31
|
export const FLOW_FILE_MAX_BYTES = 256 * 1024;
|
|
30
32
|
export const FLOW_FILE_SUFFIX = ".toolroll-flow.json";
|
|
31
33
|
const PARAM = /\{\{\s*param\.([a-z0-9][a-z0-9-]{0,39})\s*\}\}/g;
|
|
32
|
-
const PARAM_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
33
34
|
const REMOVED = "[removed]";
|
|
34
35
|
// ------------------------------------------------------------------ export
|
|
35
36
|
/** A zone in the lead's words: only what it says, in the order a person reads it. */
|
|
36
37
|
function stepOf(stage, ask) {
|
|
37
38
|
const step = { id: stage.id, title: stage.title, kind: stage.kind };
|
|
38
39
|
const words = (minutes) => durationMinutes(durationWords(minutes)) === minutes ? durationWords(minutes) : minutes;
|
|
39
|
-
|
|
40
|
+
// Only what the step's kind says: words a zone kept from a kind it was before stay behind.
|
|
41
|
+
if (stage.instructions !== null && (stage.kind === "task" || stage.kind === "report" || stage.kind === "draft" || stage.kind === "teammate"))
|
|
40
42
|
step["instructions"] = stage.instructions;
|
|
41
43
|
if (stage.kind === "task")
|
|
42
44
|
step["planning"] = stage.planning;
|
|
43
45
|
if (stage.kind === "approval")
|
|
44
46
|
step["decider"] = stage.toOwner === true ? "owner" : stage.approver === null ? "anyone"
|
|
45
47
|
: ask({ id: `decider-${stage.id}`.slice(0, 40), about: `Who decides at ${stage.title}: a person's sign-in name, owner (the flow's owner) or anyone`, default: "owner" });
|
|
46
|
-
if (stage.message !== null)
|
|
48
|
+
if (stage.message !== null && (stage.kind === "notify" || stage.kind === "update"))
|
|
47
49
|
step["message"] = stage.message;
|
|
48
50
|
if (stage.kind === "update")
|
|
49
51
|
step["close"] = stage.close;
|
|
@@ -239,26 +241,28 @@ export function exportFlow(store, flow, dir) {
|
|
|
239
241
|
format: FLOW_FILE_FORMAT, version: FLOW_FILE_VERSION, name: flow.name,
|
|
240
242
|
about: ordered.map(one => one.title).join(" → "),
|
|
241
243
|
needs: needsOf(definition, triggers, scripts.map(one => one.name)),
|
|
242
|
-
|
|
244
|
+
// Zones and triggers in the file's words (stepOf, triggerOf): what flowFileSchema reads back (flow-share.test.ts).
|
|
245
|
+
parameters, zones: zones, triggers: triggers, scripts,
|
|
243
246
|
};
|
|
244
247
|
const file = scrubDeep(raw, scrubber(values, names), null, false);
|
|
245
248
|
const slug = flow.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "flow";
|
|
246
249
|
return { file, json: `${JSON.stringify(file, null, 2)}\n`, fileName: `${slug}${FLOW_FILE_SUFFIX}`, left };
|
|
247
250
|
}
|
|
248
251
|
// ------------------------------------------------------------------ reading
|
|
252
|
+
/** A flow file refused: why, in plain words, each problem naming its path in the file (`zones[0].routes[0]: ...`). */
|
|
249
253
|
export class FlowFileError extends Error {
|
|
250
254
|
}
|
|
251
255
|
const refuse = (message) => { throw new FlowFileError(message); };
|
|
252
256
|
/** A zone from the file as the canvas stores it: references by id (or by title), the rest checked by validateFlowDefinition. */
|
|
253
257
|
function stageInputOf(step, index, find) {
|
|
254
|
-
const
|
|
258
|
+
const at = `zones[${index}]`;
|
|
255
259
|
const kind = step["kind"];
|
|
256
260
|
const object = (key) => {
|
|
257
261
|
const value = step[key];
|
|
258
262
|
if (value === undefined || value === null)
|
|
259
263
|
return undefined;
|
|
260
264
|
if (typeof value !== "object" || Array.isArray(value))
|
|
261
|
-
refuse(
|
|
265
|
+
refuse(`${at}.${key}: must be an object`);
|
|
262
266
|
return value;
|
|
263
267
|
};
|
|
264
268
|
const list = (key) => {
|
|
@@ -266,59 +270,65 @@ function stageInputOf(step, index, find) {
|
|
|
266
270
|
if (value === undefined || value === null)
|
|
267
271
|
return undefined;
|
|
268
272
|
if (!Array.isArray(value) || value.some(one => one === null || typeof one !== "object" || Array.isArray(one)))
|
|
269
|
-
refuse(
|
|
273
|
+
refuse(`${at}.${key}: must be a list of objects`);
|
|
270
274
|
return value;
|
|
271
275
|
};
|
|
272
276
|
const decider = step["decider"];
|
|
273
277
|
if (decider !== undefined && decider !== null && typeof decider !== "string")
|
|
274
|
-
refuse(
|
|
278
|
+
refuse(`${at}.decider: must be a string`);
|
|
275
279
|
const toOwner = kind === "approval" && typeof decider === "string" && decider.trim().toLowerCase() === "owner";
|
|
276
280
|
const anyone = decider === undefined || decider === null || (typeof decider === "string" && /^(anyone|any approver|)$/i.test(decider.trim()));
|
|
277
|
-
const
|
|
278
|
-
const failKey =
|
|
279
|
-
const minutes = (value) => value === undefined ? undefined : durationMinutes(value) ?? refuse(
|
|
281
|
+
const place = object("at") ?? {};
|
|
282
|
+
const [failKey, fail] = ["ifNotSure", "ifNoReply", "ifReplied", "ifFails"].map(key => [key, step[key]]).find(([, value]) => value !== undefined && value !== null) ?? ["ifFails", undefined];
|
|
283
|
+
const minutes = (value, key) => value === undefined ? undefined : durationMinutes(value) ?? refuse(`${at}.${key}: say a time like "3 days" (up to 30 days)`);
|
|
280
284
|
const sureAt = step["sureAt"];
|
|
281
285
|
return {
|
|
282
286
|
id: step["id"], title: step["title"], kind,
|
|
283
|
-
zone: { x:
|
|
287
|
+
zone: { x: place["x"], y: place["y"] ?? 0, w: place["w"], h: place["h"], color: place["color"] },
|
|
284
288
|
instructions: step["instructions"], planning: step["planning"],
|
|
285
289
|
...(toOwner ? { toOwner: true } : { approver: anyone ? null : decider }),
|
|
286
290
|
message: step["message"], close: step["close"], script: step["script"], runIn: step["runIn"],
|
|
287
|
-
routes: list("routes")?.map(one => ({ answer: one["answer"], to: find(one["goesTo"],
|
|
291
|
+
routes: list("routes")?.map((one, n) => ({ answer: one["answer"], to: find(one["goesTo"], `${at}.routes[${n}].goesTo`) })),
|
|
288
292
|
secrets: step["secrets"],
|
|
289
|
-
...(kind === "sort" ? { sort: { question: step["question"], answers: (list("answers") ?? []).map(one => ({ answer: one["answer"], means: one["means"], to: find(one["goesTo"],
|
|
293
|
+
...(kind === "sort" ? { sort: { question: step["question"], answers: (list("answers") ?? []).map((one, n) => ({ answer: one["answer"], means: one["means"], to: find(one["goesTo"], `${at}.answers[${n}].goesTo`) })),
|
|
290
294
|
sureAt: typeof sureAt === "number" && sureAt > 1 ? sureAt / 100 : sureAt, notes: list("alsoNote") ?? [] } } : {}),
|
|
291
295
|
...(kind === "request" ? { request: { method: step["method"], url: step["url"], headers: object("headers") ?? {}, body: step["body"] } } : {}),
|
|
292
296
|
...(kind === "email" ? { email: { to: step["to"], subject: step["subject"], body: step["body"] } } : {}),
|
|
293
297
|
...(kind === "tool" ? { tool: { server: step["server"], name: step["tool"], args: typeof step["args"] === "object" && step["args"] !== null ? JSON.stringify(step["args"]) : step["args"] } } : {}),
|
|
294
|
-
...(kind === "wait" ? { wait: step["waitFor"] === "hours" ? { for: "hours", from: step["from"], to: step["until"], timeZone: step["timeZone"] } : { for: step["waitFor"] ?? "reply", minutes: minutes(step["wait"] ?? "3 days") } } : {}),
|
|
298
|
+
...(kind === "wait" ? { wait: step["waitFor"] === "hours" ? { for: "hours", from: step["from"], to: step["until"], timeZone: step["timeZone"] } : { for: step["waitFor"] ?? "reply", minutes: minutes(step["wait"] ?? "3 days", "wait") } } : {}),
|
|
295
299
|
merge: step["merge"], teammate: step["teammate"], reply: step["reply"],
|
|
296
|
-
...(kind === "choose" ? { options: (list("options") ?? []).map(one => ({ label: one["label"], to: one["goesTo"] === "end" ? "end" : find(one["goesTo"],
|
|
297
|
-
...(step["remindAfter"] === undefined ? {} : { limit: { minutes: minutes(step["remindAfter"]), to: find(step["thenMoveTo"],
|
|
298
|
-
next: find(step["next"],
|
|
300
|
+
...(kind === "choose" ? { options: (list("options") ?? []).map((one, n) => ({ label: one["label"], to: one["goesTo"] === "end" ? "end" : find(one["goesTo"], `${at}.options[${n}].goesTo`) })) } : {}),
|
|
301
|
+
...(step["remindAfter"] === undefined ? {} : { limit: { minutes: minutes(step["remindAfter"], "remindAfter"), to: find(step["thenMoveTo"], `${at}.thenMoveTo`) } }),
|
|
302
|
+
next: find(step["next"], `${at}.next`), onFail: find(fail, `${at}.${failKey}`),
|
|
299
303
|
};
|
|
300
304
|
}
|
|
301
305
|
/** The zones as a flow, checked whole. Throws in plain words. */
|
|
302
306
|
function definitionOf(zones) {
|
|
303
307
|
const ids = zones.map(one => one["id"]);
|
|
304
|
-
const find = (ref,
|
|
308
|
+
const find = (ref, path) => {
|
|
305
309
|
if (ref === undefined || ref === null || ref === "")
|
|
306
310
|
return null;
|
|
307
311
|
if (typeof ref !== "string")
|
|
308
|
-
return refuse(
|
|
312
|
+
return refuse(`${path}: names a zone by its id`);
|
|
309
313
|
const hit = zones.find(one => one["id"] === ref.trim()) ?? zones.find(one => typeof one["title"] === "string" && one["title"].trim().toLowerCase() === ref.trim().toLowerCase());
|
|
310
314
|
if (hit === undefined)
|
|
311
|
-
return refuse(
|
|
312
|
-
return typeof hit["id"] === "string" ? hit["id"] : refuse(`
|
|
315
|
+
return refuse(`${path}: there's no zone called ${ref}`);
|
|
316
|
+
return typeof hit["id"] === "string" ? hit["id"] : refuse(`zones[${zones.indexOf(hit)}].id: required`);
|
|
313
317
|
};
|
|
314
|
-
|
|
315
|
-
refuse(
|
|
318
|
+
ids.forEach((one, index) => { if (typeof one !== "string")
|
|
319
|
+
refuse(`zones[${index}].id: required`); });
|
|
316
320
|
const stages = zones.map((step, index) => stageInputOf(step, index, find));
|
|
321
|
+
// Its zones are the file's, in order: a zone's problem names the file's zone.
|
|
317
322
|
try {
|
|
318
|
-
|
|
323
|
+
const definition = validateFlowDefinition({ version: 1, start: stages[0]?.["id"], stages });
|
|
324
|
+
// An imported flow is a new one: each {{stage.…}} must be one its zone hands on.
|
|
325
|
+
const references = stageReferenceProblems(definition, null);
|
|
326
|
+
if (references.length > 0)
|
|
327
|
+
throw new FlowContractError(references.map(one => one.line));
|
|
328
|
+
return definition;
|
|
319
329
|
}
|
|
320
330
|
catch (error) {
|
|
321
|
-
return refuse(error instanceof Error ? error.message : "Those zones aren't a flow.");
|
|
331
|
+
return refuse(error instanceof FlowContractError ? error.lines.map(line => inStepWords(line, "zones", zones.map(one => typeof one["kind"] === "string" ? one["kind"] : undefined))).join("\n") : error instanceof Error ? error.message : "Those zones aren't a flow.");
|
|
322
332
|
}
|
|
323
333
|
}
|
|
324
334
|
const plainText = (value, cap, what, required) => {
|
|
@@ -328,17 +338,22 @@ const plainText = (value, cap, what, required) => {
|
|
|
328
338
|
return refuse(`The file's ${what} must be plain text.`);
|
|
329
339
|
if (value.length > cap)
|
|
330
340
|
return refuse(`The file's ${what} is longer than ${cap} characters.`);
|
|
331
|
-
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f
|
|
341
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u202a-\u202e\u2066-\u2069]/.test(value))
|
|
332
342
|
return refuse(`The file's ${what} has hidden characters in it.`);
|
|
333
343
|
return value.trim();
|
|
334
344
|
};
|
|
335
|
-
/**
|
|
345
|
+
/**
|
|
346
|
+
* Read a flow file: its size, its JSON and format, then its one schema (flowFileSchema: every zone, trigger, script
|
|
347
|
+
* and parameter, strict about unknown keys — `zones[0].routes[0]: unknown key 'to' (did you mean goesTo?)`), and a
|
|
348
|
+
* newer version refused plainly. Then what the schema can't say: no keys in a trigger, ids and names used once, each
|
|
349
|
+
* {{param.x}} declared, every script valid and the zones a flow. Throws FlowFileError in plain words.
|
|
350
|
+
*/
|
|
336
351
|
export function parseFlowFile(text) {
|
|
337
352
|
if (Buffer.byteLength(text, "utf8") > FLOW_FILE_MAX_BYTES)
|
|
338
353
|
refuse(`That file is too big: a flow file is at most ${FLOW_FILE_MAX_BYTES / 1024} KB.`);
|
|
339
354
|
let raw;
|
|
340
355
|
try {
|
|
341
|
-
raw = JSON.parse(text.replace(
|
|
356
|
+
raw = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
342
357
|
}
|
|
343
358
|
catch {
|
|
344
359
|
return refuse("That isn't a flow file: it isn't valid JSON.");
|
|
@@ -348,75 +363,48 @@ export function parseFlowFile(text) {
|
|
|
348
363
|
const input = raw;
|
|
349
364
|
if (input["format"] !== FLOW_FILE_FORMAT)
|
|
350
365
|
refuse("That isn't a Toolroll flow file: its format isn't \"toolroll-flow\".");
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
: `This flow file has no version Toolroll reads; it should say "version": ${FLOW_FILE_VERSION}.`);
|
|
355
|
-
const name = plainText(input["name"], 80, "name", true);
|
|
356
|
-
const about = plainText(input["about"], 600, "about line", false);
|
|
357
|
-
const array = (key, cap) => {
|
|
358
|
-
const value = input[key];
|
|
359
|
-
if (value === undefined || value === null)
|
|
360
|
-
return [];
|
|
361
|
-
if (!Array.isArray(value))
|
|
362
|
-
return refuse(`The file's ${key} must be a list.`);
|
|
363
|
-
if (value.length > cap)
|
|
364
|
-
return refuse(`The file has more than ${cap} ${key}.`);
|
|
365
|
-
return value;
|
|
366
|
-
};
|
|
367
|
-
const needs = array("needs", 40).map(one => plainText(one, 80, "needs", true));
|
|
368
|
-
const parameters = array("parameters", 20).map((one) => {
|
|
369
|
-
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
370
|
-
return refuse("Each parameter is an object with an id and what it asks for.");
|
|
371
|
-
const row = one;
|
|
372
|
-
const id = typeof row["id"] === "string" && PARAM_ID.test(row["id"]) ? row["id"] : refuse("Each parameter needs a short id: lowercase letters, numbers and dashes.");
|
|
373
|
-
const fallback = row["default"] === undefined || row["default"] === null ? undefined : plainText(row["default"], 200, `default for ${id}`, false);
|
|
374
|
-
return { id, about: plainText(row["about"], 200, `question for ${id}`, true), ...(fallback === undefined ? {} : { default: fallback }), ...(row["optional"] === true ? { optional: true } : {}) };
|
|
375
|
-
});
|
|
376
|
-
if (new Set(parameters.map(one => one.id)).size !== parameters.length)
|
|
377
|
-
refuse("Two parameters have the same id.");
|
|
378
|
-
const rawZones = array("zones", 24);
|
|
379
|
-
if (rawZones.length === 0)
|
|
380
|
-
refuse("The file has no zones.");
|
|
381
|
-
if (rawZones.some(one => one === null || typeof one !== "object" || Array.isArray(one)))
|
|
382
|
-
refuse("Each zone is an object with an id, a title and a kind.");
|
|
383
|
-
const zones = rawZones;
|
|
384
|
-
const triggers = array("triggers", 10).map(one => {
|
|
385
|
-
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
386
|
-
return refuse("Each trigger is an object with a kind.");
|
|
387
|
-
const kind = one["kind"];
|
|
366
|
+
// Triggers that name something on the installation they were made on stay there; say so, not that the kind is unknown.
|
|
367
|
+
(Array.isArray(input["triggers"]) ? input["triggers"] : []).forEach((one, index) => {
|
|
368
|
+
const kind = one !== null && typeof one === "object" ? one["kind"] : undefined;
|
|
388
369
|
if (kind === "chat")
|
|
389
|
-
|
|
370
|
+
refuse(`triggers[${index}].kind: a chat channel trigger can't come from a file; connect the channel from the channel itself`);
|
|
390
371
|
if (kind === "flow")
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
372
|
+
refuse(`triggers[${index}].kind: a trigger from another flow can't come from a file; it names a flow on the installation it was made on`);
|
|
373
|
+
});
|
|
374
|
+
const read = readVersioned(flowFileSchema, input, {}, FLOW_ALIASES);
|
|
375
|
+
if (!read.ok)
|
|
376
|
+
return refuse(read.issues.map(one => one.line).join("\n"));
|
|
377
|
+
const file = read.value;
|
|
378
|
+
const name = file.name.trim(), about = (file.about ?? "").trim();
|
|
379
|
+
if (name === "")
|
|
380
|
+
refuse("name: must not be empty");
|
|
381
|
+
const parameters = (file.parameters ?? []).map(one => ({ ...one, about: one.about.trim(), ...(one.default === undefined ? {} : { default: one.default.trim() }) }));
|
|
382
|
+
if (new Set(parameters.map(one => one.id)).size !== parameters.length)
|
|
383
|
+
refuse("parameters: two have the same id");
|
|
384
|
+
const triggers = file.triggers ?? [];
|
|
385
|
+
triggers.forEach((one, index) => {
|
|
394
386
|
for (const [key, value] of Object.entries(one))
|
|
395
387
|
if (typeof value === "string" && scanForSecrets(value).length > 0)
|
|
396
|
-
refuse(`
|
|
397
|
-
return one;
|
|
388
|
+
refuse(`triggers[${index}].${key}: looks like a key or password; keys never go in a flow file`);
|
|
398
389
|
});
|
|
399
|
-
const scripts =
|
|
400
|
-
if (one === null || typeof one !== "object" || Array.isArray(one))
|
|
401
|
-
return refuse("Each script is an object with a name.");
|
|
402
|
-
const row = one;
|
|
390
|
+
const scripts = (file.scripts ?? []).map((row, index) => {
|
|
403
391
|
let draft;
|
|
404
392
|
try {
|
|
405
393
|
draft = validateScript(row);
|
|
406
394
|
}
|
|
407
395
|
catch (error) {
|
|
408
|
-
return refuse(`
|
|
396
|
+
return refuse(`scripts[${index}]: ${error instanceof Error ? error.message : "it isn't valid."}`);
|
|
409
397
|
}
|
|
410
398
|
return { name: draft.name, about: draft.about, language: draft.language, timeoutMinutes: draft.timeoutMinutes, ...(draft.file === null ? { body: draft.body } : { file: draft.file }) };
|
|
411
399
|
});
|
|
412
400
|
if (new Set(scripts.map(one => one.name)).size !== scripts.length)
|
|
413
|
-
refuse("
|
|
401
|
+
refuse("scripts: two have the same name");
|
|
414
402
|
// Every {{param.x}} names a parameter the file declares.
|
|
415
|
-
for (const match of JSON.stringify([zones, triggers]).matchAll(PARAM))
|
|
403
|
+
for (const match of JSON.stringify([file.zones, triggers]).matchAll(PARAM))
|
|
416
404
|
if (!parameters.some(one => one.id === match[1]))
|
|
417
405
|
refuse(`The file uses {{param.${match[1]}}} but doesn't say what it asks for.`);
|
|
418
|
-
definitionOf(zones);
|
|
419
|
-
return { format: FLOW_FILE_FORMAT, version: FLOW_FILE_VERSION, name, about, needs, parameters, zones, triggers, scripts };
|
|
406
|
+
definitionOf(file.zones);
|
|
407
|
+
return { format: FLOW_FILE_FORMAT, version: FLOW_FILE_VERSION, name, about, needs: (file.needs ?? []).map(one => one.trim()), parameters, zones: file.zones, triggers, scripts };
|
|
420
408
|
}
|
|
421
409
|
/** --param k=v lines (or a form's fields) as values; refuses parameters the file doesn't ask for. */
|
|
422
410
|
export function parameterValues(file, given) {
|
package/dist/flow-sort.d.ts
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type SortDecision } from "./contracts/sort-answer.js";
|
|
2
|
+
import type { FlowSort, FlowStage } from "./flows.js";
|
|
2
3
|
export declare const JEV_URL = "https://openrouter.ai/api/alpha/decisions";
|
|
3
4
|
export declare const JEV_MODEL = "~typesafe/jev-latest";
|
|
4
|
-
export type SortNoteAnswer
|
|
5
|
-
id: string;
|
|
6
|
-
question: string;
|
|
7
|
-
kind: "score" | "yes-no";
|
|
8
|
-
answer: string;
|
|
9
|
-
sure: number;
|
|
10
|
-
};
|
|
11
|
-
/** What a sort step decided, kept on its run (decision_json) and shown on the card. */
|
|
12
|
-
export type SortDecision = {
|
|
13
|
-
model: string;
|
|
14
|
-
/** The answer as the zone names it, and how sure Jev was (0–1). */
|
|
15
|
-
answer: string;
|
|
16
|
-
sure: number;
|
|
17
|
-
sureAt: number;
|
|
18
|
-
/** Sure enough to act alone, and where the card went (null: it waited in the zone). */
|
|
19
|
-
confident: boolean;
|
|
20
|
-
to: string | null;
|
|
21
|
-
/** Every answer's chance, by name. */
|
|
22
|
-
chances: Record<string, number>;
|
|
23
|
-
notes: SortNoteAnswer[];
|
|
24
|
-
cost: number | null;
|
|
25
|
-
ms: number;
|
|
26
|
-
};
|
|
5
|
+
export type { SortDecision, SortNoteAnswer } from "./contracts/sort-answer.js";
|
|
27
6
|
export type SortCard = {
|
|
28
7
|
title: string;
|
|
29
8
|
description: string | null;
|
|
@@ -67,7 +46,10 @@ export type JevAsked = {
|
|
|
67
46
|
};
|
|
68
47
|
/** Ask Jev through OpenRouter. Trouble reaching it (or a refused key, or no credit) is a reason to try again, in words. */
|
|
69
48
|
export declare function askJev(fetcher: typeof fetch, key: string, request: JevRequest): Promise<JevAsked>;
|
|
70
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Jev's answers, read through the reply's contract (src/contracts/sort-answer.ts) and then against the zone's own
|
|
51
|
+
* options only; anything else is a problem, never a route. Confidences and chances are clamped into 0–1.
|
|
52
|
+
*/
|
|
71
53
|
export declare function readJevAnswers(stage: FlowStage, body: Record<string, unknown>, ms: number): SortDecision | {
|
|
72
54
|
problem: string;
|
|
73
55
|
};
|
|
@@ -79,4 +61,3 @@ export declare function sortLog(stage: FlowStage, decision: SortDecision): strin
|
|
|
79
61
|
export declare function parseSortDecision(json: string | null): SortDecision | null;
|
|
80
62
|
/** A card's chip on the canvas: the answer and how sure, and the first score's short level. */
|
|
81
63
|
export declare function sortChip(decision: SortDecision): string;
|
|
82
|
-
export {};
|
package/dist/flow-sort.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* is instructions: an answer is only ever one of the zone's own options.
|
|
9
9
|
*/
|
|
10
10
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
11
|
-
import { sortKeyOf } from "./
|
|
11
|
+
import { SORT_ROUTE_KEY, sortKeyOf, sortNoteKeyOf } from "./contracts/flow.js";
|
|
12
|
+
import { readJevReply } from "./contracts/sort-answer.js";
|
|
12
13
|
export const JEV_URL = "https://openrouter.ai/api/alpha/decisions";
|
|
13
14
|
export const JEV_MODEL = "~typesafe/jev-latest";
|
|
14
15
|
/** Jev reads up to 32,000 tokens; the card is kept well under that. */
|
|
@@ -39,10 +40,10 @@ export function sortState(card, earlier) {
|
|
|
39
40
|
/** The one request for a card: the zone's question over its answers, plus what it also notes. */
|
|
40
41
|
export function sortRequest(sort, state) {
|
|
41
42
|
const questions = {
|
|
42
|
-
|
|
43
|
+
[SORT_ROUTE_KEY]: { type: "choice", instructions: `${sort.question} Read the card's title and details.`, criteria: Object.fromEntries(sort.answers.map(one => [sortKeyOf(one.answer), one.means])) },
|
|
43
44
|
};
|
|
44
45
|
for (const note of sort.notes) {
|
|
45
|
-
questions[
|
|
46
|
+
questions[sortNoteKeyOf(note.id)] = note.kind === "score" && note.levels !== null
|
|
46
47
|
? { type: "score", instructions: note.question, criteria: note.levels }
|
|
47
48
|
: { type: "noul", instructions: note.question };
|
|
48
49
|
}
|
|
@@ -80,38 +81,42 @@ export async function askJev(fetcher, key, request) {
|
|
|
80
81
|
return { ok: true, body, ms: Date.now() - started };
|
|
81
82
|
}
|
|
82
83
|
const shortLevel = (level) => level.split(":")[0].trim();
|
|
83
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Jev's answers, read through the reply's contract (src/contracts/sort-answer.ts) and then against the zone's own
|
|
86
|
+
* options only; anything else is a problem, never a route. Confidences and chances are clamped into 0–1.
|
|
87
|
+
*/
|
|
84
88
|
export function readJevAnswers(stage, body, ms) {
|
|
85
89
|
const sort = stage.sort;
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
90
|
+
const read = readJevReply(body);
|
|
91
|
+
if (!read.ok)
|
|
92
|
+
return { problem: `Jev's answer didn't say which way to go (${read.issues.map(issue => issue.line).join("; ")}).` };
|
|
93
|
+
const reply = read.value;
|
|
94
|
+
const route = reply.answers[SORT_ROUTE_KEY];
|
|
95
|
+
const picked = sort.answers.find(one => sortKeyOf(one.answer) === route.choice);
|
|
91
96
|
if (picked === undefined)
|
|
92
|
-
return { problem: "Jev picked an answer this zone doesn't have." };
|
|
97
|
+
return { problem: "answers.route.choice: Jev picked an answer this zone doesn't have." };
|
|
93
98
|
const number = (value) => typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
94
|
-
const sure = number(route
|
|
95
|
-
const probabilities = (route
|
|
99
|
+
const sure = number(route.confidence);
|
|
100
|
+
const probabilities = (route.probabilities ?? {});
|
|
96
101
|
const chances = Object.fromEntries(sort.answers.map(one => [one.answer, Math.round(number(probabilities[sortKeyOf(one.answer)]) * 100) / 100]));
|
|
97
102
|
const notes = [];
|
|
98
103
|
for (const note of sort.notes) {
|
|
99
|
-
const said = answers
|
|
104
|
+
const said = reply.answers[sortNoteKeyOf(note.id)];
|
|
100
105
|
if (said === undefined)
|
|
101
106
|
continue;
|
|
102
|
-
if (note.kind === "score" && note.levels !== null && typeof said
|
|
103
|
-
const level = note.levels[Math.min(note.levels.length - 1, Math.max(0, Math.round(said
|
|
104
|
-
notes.push({ id: note.id, question: note.question, kind: "score", answer: level, sure: Math.round(number(said
|
|
107
|
+
if (note.kind === "score" && note.levels !== null && typeof said.score === "number") {
|
|
108
|
+
const level = note.levels[Math.min(note.levels.length - 1, Math.max(0, Math.round(said.score)))];
|
|
109
|
+
notes.push({ id: note.id, question: note.question, kind: "score", answer: level, sure: Math.round(number(said.confidence) * 100) / 100 });
|
|
105
110
|
}
|
|
106
|
-
else if (typeof said
|
|
107
|
-
const yes = number(said
|
|
111
|
+
else if (typeof said.noul === "number") {
|
|
112
|
+
const yes = number(said.noul);
|
|
108
113
|
notes.push({ id: note.id, question: note.question, kind: "yes-no", answer: yes >= 0.5 ? "yes" : "no", sure: Math.round(Math.max(yes, 1 - yes) * 100) / 100 });
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
const confident = sure >= sort.sureAt;
|
|
112
|
-
const usage =
|
|
117
|
+
const usage = reply.usage;
|
|
113
118
|
return {
|
|
114
|
-
model: typeof
|
|
119
|
+
model: typeof reply.model === "string" ? reply.model : JEV_MODEL,
|
|
115
120
|
answer: picked.answer, sure: Math.round(sure * 100) / 100, sureAt: sort.sureAt,
|
|
116
121
|
confident, to: confident ? picked.to : stage.onFail, chances, notes,
|
|
117
122
|
cost: typeof usage?.cost === "number" ? usage.cost : null, ms,
|
package/dist/flow-starters.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FlowStepInput } from "./flows.js";
|
|
2
|
+
import type { TriggerInput } from "./contracts/flow.js";
|
|
2
3
|
import type { FlowRow, Store } from "./store.js";
|
|
3
4
|
export declare const STARTER_IDS: readonly ["ci-fix", "issue-task", "overnight", "plane-review"];
|
|
4
5
|
export type StarterId = (typeof STARTER_IDS)[number];
|
|
@@ -22,7 +23,7 @@ export type StarterFlow = {
|
|
|
22
23
|
ownSteps?: FlowStepInput[];
|
|
23
24
|
trigger: (where: {
|
|
24
25
|
branch: string;
|
|
25
|
-
}) =>
|
|
26
|
+
}) => TriggerInput;
|
|
26
27
|
};
|
|
27
28
|
export declare const STARTER_FLOWS: readonly StarterFlow[];
|
|
28
29
|
/** The morning plane review's steps: find the cause, fix it, open a pull request; any failure waits for a person. */
|
package/dist/flow-steps.js
CHANGED
|
@@ -44,6 +44,7 @@ import { ASKED, teammateReady, teammateTurn } from "./teammate-work.js";
|
|
|
44
44
|
import { budgetHoldWords, claudeMachineBilling, monthOf } from "./spend.js";
|
|
45
45
|
import { toolRefusal } from "./policy.js";
|
|
46
46
|
import { pullRequestStep } from "./flow-pull-request.js";
|
|
47
|
+
import { withStageHandoff } from "./contracts/stage-output.js";
|
|
47
48
|
const RETRY_MS = [5 * 60_000, 15 * 60_000];
|
|
48
49
|
/** A teammate's turn tries again sooner: a card waiting on a person's colleague shouldn't wait long for a hiccup. */
|
|
49
50
|
const TEAMMATE_RETRY_MS = [60_000, 5 * 60_000];
|
|
@@ -248,12 +249,12 @@ function settle(store, definition, stage, card, outcome, now, durationMs) {
|
|
|
248
249
|
}
|
|
249
250
|
store.finishFlowStep(card.id, card.entry, { state: outcome.state === "passed" ? "passed" : "failed", result: outcome.said, ...kept }, now);
|
|
250
251
|
// What the steps after it read: whole up to TEXT_LIMITS.stageOutput; longer is attached whole to the card's discussion,
|
|
251
|
-
// and they read a link to it. Never cut.
|
|
252
|
+
// and they read a link to it, while a task filed after it is given it whole (the card keeps it attached). Never cut.
|
|
252
253
|
const whole = outcome.output ?? outcome.said;
|
|
253
254
|
const passed = passOn(whole, TEXT_LIMITS.stageOutput, { label: "the card's discussion", href: flowCardHref(card.flow, card.id) });
|
|
254
255
|
if (passed.kept)
|
|
255
256
|
store.addFlowComment({ card: card.id, author: "flow", body: `What ${stage.title} produced, in full (${whole.length.toLocaleString("en-US")} characters):\n\n${whole}`, mentions: [] }, now);
|
|
256
|
-
store.updateFlowCard(card.id, { outputs:
|
|
257
|
+
store.updateFlowCard(card.id, { outputs: withStageHandoff(card.outputs, stage.id, { text: passed.text }), ...(passed.kept ? { attached: { [stage.id]: whole } } : {}), waiting: null }, now);
|
|
257
258
|
const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
|
|
258
259
|
if (outcome.state === "passed") {
|
|
259
260
|
// A sort names where the card goes; one it isn't sure about is a person's to place.
|