toolroll 0.9.26 → 0.9.28
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 +25 -0
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/dist/agentconfig.js +71 -3
- package/dist/brand-icons.d.ts +168 -0
- package/dist/brand-icons.js +32 -0
- package/dist/brand-mark.d.ts +27 -0
- package/dist/brand-mark.js +49 -0
- package/dist/browser/THIRD_PARTY_NOTICES.txt +41 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +21 -23
- package/dist/browser-workspace.d.ts +17 -0
- package/dist/build-review.js +3 -2
- package/dist/builder.js +10 -8
- package/dist/chat-actions.js +7 -4
- package/dist/chat-delivery.js +3 -3
- package/dist/chat-flow.d.ts +4 -2
- package/dist/chat-flow.js +36 -10
- package/dist/converse.js +3 -2
- package/dist/coordinator-proposals.js +2 -2
- package/dist/decision.d.ts +3 -3
- package/dist/decision.js +4 -3
- package/dist/desktop-update.d.ts +2 -2
- package/dist/evidence.js +1 -1
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +6 -7
- package/dist/flow-code.d.ts +3 -2
- package/dist/flow-code.js +6 -4
- package/dist/flow-draft.d.ts +10 -4
- package/dist/flow-draft.js +19 -8
- package/dist/flow-engine.d.ts +0 -3
- package/dist/flow-engine.js +17 -10
- package/dist/flow-items.d.ts +46 -0
- package/dist/flow-items.js +152 -0
- package/dist/flow-people.d.ts +0 -1
- package/dist/flow-people.js +9 -1
- package/dist/flow-send.d.ts +5 -0
- package/dist/flow-send.js +33 -6
- package/dist/flow-steps.js +33 -10
- package/dist/flows-ui.js +5 -1
- package/dist/flows.d.ts +23 -8
- package/dist/flows.js +106 -52
- package/dist/guides.js +1 -1
- package/dist/integrations-ui.js +10 -3
- package/dist/integrations.d.ts +2 -0
- package/dist/integrations.js +2 -2
- package/dist/lead-context.d.ts +3 -9
- package/dist/lead-context.js +21 -5
- package/dist/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +122 -8
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +228 -16
- package/dist/project-skills.d.ts +10 -0
- package/dist/proposal.d.ts +10 -2
- package/dist/proposal.js +25 -10
- package/dist/recipe-ui.js +2 -1
- package/dist/result-shots.d.ts +5 -1
- package/dist/result-shots.js +33 -3
- package/dist/scout-report.d.ts +10 -4
- package/dist/scout-report.js +23 -6
- package/dist/scout.d.ts +2 -0
- package/dist/scout.js +84 -11
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +76 -23
- package/dist/store.d.ts +64 -5
- package/dist/store.js +192 -7
- package/dist/task-sizing.d.ts +78 -0
- package/dist/task-sizing.js +228 -0
- package/dist/task-text.d.ts +4 -4
- package/dist/task-text.js +10 -6
- package/dist/teammate-work.js +31 -7
- package/dist/teammates.d.ts +20 -1
- package/dist/teammates.js +18 -5
- package/dist/teams-chat.d.ts +4 -1
- package/dist/teams-chat.js +17 -6
- package/dist/telegram-flow.js +6 -2
- package/dist/telegram.js +37 -6
- package/dist/test-temp.js +1 -1
- package/dist/text-limits.d.ts +67 -0
- package/dist/text-limits.js +73 -0
- package/dist/tier-report.d.ts +44 -0
- package/dist/tier-report.js +71 -0
- package/dist/tools-ui.d.ts +1 -3
- package/dist/tools-ui.js +5 -4
- package/package.json +3 -1
package/dist/flow-engine.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
9
|
import { filerFor } from "./approval-policy.js";
|
|
10
10
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
11
|
-
import {
|
|
11
|
+
import { passOn, TEXT_LIMITS } from "./text-limits.js";
|
|
12
|
+
import { deciderOf, durationWords, fillFlowText, flowGoal, flowValueLabel, flowWorkTemplate, validateFlowDefinition, withinHours } from "./flows.js";
|
|
12
13
|
import { reportFillIns, reportSummaryFor } from "./report-summary.js";
|
|
13
14
|
import { fileTaskProposal } from "./proposal.js";
|
|
14
15
|
import { requestResultChanges } from "./result-actions.js";
|
|
15
16
|
import { revisionSourceOf } from "./result-review.js";
|
|
16
17
|
import { readVerifiedReport, scanForSecrets } from "./evidence.js";
|
|
17
18
|
import { cardFollowers, notifyPeople } from "./flow-people.js";
|
|
18
|
-
import { keptDraft } from "./flow-draft.js";
|
|
19
|
+
import { DRAFT_CHARS, keptDraft } from "./flow-draft.js";
|
|
19
20
|
import { chooseStep, flowPersonOf, sendStep } from "./flow-send.js";
|
|
20
21
|
import { parseSoul, teammateLabel } from "./teammates.js";
|
|
21
22
|
/** The flow's drawing, or null when it can't be read (the card then says so and waits). */
|
|
@@ -207,7 +208,7 @@ function advanceCard(store, flow, definition, card, now, options, outcome) {
|
|
|
207
208
|
dedupeKey: `flow-decide:${card.id}:${card.entry}`, kind: "flow-decision", pushClass: "attention", ...(decider === null ? {} : { recipient: decider }),
|
|
208
209
|
subject: (handoff !== null ? `${handoff.label}: ${card.title} needs ${decider === null ? "a decision" : `${decider}'s decision`}` : `${flow.name}: ${card.title} needs ${decider === null ? "a decision" : `${decider}'s decision`}`).slice(0, 200),
|
|
209
210
|
body: `${handoff === null ? "" : `${handoff.said}\n\n`}${text === undefined ? `${stage.title}: approve it, or send it back with a note.`
|
|
210
|
-
: `${stage.title}: approve this draft to send it as written, edit it, or send it back with a note.\n\n${text}`}
|
|
211
|
+
: `${stage.title}: approve this draft to send it as written, edit it, or send it back with a note.\n\n${text}`}`,
|
|
211
212
|
link: flowCardHref(flow.id, card.id), source: { project: flow.repo },
|
|
212
213
|
}, now);
|
|
213
214
|
store.updateFlowCard(card.id, { waiting: `Waiting for ${who} to approve or send it back` }, now);
|
|
@@ -247,9 +248,9 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
|
|
|
247
248
|
// Waiting cards try again every pass, so a cause that's fixed (a shorter zone, a freed backlog) files the work then.
|
|
248
249
|
const wait = (why) => { const waiting = `Couldn't file the work: ${why} It tries again on the next pass.`; if (card.waiting !== waiting)
|
|
249
250
|
store.updateFlowCard(card.id, { waiting }, now); };
|
|
250
|
-
// The goal fits a task: the zone's words whole
|
|
251
|
-
//
|
|
252
|
-
const goal =
|
|
251
|
+
// The goal fits a task: the zone's words whole and the card's details whole; a value too long for the goal (a
|
|
252
|
+
// script's output) is attached instead, and the agent is given it in full (flowGoalCuts). Nothing is cut.
|
|
253
|
+
const goal = flowGoal(stage.instructions ?? card.title, card, id => titleIn(definition, id));
|
|
253
254
|
if (goal === null) {
|
|
254
255
|
wait(`${stage.title}'s instructions are too long for a task. Open the flow and shorten them.`);
|
|
255
256
|
return;
|
|
@@ -313,7 +314,8 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
|
|
|
313
314
|
if (stage.kind === "report") {
|
|
314
315
|
const ref = store.lookupRef(current);
|
|
315
316
|
const summary = ref === null ? null : reportSummaryFor(store, options.evidenceRoot, ref.id);
|
|
316
|
-
|
|
317
|
+
// The summary whole (a report's summary is within TEXT_LIMITS.reportSummary bytes; an older, longer one is linked).
|
|
318
|
+
outputs[stage.id] = summary !== null && "summary" in summary ? passOn(summary.summary, TEXT_LIMITS.stageOutput, { label: `task ${current}`, href: null }).text : "The report is ready on its task.";
|
|
317
319
|
// {{stage.<id>.items}} and {{stage.<id>.report}}: what it found and the whole report, for the zones after it.
|
|
318
320
|
// A visit's report replaces the last visit's, so nothing stale is left behind.
|
|
319
321
|
const view = ref === null || options.evidenceRoot === undefined ? null : readVerifiedReport(store, options.evidenceRoot, ref.id);
|
|
@@ -353,6 +355,9 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
|
|
|
353
355
|
/** What a flow task's goal had to cut, whole, for the agent doing it: each value its zone fills in from the card that the
|
|
354
356
|
* goal doesn't hold in full. Empty when the task didn't come from a flow card or nothing was cut. Script output and
|
|
355
357
|
* card text can come from outside, so the brief quotes these as untrusted data. */
|
|
358
|
+
/** The most of one attached value the agent's brief carries: more than any card value holds (a whole report is at most
|
|
359
|
+
* 64 KiB), so in practice an attachment is never shortened. */
|
|
360
|
+
const FLOW_ATTACHED_LIMIT = 128_000;
|
|
356
361
|
export function flowGoalCuts(store, taskId, goal) {
|
|
357
362
|
try {
|
|
358
363
|
return goalCuts(store, taskId, goal);
|
|
@@ -369,7 +374,6 @@ function goalCuts(store, taskId, goal) {
|
|
|
369
374
|
const stage = definition?.stages.find(one => one.id === card.stage);
|
|
370
375
|
if (card === null || definition == null || stage === undefined || (stage.kind !== "task" && stage.kind !== "report"))
|
|
371
376
|
return [];
|
|
372
|
-
const labels = { "card.title": "The card's title", "card.description": "The card's description", note: "The note it was sent back with" };
|
|
373
377
|
const cuts = [];
|
|
374
378
|
const seen = new Set();
|
|
375
379
|
for (const match of flowWorkTemplate(stage.instructions ?? card.title, card).matchAll(/\{\{\s*(card\.title|card\.description|note|stage\.([a-z0-9-]+)(?:\.(items|report))?)\s*\}\}/g)) {
|
|
@@ -379,8 +383,7 @@ function goalCuts(store, taskId, goal) {
|
|
|
379
383
|
const text = fillFlowText(`{{${match[1]}}}`, card);
|
|
380
384
|
if (text === "" || goal.includes(text))
|
|
381
385
|
continue;
|
|
382
|
-
|
|
383
|
-
cuts.push({ label: labels[match[1]] ?? `What ${titleIn(definition, match[2])} ${part}`, text: text.slice(0, 50_000) });
|
|
386
|
+
cuts.push({ label: flowValueLabel(match[1], id => titleIn(definition, id)), text: text.slice(0, FLOW_ATTACHED_LIMIT) });
|
|
384
387
|
}
|
|
385
388
|
return cuts;
|
|
386
389
|
}
|
|
@@ -488,6 +491,10 @@ export function decideFlowCard(store, input, now) {
|
|
|
488
491
|
if (input.decision === "approve") {
|
|
489
492
|
// An edited draft replaces the one Claude wrote, so the steps after this send what the person approved.
|
|
490
493
|
const draft = draftFor(definition, stage);
|
|
494
|
+
// A person's edit over the draft limit is refused with it, never cut. (A teammate's was asked once to shorten and is kept whole.)
|
|
495
|
+
if (draft !== null && input.teammate === undefined && typeof input.draft === "string" && input.draft.trim().length > DRAFT_CHARS) {
|
|
496
|
+
return { ok: false, message: `Keep the draft to ${DRAFT_CHARS.toLocaleString("en-US")} characters; this is ${input.draft.trim().length.toLocaleString("en-US")}.` };
|
|
497
|
+
}
|
|
491
498
|
if (draft !== null && typeof input.draft === "string" && input.draft.trim() !== "" && input.draft.trim() !== card.outputs[draft.id]?.trim()) {
|
|
492
499
|
store.updateFlowCard(card.id, { outputs: { ...card.outputs, [draft.id]: keptDraft(input.draft) } }, now);
|
|
493
500
|
store.addFlowComment({ card: card.id, author: input.actor, body: "Edited the draft before approving it.", mentions: [] }, now);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A research report's items in a flow's message (flow-send.ts): after a report step, each item is listed under the
|
|
3
|
+
* summary, numbered — its title in bold, its why in plain lines, and its link named by its source ("Mobbin", or the
|
|
4
|
+
* site's name). Each screenshot of an item is captioned with the same number, its title and how it plugs in
|
|
5
|
+
* (result-shots.ts), so "2." in the album is item 2 in the text.
|
|
6
|
+
*
|
|
7
|
+
* The message is written in reply-shape.ts's canonical form (`**bold**`, `[label](url)`), which every channel renders
|
|
8
|
+
* in its own format. Its words go through the channel's own cleaner first. Fitted to a channel's limit, each why is
|
|
9
|
+
* shortened evenly first; then the summary, then the titles; past that it is split into parts. Links are never cut.
|
|
10
|
+
*/
|
|
11
|
+
export type FlowSendItem = {
|
|
12
|
+
title: string;
|
|
13
|
+
why: string;
|
|
14
|
+
url: string;
|
|
15
|
+
/** Where the link goes, by name: "Mobbin", "Linear", "Apple". */
|
|
16
|
+
source: string;
|
|
17
|
+
/** One line: how it plugs in (from the why, else the image's own caption). */
|
|
18
|
+
plug: string;
|
|
19
|
+
/** The item's screenshot (its evidence id in the report's run), when it has one. */
|
|
20
|
+
shot: number | null;
|
|
21
|
+
};
|
|
22
|
+
/** A link's source by name: a few known sites as they write themselves, else the site's own name from its address. */
|
|
23
|
+
export declare function itemSource(url: string): string;
|
|
24
|
+
/** A why as plain lines: its own lines, or "the pattern; why it fits us; how it plugs in" one to a line. */
|
|
25
|
+
export declare function whyLines(why: string): string[];
|
|
26
|
+
/** How an item plugs in, in one line: the why's last line when it has several, else the image's caption, else the why. */
|
|
27
|
+
export declare function itemPlug(why: string, imageCaption: string | null): string;
|
|
28
|
+
/** A screenshot's caption: its item's number, title and how it plugs in. */
|
|
29
|
+
export declare function itemCaption(number: number, item: Pick<FlowSendItem, "title" | "plug">): string;
|
|
30
|
+
export type FlowMessage = {
|
|
31
|
+
head: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
items: readonly FlowSendItem[];
|
|
34
|
+
tail: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
/** The message's own words through a channel's cleaner — head, summary, each title and why, and tail — before it is fitted. */
|
|
37
|
+
export declare function cleanFlowMessage(message: FlowMessage, clean: (text: string) => string): FlowMessage;
|
|
38
|
+
/**
|
|
39
|
+
* The message in canonical form, as parts within `limit` as `measure` counts each once the channel renders it: one
|
|
40
|
+
* whole part when it fits; else every why shortened to the same length; then the summary; then the titles. When the
|
|
41
|
+
* head, links and tail alone are over the limit, the whole message goes in several parts instead, split between
|
|
42
|
+
* blocks (else between lines). Every link stays.
|
|
43
|
+
*/
|
|
44
|
+
export declare function fitFlowMessage(message: FlowMessage, limit: number, measure: (shaped: string) => number): string[];
|
|
45
|
+
/** Items read back from a kept content; anything malformed is left out. */
|
|
46
|
+
export declare function readFlowItems(raw: unknown): FlowSendItem[];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A research report's items in a flow's message (flow-send.ts): after a report step, each item is listed under the
|
|
3
|
+
* summary, numbered — its title in bold, its why in plain lines, and its link named by its source ("Mobbin", or the
|
|
4
|
+
* site's name). Each screenshot of an item is captioned with the same number, its title and how it plugs in
|
|
5
|
+
* (result-shots.ts), so "2." in the album is item 2 in the text.
|
|
6
|
+
*
|
|
7
|
+
* The message is written in reply-shape.ts's canonical form (`**bold**`, `[label](url)`), which every channel renders
|
|
8
|
+
* in its own format. Its words go through the channel's own cleaner first. Fitted to a channel's limit, each why is
|
|
9
|
+
* shortened evenly first; then the summary, then the titles; past that it is split into parts. Links are never cut.
|
|
10
|
+
*/
|
|
11
|
+
const PLUG_CHARS = 160;
|
|
12
|
+
const NAMES = {
|
|
13
|
+
"mobbin.com": "Mobbin", "github.com": "GitHub", "figma.com": "Figma", "dribbble.com": "Dribbble", "behance.net": "Behance",
|
|
14
|
+
"youtube.com": "YouTube", "linkedin.com": "LinkedIn", "producthunt.com": "Product Hunt", "pageflows.com": "Page Flows",
|
|
15
|
+
};
|
|
16
|
+
const cutTo = (text, cap) => cap <= 0 ? "" : text.length <= cap ? text : `${text.slice(0, Math.max(0, cap - 1)).trimEnd()}…`;
|
|
17
|
+
const oneLine = (text) => text.replace(/\s+/g, " ").trim();
|
|
18
|
+
/** A link's source by name: a few known sites as they write themselves, else the site's own name from its address. */
|
|
19
|
+
export function itemSource(url) {
|
|
20
|
+
let host;
|
|
21
|
+
try {
|
|
22
|
+
host = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "Link";
|
|
26
|
+
}
|
|
27
|
+
for (const [site, name] of Object.entries(NAMES))
|
|
28
|
+
if (host === site || host.endsWith(`.${site}`))
|
|
29
|
+
return name;
|
|
30
|
+
const labels = host.split(".");
|
|
31
|
+
if (labels.length < 2 || /^[0-9.]+$/.test(host) || host.includes(":"))
|
|
32
|
+
return host;
|
|
33
|
+
// "bbc.co.uk" is the BBC's; "developer.apple.com" is Apple's.
|
|
34
|
+
const short = labels.length >= 3 && labels[labels.length - 1].length === 2 && labels[labels.length - 2].length <= 3;
|
|
35
|
+
const name = labels[labels.length - (short ? 3 : 2)];
|
|
36
|
+
return name === "" ? host : `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
37
|
+
}
|
|
38
|
+
/** A why as plain lines: its own lines, or "the pattern; why it fits us; how it plugs in" one to a line. */
|
|
39
|
+
export function whyLines(why) {
|
|
40
|
+
const lines = why.split(/\n+/).map(one => one.trim()).filter(one => one !== "");
|
|
41
|
+
if (lines.length !== 1)
|
|
42
|
+
return lines;
|
|
43
|
+
const parts = lines[0].split(/;\s+/).map(one => one.trim()).filter(one => one !== "");
|
|
44
|
+
return parts.length >= 2 && parts.length <= 4 ? parts.map(one => `${one.charAt(0).toUpperCase()}${one.slice(1)}`) : lines;
|
|
45
|
+
}
|
|
46
|
+
/** How an item plugs in, in one line: the why's last line when it has several, else the image's caption, else the why. */
|
|
47
|
+
export function itemPlug(why, imageCaption) {
|
|
48
|
+
const lines = whyLines(why);
|
|
49
|
+
const words = lines.length >= 2 ? lines[lines.length - 1] : imageCaption !== null && imageCaption.trim() !== "" ? imageCaption : lines.join(" ");
|
|
50
|
+
return cutTo(oneLine(words), PLUG_CHARS);
|
|
51
|
+
}
|
|
52
|
+
/** A screenshot's caption: its item's number, title and how it plugs in. */
|
|
53
|
+
export function itemCaption(number, item) {
|
|
54
|
+
return item.plug === "" ? `${number}. ${item.title}` : `${number}. ${item.title} · ${item.plug}`;
|
|
55
|
+
}
|
|
56
|
+
/** Plain words can't become bold or a link by accident. */
|
|
57
|
+
const inert = (text) => text.replace(/\*\*/g, "*").replace(/\]\(/g, "] (");
|
|
58
|
+
/** A link the canonical form keeps whole: the characters it stops at are written percent-encoded, as browsers read them. */
|
|
59
|
+
const linkable = (url) => url.replace(/[()[\]<>"'`\s]/g, char => `%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
60
|
+
/** The message's own words through a channel's cleaner — head, summary, each title and why, and tail — before it is fitted. */
|
|
61
|
+
export function cleanFlowMessage(message, clean) {
|
|
62
|
+
return { head: clean(message.head), summary: clean(message.summary), items: message.items.map(item => ({ ...item, title: clean(item.title), why: clean(item.why) })),
|
|
63
|
+
tail: message.tail.map(clean) };
|
|
64
|
+
}
|
|
65
|
+
function blocks(message, whyCap, summaryCap, titleCap) {
|
|
66
|
+
const items = message.items.map((item, index) => {
|
|
67
|
+
const title = cutTo(oneLine(item.title).replace(/\*/g, ""), titleCap);
|
|
68
|
+
const why = cutTo(whyLines(item.why).join("\n"), whyCap);
|
|
69
|
+
return [`${index + 1}. ${title === "" ? "" : `**${title}**`}`.trimEnd(), ...(why === "" ? [] : [inert(why)]), `[${item.source.replace(/[[\]]/g, "")}](${linkable(item.url)})`].join("\n");
|
|
70
|
+
});
|
|
71
|
+
return [inert(message.head), inert(cutTo(message.summary, summaryCap)), ...items, ...message.tail.map(inert)].filter(one => one.trim() !== "");
|
|
72
|
+
}
|
|
73
|
+
const compose = (message, whyCap, summaryCap, titleCap) => blocks(message, whyCap, summaryCap, titleCap).join("\n\n");
|
|
74
|
+
/** The largest n in [0, high] that fits, or -1. */
|
|
75
|
+
function largest(high, fits) {
|
|
76
|
+
if (!fits(0))
|
|
77
|
+
return -1;
|
|
78
|
+
let low = 0;
|
|
79
|
+
while (low < high) {
|
|
80
|
+
const mid = Math.ceil((low + high) / 2);
|
|
81
|
+
if (fits(mid))
|
|
82
|
+
low = mid;
|
|
83
|
+
else
|
|
84
|
+
high = mid - 1;
|
|
85
|
+
}
|
|
86
|
+
return low;
|
|
87
|
+
}
|
|
88
|
+
/** Whole pieces packed into as few parts as fit, joined by `glue`; a piece too long alone is broken by `smaller`. */
|
|
89
|
+
function pack(pieces, glue, fits, smaller) {
|
|
90
|
+
const parts = [];
|
|
91
|
+
let open = "";
|
|
92
|
+
for (const piece of pieces.flatMap(one => fits(one) ? [one] : smaller(one))) {
|
|
93
|
+
const joined = open === "" ? piece : `${open}${glue}${piece}`;
|
|
94
|
+
if (open === "" || fits(joined))
|
|
95
|
+
open = joined;
|
|
96
|
+
else {
|
|
97
|
+
parts.push(open);
|
|
98
|
+
open = piece;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return open === "" ? parts : [...parts, open];
|
|
102
|
+
}
|
|
103
|
+
/** A line longer than a whole part, in the longest prefixes that fit (never splitting a surrogate pair). */
|
|
104
|
+
function chop(line, fits) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (let rest = line; rest !== "";) {
|
|
107
|
+
let at = Math.max(1, largest(rest.length, n => fits(rest.slice(0, n))));
|
|
108
|
+
if (at < rest.length && /[\uD800-\uDBFF]/.test(rest[at - 1]))
|
|
109
|
+
at = at > 1 ? at - 1 : at + 1;
|
|
110
|
+
out.push(rest.slice(0, at));
|
|
111
|
+
rest = rest.slice(at);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The message in canonical form, as parts within `limit` as `measure` counts each once the channel renders it: one
|
|
117
|
+
* whole part when it fits; else every why shortened to the same length; then the summary; then the titles. When the
|
|
118
|
+
* head, links and tail alone are over the limit, the whole message goes in several parts instead, split between
|
|
119
|
+
* blocks (else between lines). Every link stays.
|
|
120
|
+
*/
|
|
121
|
+
export function fitFlowMessage(message, limit, measure) {
|
|
122
|
+
const fits = (text) => measure(text) <= limit;
|
|
123
|
+
const full = compose(message, Infinity, Infinity, Infinity);
|
|
124
|
+
if (fits(full))
|
|
125
|
+
return [full];
|
|
126
|
+
const longest = Math.max(0, ...message.items.map(one => whyLines(one.why).join("\n").length));
|
|
127
|
+
const why = largest(longest, n => fits(compose(message, n, Infinity, Infinity)));
|
|
128
|
+
if (why >= 0)
|
|
129
|
+
return [compose(message, why, Infinity, Infinity)];
|
|
130
|
+
const summary = largest(message.summary.length, n => fits(compose(message, 0, n, Infinity)));
|
|
131
|
+
if (summary >= 0)
|
|
132
|
+
return [compose(message, 0, summary, Infinity)];
|
|
133
|
+
const title = largest(Math.max(0, ...message.items.map(one => one.title.length)), n => fits(compose(message, 0, 0, n)));
|
|
134
|
+
if (title >= 0)
|
|
135
|
+
return [compose(message, 0, 0, title)];
|
|
136
|
+
return pack(blocks(message, Infinity, Infinity, Infinity), "\n\n", fits, block => pack(block.split("\n"), "\n", fits, line => chop(line, fits)));
|
|
137
|
+
}
|
|
138
|
+
/** Items read back from a kept content; anything malformed is left out. */
|
|
139
|
+
export function readFlowItems(raw) {
|
|
140
|
+
if (!Array.isArray(raw))
|
|
141
|
+
return [];
|
|
142
|
+
return raw.flatMap((one) => {
|
|
143
|
+
if (typeof one !== "object" || one === null)
|
|
144
|
+
return [];
|
|
145
|
+
const item = one;
|
|
146
|
+
const text = (key) => typeof item[key] === "string" ? item[key] : null;
|
|
147
|
+
const [title, why, url, source, plug] = [text("title"), text("why"), text("url"), text("source"), text("plug")];
|
|
148
|
+
if (title === null || why === null || url === null || source === null || plug === null || !/^https?:\/\//.test(url))
|
|
149
|
+
return [];
|
|
150
|
+
return [{ title, why, url, source, plug, shot: typeof item["shot"] === "number" && Number.isSafeInteger(item["shot"]) ? item["shot"] : null }];
|
|
151
|
+
});
|
|
152
|
+
}
|
package/dist/flow-people.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ type CardEvent = {
|
|
|
10
10
|
body: string;
|
|
11
11
|
attention?: boolean;
|
|
12
12
|
};
|
|
13
|
-
/** One notification per person, to that person only. The actor never notifies themself. */
|
|
14
13
|
export declare function notifyPeople(store: Store, card: FlowCardRow, people: Iterable<string>, actor: string | null, event: CardEvent, now: Date): string[];
|
|
15
14
|
/** Who follows a card: its owner and its watchers. */
|
|
16
15
|
export declare function cardFollowers(store: Store, card: FlowCardRow): string[];
|
package/dist/flow-people.js
CHANGED
|
@@ -22,6 +22,14 @@ export function mentionsIn(body, people) {
|
|
|
22
22
|
return found;
|
|
23
23
|
}
|
|
24
24
|
/** One notification per person, to that person only. The actor never notifies themself. */
|
|
25
|
+
/** A notice's body: whole up to 2,000 characters; longer, its start and a line saying the rest is on the card it links. */
|
|
26
|
+
const NOTICE_CHARS = 2000;
|
|
27
|
+
function noticeBody(body) {
|
|
28
|
+
if (body.length <= NOTICE_CHARS)
|
|
29
|
+
return body;
|
|
30
|
+
const mark = "\n… (the rest is on the card)";
|
|
31
|
+
return `${body.slice(0, NOTICE_CHARS - mark.length)}${mark}`;
|
|
32
|
+
}
|
|
25
33
|
export function notifyPeople(store, card, people, actor, event, now) {
|
|
26
34
|
const flow = store.getFlow(card.flow);
|
|
27
35
|
if (flow === null)
|
|
@@ -33,7 +41,7 @@ export function notifyPeople(store, card, people, actor, event, now) {
|
|
|
33
41
|
const queued = store.enqueueNotification({
|
|
34
42
|
dedupeKey: `flow-card:${card.id}:${event.key}:${person}`, kind: "flow-card", recipient: person,
|
|
35
43
|
...(event.attention === true ? { pushClass: "attention" } : {}),
|
|
36
|
-
subject: `${flow.name}: ${event.subject}`.slice(0, 200), body: event.body
|
|
44
|
+
subject: `${flow.name}: ${event.subject}`.slice(0, 200), body: noticeBody(event.body), link: flowCardHref(flow.id, card.id), source: { project: flow.repo },
|
|
37
45
|
}, now);
|
|
38
46
|
if (queued)
|
|
39
47
|
sent.push(person);
|
package/dist/flow-send.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FlowDecision } from "./flow-engine.js";
|
|
2
|
+
import { type FlowSendItem } from "./flow-items.js";
|
|
2
3
|
import { type FlowDefinition, type FlowStage } from "./flows.js";
|
|
3
4
|
import { type FlowCardRow, type FlowRow, type Store } from "./store.js";
|
|
4
5
|
/** The notice a "Send to me" visit sends: `flow-send:<card>:<entry>`. */
|
|
@@ -26,6 +27,8 @@ export type FlowSendContent = {
|
|
|
26
27
|
taskId: string;
|
|
27
28
|
run: number;
|
|
28
29
|
} | null;
|
|
30
|
+
/** After research: the report's items, numbered in this order in every message and screenshot caption (flow-items.ts). */
|
|
31
|
+
items?: FlowSendItem[];
|
|
29
32
|
/** choose: the options as they were offered, and whether a reply is taken. */
|
|
30
33
|
options?: {
|
|
31
34
|
choice: number;
|
|
@@ -52,6 +55,8 @@ export declare function sendStep(store: Store, flow: FlowRow, definition: FlowDe
|
|
|
52
55
|
export declare function chooseStep(store: Store, flow: FlowRow, definition: FlowDefinition, stage: FlowStage, card: FlowCardRow, now: Date, evidenceRoot?: string): void;
|
|
53
56
|
/** The words above a choice's buttons: what was done, then what to do. */
|
|
54
57
|
export declare function flowChooseBody(content: FlowSendContent): string;
|
|
58
|
+
/** What a sent visit's message ends with, after its summary (and items): its pull request, and for a choice what to do. */
|
|
59
|
+
export declare function flowSendTail(content: FlowSendContent): string[];
|
|
55
60
|
export type FlowChoiceVisit = {
|
|
56
61
|
card: FlowCardRow;
|
|
57
62
|
flow: FlowRow;
|
package/dist/flow-send.js
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* Deterministic and model-free, in the worker's flow pass.
|
|
15
15
|
*/
|
|
16
16
|
import { assignmentOf } from "./assignment.js";
|
|
17
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
17
18
|
import { withActor } from "./actor.js";
|
|
18
19
|
import { flowCardHref, flowDefinitionOf, revisionWithNote } from "./flow-engine.js";
|
|
20
|
+
import { itemPlug, itemSource, readFlowItems } from "./flow-items.js";
|
|
19
21
|
import { notifyPeople } from "./flow-people.js";
|
|
22
|
+
import { readVerifiedReport } from "./evidence.js";
|
|
20
23
|
import { FLOW_END, replyTarget } from "./flows.js";
|
|
21
24
|
import { FLOW_SHOTS_KIND } from "./store.js";
|
|
22
25
|
/** The notice a "Send to me" visit sends: `flow-send:<card>:<entry>`. */
|
|
@@ -63,6 +66,18 @@ function reportShotsOf(store, taskId) {
|
|
|
63
66
|
const run = store.runsFor(ref.id).find(one => one.finishedAt !== null && one.role === "scout");
|
|
64
67
|
return run !== undefined && run.outcome === "built" && store.artifactsFor(run.id).some(one => one.kind === "screenshot") ? { taskId, run: run.id } : null;
|
|
65
68
|
}
|
|
69
|
+
/** A research task's report items, each with its source, how it plugs in, and its screenshot from that run. */
|
|
70
|
+
function reportItemsOf(store, taskId, shots, evidenceRoot) {
|
|
71
|
+
const ref = store.lookupRef(taskId);
|
|
72
|
+
const view = ref === null || evidenceRoot === undefined ? null : readVerifiedReport(store, evidenceRoot, ref.id);
|
|
73
|
+
if (view === null || !view.ok)
|
|
74
|
+
return [];
|
|
75
|
+
return view.report.items.map(item => {
|
|
76
|
+
const image = item.image === null ? undefined : view.report.images.find(one => one.file === item.image);
|
|
77
|
+
const shot = item.image === null || shots?.run !== view.run ? null : view.shots.find(one => one.file === item.image)?.artifactId ?? null;
|
|
78
|
+
return { title: plain(item.title), why: plain(item.why), url: item.url, source: itemSource(item.url), plug: plain(itemPlug(item.why, image?.caption ?? null)), shot };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
/** What a visit sends its person, worked out from the card as it is now. */
|
|
67
82
|
export function flowSendContent(store, flow, definition, stage, card, now, evidenceRoot) {
|
|
68
83
|
const before = stepBefore(store, definition, card);
|
|
@@ -96,10 +111,12 @@ export function flowSendContent(store, flow, definition, stage, card, now, evide
|
|
|
96
111
|
links.push({ label: "Report", path: `/t/${encodeURIComponent(report)}` });
|
|
97
112
|
links.push({ label: "Card", path: flowCardHref(flow.id, card.id) });
|
|
98
113
|
const options = stage.kind === "choose" ? (stage.options ?? []).map((one, choice) => ({ choice, label: one.label })) : undefined;
|
|
114
|
+
// After research, its own screenshots are what that step produced; otherwise the build's.
|
|
115
|
+
const shots = (report === null ? null : reportShotsOf(store, report)) ?? result?.shots ?? null;
|
|
116
|
+
const items = report === null ? [] : reportItemsOf(store, report, shots?.taskId === report ? shots : null, evidenceRoot);
|
|
99
117
|
return {
|
|
100
118
|
title: cut(plain(before === null ? card.title : `${card.title} · ${before.title}`), 200), ...(before === null ? {} : { from: before.title }),
|
|
101
|
-
|
|
102
|
-
summary: plain(lines.join("\n\n")), links, shots: (report === null ? null : reportShotsOf(store, report)) ?? result?.shots ?? null,
|
|
119
|
+
summary: plain(lines.join("\n\n")), links, shots, ...(items.length === 0 ? {} : { items }),
|
|
103
120
|
...(options === undefined ? {} : { options, reply: replyTarget(stage) !== null }),
|
|
104
121
|
};
|
|
105
122
|
}
|
|
@@ -107,7 +124,13 @@ export function flowSendContent(store, flow, definition, stage, card, now, evide
|
|
|
107
124
|
export function readFlowSend(json) {
|
|
108
125
|
try {
|
|
109
126
|
const raw = JSON.parse(json);
|
|
110
|
-
|
|
127
|
+
if (typeof raw.title !== "string" || typeof raw.summary !== "string" || !Array.isArray(raw.links))
|
|
128
|
+
return null;
|
|
129
|
+
if (raw.items === undefined)
|
|
130
|
+
return raw;
|
|
131
|
+
const items = readFlowItems(raw.items);
|
|
132
|
+
const { items: _, ...rest } = raw;
|
|
133
|
+
return items.length === 0 ? rest : { ...rest, items };
|
|
111
134
|
}
|
|
112
135
|
catch {
|
|
113
136
|
return null;
|
|
@@ -168,8 +191,12 @@ export function chooseStep(store, flow, definition, stage, card, now, evidenceRo
|
|
|
168
191
|
}
|
|
169
192
|
/** The words above a choice's buttons: what was done, then what to do. */
|
|
170
193
|
export function flowChooseBody(content) {
|
|
194
|
+
return [content.summary, ...flowSendTail(content)].join("\n\n");
|
|
195
|
+
}
|
|
196
|
+
/** What a sent visit's message ends with, after its summary (and items): its pull request, and for a choice what to do. */
|
|
197
|
+
export function flowSendTail(content) {
|
|
171
198
|
const pull = content.links.find((one) => "url" in one);
|
|
172
|
-
return [
|
|
199
|
+
return [...(pull === undefined ? [] : [`${pull.label}: ${pull.url}`]), ...(content.options === undefined ? [] : [content.reply === true ? `Choose one. ${REPLY_ASK}` : "Choose one."])];
|
|
173
200
|
}
|
|
174
201
|
/** The card, if it is still at that visit, waiting for its person's choice. */
|
|
175
202
|
export function flowChoiceAt(store, cardId, entry) {
|
|
@@ -240,8 +267,8 @@ export function chooseFlowCard(store, input, now) {
|
|
|
240
267
|
return { ok: false, message: "Where replies go changed since; nothing was changed." };
|
|
241
268
|
if (note === "")
|
|
242
269
|
return { ok: false, message: "Say what you'd change." };
|
|
243
|
-
if (note.length >
|
|
244
|
-
return { ok: false, message:
|
|
270
|
+
if (note.length > TEXT_LIMITS.note)
|
|
271
|
+
return { ok: false, message: `Keep it to ${TEXT_LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` };
|
|
245
272
|
const revision = revisionWithNote(store, card, definition.stages.find(one => one.id === target), note, input.actor, input.repos, input.evidenceRoot, now);
|
|
246
273
|
const moved = store.transact(() => {
|
|
247
274
|
if (!store.moveFlowCard(card.id, { to: target, outcome: "sent-back", actor: input.actor, note, ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now))
|
package/dist/flow-steps.js
CHANGED
|
@@ -27,12 +27,13 @@ import { join } from "node:path";
|
|
|
27
27
|
import { approvedCommandShell, redactSecretAssignments, SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
28
28
|
import { assignmentOf } from "./assignment.js";
|
|
29
29
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
30
|
-
import { flowDefinitionOf } from "./flow-engine.js";
|
|
30
|
+
import { flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
31
31
|
import { cardFollowers, notifyPeople } from "./flow-people.js";
|
|
32
32
|
import { LINEAR_URL, readLinearKey } from "./flow-triggers.js";
|
|
33
33
|
import { cardEmailOf, fillFlowText } from "./flows.js";
|
|
34
34
|
import { JEV_MODEL, askJev, readJevAnswers, sortLog, sortRequest, sortState, sortWords } from "./flow-sort.js";
|
|
35
|
-
import { claudeDraftRunner, DRAFT_TIMEOUT, draftPrompt,
|
|
35
|
+
import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft } from "./flow-draft.js";
|
|
36
|
+
import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
|
|
36
37
|
import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
|
|
37
38
|
import { cleanFolder, runCode } from "./flow-code.js";
|
|
38
39
|
import { agentFence } from "./agent-fence.js";
|
|
@@ -246,7 +247,13 @@ function settle(store, definition, stage, card, outcome, now, durationMs) {
|
|
|
246
247
|
outcome = { state: "failed", said: `${outcome.said} It didn't work after three tries.` };
|
|
247
248
|
}
|
|
248
249
|
store.finishFlowStep(card.id, card.entry, { state: outcome.state === "passed" ? "passed" : "failed", result: outcome.said, ...kept }, now);
|
|
249
|
-
|
|
250
|
+
// 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
|
+
const whole = outcome.output ?? outcome.said;
|
|
253
|
+
const passed = passOn(whole, TEXT_LIMITS.stageOutput, { label: "the card's discussion", href: flowCardHref(card.flow, card.id) });
|
|
254
|
+
if (passed.kept)
|
|
255
|
+
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: { ...card.outputs, [stage.id]: passed.text }, waiting: null }, now);
|
|
250
257
|
const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
|
|
251
258
|
if (outcome.state === "passed") {
|
|
252
259
|
// A sort names where the card goes; one it isn't sure about is a person's to place.
|
|
@@ -289,16 +296,32 @@ function draftModel(store) {
|
|
|
289
296
|
/** A draft: Claude writes what the zone asks, from the card, with the lead chat's Claude model; the text stays on the card. */
|
|
290
297
|
async function draftCard(store, definition, stage, card, io, repo, now) {
|
|
291
298
|
const model = draftModel(store);
|
|
292
|
-
const
|
|
299
|
+
const runner = io.draft ?? claudeDraftRunner();
|
|
300
|
+
// Claude is told the limit before it writes; a draft over it is asked once to shorten, and one still over is kept whole
|
|
301
|
+
// in the step's log and attached to the card's discussion, linked from what the next steps read (settle): never cut.
|
|
302
|
+
let first = null;
|
|
303
|
+
const written = await writeWithin(async (shorten) => {
|
|
304
|
+
const answer = await runner({ model, prompt: draftPrompt(stage, card, definition, shorten === null || first === null ? null : { draft: first, ask: shorten }), timeoutMs: DRAFT_TIMEOUT });
|
|
305
|
+
// v105: what it cost, billed as this computer's Claude does (a plan's is $0).
|
|
306
|
+
if (answer.ok) {
|
|
307
|
+
store.recordDraftSpend({ repo, model: model === "default" ? null : model, costUsd: answer.costUsd ?? null }, now);
|
|
308
|
+
first ??= wholeDraft(answer.text);
|
|
309
|
+
}
|
|
310
|
+
return answer;
|
|
311
|
+
}, answer => answer.ok ? overruns({ draft: wholeDraft(answer.text) }, { draft: DRAFT_CHARS }) : []);
|
|
312
|
+
const answer = written.answer;
|
|
293
313
|
if (!answer.ok)
|
|
294
|
-
return { state: "retry", said: answer.said };
|
|
295
|
-
|
|
296
|
-
store.recordDraftSpend({ repo, model: model === "default" ? null : model, costUsd: answer.costUsd ?? null }, now);
|
|
297
|
-
const text = keptDraft(answer.text);
|
|
314
|
+
return written.repaired && first !== null ? draftOutcome(first, model, null, true) : { state: "retry", said: answer.said };
|
|
315
|
+
const text = wholeDraft(answer.text);
|
|
298
316
|
if (text === "")
|
|
299
|
-
return { state: "retry", said: "Claude's draft came back empty." };
|
|
317
|
+
return written.repaired && first !== null ? draftOutcome(first, model, answer.ms, true) : { state: "retry", said: "Claude's draft came back empty." };
|
|
318
|
+
return draftOutcome(text, model, answer.ms, written.repaired);
|
|
319
|
+
}
|
|
320
|
+
/** A draft step's result: the draft whole, on the card and in its log. */
|
|
321
|
+
function draftOutcome(text, model, ms, shortened) {
|
|
300
322
|
const words = text.split(/\s+/).filter(Boolean).length;
|
|
301
|
-
|
|
323
|
+
const over = text.length > DRAFT_CHARS ? ` It is still over the ${DRAFT_CHARS.toLocaleString("en-US")}-character limit after one ask to shorten, so it is kept whole.` : shortened ? " Shortened once to fit." : "";
|
|
324
|
+
return { state: "passed", said: `Drafted ${words} word${words === 1 ? "" : "s"}.${over}`, output: text, log: `Claude (${model})${ms === null ? "" : ` · ${(ms / 1000).toFixed(1)} s`}\n\n${text}` };
|
|
302
325
|
}
|
|
303
326
|
/** A sort: Jev picks one of the zone's answers for the card; the answer (or the not-sure path) says where it goes. */
|
|
304
327
|
async function sortCard(definition, stage, card, key, io) {
|
package/dist/flows-ui.js
CHANGED
|
@@ -7,6 +7,7 @@ import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions
|
|
|
7
7
|
import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
|
|
8
8
|
import { flowFingerprint } from "./flow-live.js";
|
|
9
9
|
import { flowChoiceAt, readFlowSend } from "./flow-send.js";
|
|
10
|
+
import { itemCaption, whyLines } from "./flow-items.js";
|
|
10
11
|
import { FLOW_FILE_MAX_BYTES } from "./flow-share.js";
|
|
11
12
|
import { googleConnected } from "./google-mail.js";
|
|
12
13
|
import { mailboxReady } from "./mailbox.js";
|
|
@@ -130,7 +131,10 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
130
131
|
// 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
132
|
const own = new Set([`/flows/${flow.id}?card=${card.id}`, ...(task === null ? [] : [`/t/${encodeURIComponent(task)}`])]);
|
|
132
133
|
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))
|
|
134
|
+
links: content.links.map(one => ({ label: one.label, href: "url" in one ? one.url : one.path })).filter(one => !own.has(one.href)),
|
|
135
|
+
// After research: the same numbered items the message carried, each beside its screenshot.
|
|
136
|
+
...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
|
|
137
|
+
image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
|
|
134
138
|
return {
|
|
135
139
|
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state, waiting: card.waiting,
|
|
136
140
|
task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}` },
|
package/dist/flows.d.ts
CHANGED
|
@@ -167,6 +167,14 @@ export declare function deciderOf(stage: Pick<FlowStage, "approver" | "toOwner">
|
|
|
167
167
|
export declare function validateFlowDefinition(input: unknown, options?: {
|
|
168
168
|
stored?: boolean;
|
|
169
169
|
}): FlowDefinition;
|
|
170
|
+
/** The instructions 0.9.26 and earlier read from a saved zone: they re-check this length on every read, and a flow with a
|
|
171
|
+
* longer one won't load. A saved zone keeps its first part under it, and the rest in `instructionsMore`, which they ignore. */
|
|
172
|
+
export declare const FLOW_INSTRUCTIONS_STORED = 4000;
|
|
173
|
+
/** A flow's definition as the store saves it: any zone's instructions over FLOW_INSTRUCTIONS_STORED are split, so a
|
|
174
|
+
* person who goes back a version still opens the flow (with its instructions' first part). Unchanged otherwise. */
|
|
175
|
+
export declare function flowDefinitionForStore(json: string): string;
|
|
176
|
+
/** A saved flow's definition with every zone's instructions whole again (flowDefinitionForStore). */
|
|
177
|
+
export declare function flowDefinitionFromStore(json: string): string;
|
|
170
178
|
/** The zones a card can reach from `start` without passing through a zone `stop` picks (those zones aren't included). */
|
|
171
179
|
export declare function reachableWithout(stages: readonly FlowStage[], start: string, stop: (stage: FlowStage) => boolean): Set<string>;
|
|
172
180
|
/** The zones a choose zone's options lead to (an option that ends the card leads to none). */
|
|
@@ -183,20 +191,27 @@ type FlowFillCard = {
|
|
|
183
191
|
};
|
|
184
192
|
/** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
|
|
185
193
|
export declare function fillFlowText(template: string, card: FlowFillCard, encode?: (value: string) => string): string;
|
|
186
|
-
/** The task door's goal limit (task-text.ts). A work zone's goal is kept
|
|
187
|
-
export declare const FLOW_GOAL_LIMIT
|
|
188
|
-
/** What stands in
|
|
189
|
-
export declare const
|
|
194
|
+
/** The task door's goal limit (task-text.ts). A work zone's goal is kept within it. */
|
|
195
|
+
export declare const FLOW_GOAL_LIMIT: 8000;
|
|
196
|
+
/** What stands in a goal for a filled-in value too long to hold whole: the agent is given it whole, attached (flowGoalCuts). */
|
|
197
|
+
export declare const flowAttachedMark: (label: string) => string;
|
|
190
198
|
/** A work zone's words, plus the card and any send-back note when the words leave them out. "Fix the bug on the card"
|
|
191
199
|
* says nothing on its own (found in the real e2e: the builder saw only the branch name). */
|
|
192
200
|
export declare function flowWorkTemplate(instructions: string, card: {
|
|
193
201
|
description: string | null;
|
|
194
202
|
note: string | null;
|
|
195
203
|
}): string;
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
/** How a filled-in value is named where it is attached: the same words flowGoalCuts labels it with. */
|
|
205
|
+
export declare function flowValueLabel(key: string, stageTitle?: (id: string) => string): string;
|
|
206
|
+
/** A work zone's goal, filled from the card and kept within `limit`: the zone's own words stay whole, and every filled-in
|
|
207
|
+
* value goes in whole. When they don't all fit, the longest values are attached instead (each named in the goal by
|
|
208
|
+
* flowAttachedMark, and given to the agent whole by flowGoalCuts) until the rest fits: nothing is cut. Words that fill
|
|
209
|
+
* the limit to the last character leave attached values a shorter mark, then none (they are still attached). Null only
|
|
210
|
+
* when the template's own words are over the limit: the caller then files the zone's words alone (flowGoal). */
|
|
211
|
+
export declare function fitFlowText(template: string, card: FlowFillCard, limit?: 8000, stageTitle?: (id: string) => string): string | null;
|
|
212
|
+
/** A work zone's goal for a card: its words, the card and any note, fitted (fitFlowText); when the zone's words leave no
|
|
213
|
+
* room for the card's own lines, its words alone, with the card attached. Null only for words over the limit. */
|
|
214
|
+
export declare function flowGoal(instructions: string, card: FlowFillCard, stageTitle?: (id: string) => string): string | null;
|
|
200
215
|
/** The first email address a card mentions, or "" — what {{card.email}} holds. */
|
|
201
216
|
export declare function cardEmailOf(card: {
|
|
202
217
|
title: string;
|