@foldrun/core 0.4.0 → 0.5.0
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/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +129 -0
- package/dist/src/browser-pod.js +284 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +6 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +245 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +403 -42
- package/dist/src/runner.d.ts +142 -9
- package/dist/src/runner.js +1027 -122
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +3 -2
- package/dist/src/script-tools.js +35 -3
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +61 -2
- package/dist/src/step-exec.js +414 -36
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +218 -37
- package/dist/src/store.js +347 -60
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +3 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +313 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +11 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +316 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +439 -43
- package/src/runner.ts +1050 -122
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +38 -5
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +429 -37
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +500 -68
- package/src/subagents.ts +189 -0
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/hello/agents/curator/agent.md +1 -1
- package/templates/hello/agents/notetaker/agent.md +1 -1
package/dist/src/flow-lint.js
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
// Every check here is *structural*: it reasons about what a step can possibly
|
|
11
11
|
// have in context, never about whether the model did a good job. Nothing is
|
|
12
12
|
// blocked — flows still run, warnings are advisory.
|
|
13
|
+
import { whenRowsPath } from "./store.js";
|
|
14
|
+
import { timezoneProblem } from "./clock.js";
|
|
15
|
+
/** What a `[[flow:x]]` step's options are called in a flow file, for the
|
|
16
|
+
* ones expandSubflows does not carry into the nested steps. */
|
|
17
|
+
const SUBFLOW_DROPPED = [
|
|
18
|
+
["approve", "an approval (!)"], ["when", "when:"], ["case", "case:"], ["else", "else:"],
|
|
19
|
+
["retry", "retry:"], ["timeout", "timeout:"], ["verify", "verify:"], ["model", "model:"],
|
|
20
|
+
["effort", "effort:"], ["onFail", "on-fail:"], ["waitSecs", "wait:"], ["waitFor", "wait:"],
|
|
21
|
+
["ask", "ask:"], ["preview", "preview:"], ["delegate", "delegate:"], ["loop", "loop:"],
|
|
22
|
+
["until", "until:"], ["each", "each:"], ["max", "max:"], ["output", "output:"],
|
|
23
|
+
["schema", "schema:"], ["parallel", "parallel:"], ["maxTurns", "max_turns:"], ["limits", "limits:"],
|
|
24
|
+
];
|
|
13
25
|
// Anaphora: phrases that only mean something if earlier output exists.
|
|
14
26
|
// Deliberately conservative — a false warning is worse than a missed one,
|
|
15
27
|
// because noisy warnings get ignored and then the real one is invisible too.
|
|
@@ -26,9 +38,40 @@ function looksLikeAddress(a) {
|
|
|
26
38
|
const dot = domain.indexOf(".");
|
|
27
39
|
return dot > 0 && dot < domain.length - 1;
|
|
28
40
|
}
|
|
41
|
+
/** The assertion words a `verify:` may open with. Anything else is handed to
|
|
42
|
+
* a shell — see `checkVerify` in step-exec.ts, which owns this list. */
|
|
43
|
+
const VERIFY_ASSERTION = /^(contains|not-contains|matches|file|judge):\s/;
|
|
44
|
+
/** What a shell command has that a sentence does not: an operator that only
|
|
45
|
+
* means something to a shell, a path or bracket at the very start, or one
|
|
46
|
+
* of the commands these flows actually reach for. Deliberately NOT any
|
|
47
|
+
* punctuation a sentence might carry — the first version disqualified on
|
|
48
|
+
* `;`, `,`, `'` and `"`, so "the reply opens with GOOD; a GOOD reply names
|
|
49
|
+
* the SHA" read as shell and a guaranteed exit 127 sat in fix-desk unseen
|
|
50
|
+
* by the very lint written to catch it. */
|
|
51
|
+
const LOOKS_LIKE_SHELL = /&&|\|\||\s\|\s|\$\(|\$\{|`|\s[<>]\s|\d?>[>&]?\s|^\[|^[.~/]|^\s*\b(test|grep|egrep|find|head|tail|cat|ls|node|npm|npx|bash|sh|zsh|python3?|jq|awk|sed|curl|git|make|exit|n=|[A-Za-z_]+=)/;
|
|
52
|
+
/** A `verify:` that is a claim in English rather than a command. Conservative
|
|
53
|
+
* on purpose, the same bargain as REFERS_BACK: it must have no assertion
|
|
54
|
+
* prefix, nothing shell-shaped anywhere in it, and enough words to be a
|
|
55
|
+
* sentence — so a bare `./check.sh` or a one-word command is left alone and
|
|
56
|
+
* the warning only fires where the author plainly wrote prose. */
|
|
57
|
+
function looksLikeProse(verify) {
|
|
58
|
+
const v = verify.trim();
|
|
59
|
+
if (!v || VERIFY_ASSERTION.test(v))
|
|
60
|
+
return false;
|
|
61
|
+
if (LOOKS_LIKE_SHELL.test(v))
|
|
62
|
+
return false;
|
|
63
|
+
return v.split(/\s+/).length >= 4;
|
|
64
|
+
}
|
|
29
65
|
export function lintFlow(flow, known) {
|
|
30
66
|
const warnings = [];
|
|
31
67
|
const agentNames = known ? new Set(known.agents) : null;
|
|
68
|
+
// A frontmatter enum the parser could not read, or a step line whose
|
|
69
|
+
// `[[target]]` is malformed — recorded by parseFlow, reported here at
|
|
70
|
+
// error level so `foldrun check` fails on it, the way a bad step option
|
|
71
|
+
// does. A value nothing can read is never a silent default.
|
|
72
|
+
for (const fp of flow.frontProblems ?? []) {
|
|
73
|
+
warnings.push({ step: null, level: "error", message: fp, detail: "The line is written but its value cannot be read, so a run would use a silent default instead of what the author meant. Write one of the values the option names, or a target in lower case, digits and dashes." });
|
|
74
|
+
}
|
|
32
75
|
// Cron's oldest trap, and an expensive one: when day-of-month AND
|
|
33
76
|
// day-of-week are both restricted they are OR'd, not AND'd — the scheduler
|
|
34
77
|
// does it too, correctly (`domRestricted && dowRestricted` → `domOk ||
|
|
@@ -60,6 +103,20 @@ export function lintFlow(flow, known) {
|
|
|
60
103
|
"a second pair of eyes with `!` (or `approve: true`), or drop the approvers: line.",
|
|
61
104
|
});
|
|
62
105
|
}
|
|
106
|
+
// A zone nobody can read is two faults at once: the cron fires at the
|
|
107
|
+
// wrong hour, and every step's calendar quietly falls through to the level
|
|
108
|
+
// above. Both are invisible in a green run, so `foldrun check` refuses it.
|
|
109
|
+
const zoneProblem = timezoneProblem(flow.timezone);
|
|
110
|
+
if (zoneProblem) {
|
|
111
|
+
warnings.push({
|
|
112
|
+
step: null,
|
|
113
|
+
level: "error",
|
|
114
|
+
message: zoneProblem,
|
|
115
|
+
detail: "This is the zone the schedule fires in AND the calendar every step in this flow works " +
|
|
116
|
+
"to. Unreadable, the schedule falls back to UTC and each step inherits the workspace's " +
|
|
117
|
+
"zone instead of this one.",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
63
120
|
// A deadline on a gate that does not exist reads as a safety net and is not
|
|
64
121
|
// one. Same reasoning, said separately because the fix is different.
|
|
65
122
|
if (flow.approveWithin && !flow.steps.some((s) => s.approve || s.ask)) {
|
|
@@ -153,6 +210,22 @@ export function lintFlow(flow, known) {
|
|
|
153
210
|
"within the key's cap (retry and loop at most 5, max at most 20).",
|
|
154
211
|
});
|
|
155
212
|
}
|
|
213
|
+
// An instruction written over more than one line. It is read in full
|
|
214
|
+
// now, but the parser had to decide where it stopped, and the line
|
|
215
|
+
// below a wrapped instruction is one edit away from being prose. Said
|
|
216
|
+
// out loud so an author who meant to wrap can see it, and one who did
|
|
217
|
+
// not can see what got joined on.
|
|
218
|
+
if (step.wrapped) {
|
|
219
|
+
warnings.push({
|
|
220
|
+
step: i,
|
|
221
|
+
line: step.line,
|
|
222
|
+
message: `"${step.subflow ?? step.agent}" has an instruction spread over more than one line`,
|
|
223
|
+
detail: "A step's instruction ends where the step ends: the indented lines directly under it " +
|
|
224
|
+
"are joined on, and the first blank line, option, heading, bullet or unindented line " +
|
|
225
|
+
"finishes it. Put the whole instruction on the step's own line — one line, however " +
|
|
226
|
+
"long — so what the agent is told does not depend on where the paragraph below begins.",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
156
229
|
// The first group starts with context = null. An instruction that points
|
|
157
230
|
// at earlier output therefore points at nothing.
|
|
158
231
|
if (inFirstGroup && step.instruction && REFERS_BACK.test(step.instruction)) {
|
|
@@ -164,6 +237,29 @@ export function lintFlow(flow, known) {
|
|
|
164
237
|
"invent what it was asked to review rather than fail. Move it to a later group.",
|
|
165
238
|
});
|
|
166
239
|
}
|
|
240
|
+
// `../../storage/…` still works everywhere; `workspace/storage/…` is the
|
|
241
|
+
// spelling that also works in a shell, and the one the docs teach. Said
|
|
242
|
+
// once per step, advisory — never an error on a path that resolves.
|
|
243
|
+
{
|
|
244
|
+
const rowsOf = step.when ? whenRowsPath(step.when) : null;
|
|
245
|
+
const spots = [
|
|
246
|
+
step.eachPath ? ["each: rows of", step.eachPath] : null,
|
|
247
|
+
rowsOf ? ["when: rows of", rowsOf] : null,
|
|
248
|
+
step.verify ? ["verify:", step.verify] : null,
|
|
249
|
+
step.schemaPath ? ["schema:", step.schemaPath] : null,
|
|
250
|
+
step.instruction ? ["the instruction", step.instruction] : null,
|
|
251
|
+
].filter((x) => !!x && /\.\.\/\.\.\/(storage|state)\b/.test(x[1]));
|
|
252
|
+
if (spots.length) {
|
|
253
|
+
warnings.push({
|
|
254
|
+
step: i,
|
|
255
|
+
line: step.line,
|
|
256
|
+
kind: "spelling",
|
|
257
|
+
message: `"${step.subflow ?? step.agent}" writes ../../storage or ../../state in ${spots.map((x) => x[0]).join(", ")}`,
|
|
258
|
+
detail: "It still works. The documented spelling is workspace/storage/… and workspace/state/… — " +
|
|
259
|
+
"the same place, and it also resolves in the shell and in scripts.",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
167
263
|
// A gate with no `preview:` shows only the previous step's reply and
|
|
168
264
|
// whatever files that reply happens to name. The person deciding sees a
|
|
169
265
|
// summary, not the thing — unless the agent remembered.
|
|
@@ -177,6 +273,41 @@ export function lintFlow(flow, known) {
|
|
|
177
273
|
"always sees the document, not a pointer, even when the agent forgets to name it.",
|
|
178
274
|
});
|
|
179
275
|
}
|
|
276
|
+
// A step that can act on the outside world, with nothing to say whether
|
|
277
|
+
// it did. The two answers are a check the runtime can fail on, or a
|
|
278
|
+
// person; either is fine, neither is not. This is the one lint here that
|
|
279
|
+
// is an error: every other structural warning costs a reread, and this
|
|
280
|
+
// one costs a send nobody can take back.
|
|
281
|
+
if (known?.outwardAgents?.includes(step.agent ?? "") && !step.verify && !step.approve && !step.ask) {
|
|
282
|
+
warnings.push({
|
|
283
|
+
step: step.group,
|
|
284
|
+
line: step.line,
|
|
285
|
+
level: "error",
|
|
286
|
+
kind: "outward",
|
|
287
|
+
message: `[[${step.agent}]] can act outside this workspace and this step has no verify: and no gate`,
|
|
288
|
+
detail: "A step that sends, posts, buys or publishes needs one of two things: a `verify:` the " +
|
|
289
|
+
"runtime can fail the step on — a receipt file, a ledger row, the reply's own verdict " +
|
|
290
|
+
"line — or a person, with `!` or `ask:`. Without either, a step that sent nothing and a " +
|
|
291
|
+
"step that sent everything end the same way: reported as done.",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// retry: re-runs a failed step from the start. For a step that sends,
|
|
295
|
+
// a failure can come after the send — the verify: or the JSON check
|
|
296
|
+
// failed on a step whose email went — and the retry sends it again. The
|
|
297
|
+
// runner declines that one retry (see actedThenFailedCheck in runner.ts)
|
|
298
|
+
// but a crash mid-send still reruns; the author should know.
|
|
299
|
+
if (known?.outwardAgents?.includes(step.agent ?? "") && step.retry) {
|
|
300
|
+
warnings.push({
|
|
301
|
+
step: i,
|
|
302
|
+
line: step.line,
|
|
303
|
+
kind: "outward",
|
|
304
|
+
message: `[[${step.agent}]] can act outside this workspace and this step has retry: ${step.retry}`,
|
|
305
|
+
detail: "A retry runs the whole step again, sends included. The runner does not retry a failure " +
|
|
306
|
+
"that came from the check after the step's tools ran, but an attempt that failed mid-way — " +
|
|
307
|
+
"a timeout, a crash, a provider error after the first send — is retried and may repeat " +
|
|
308
|
+
"what it already did. Make the send idempotent (check a ledger before sending), or drop retry:.",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
180
311
|
// `each: items` fans out over DATA, and only an `output: json` step in
|
|
181
312
|
// an earlier group produces any. Without one the step expands to
|
|
182
313
|
// nothing every time, quietly — the exact failure this file exists for.
|
|
@@ -210,6 +341,42 @@ export function lintFlow(flow, known) {
|
|
|
210
341
|
detail: "schema: describes the JSON value an output: json step returns. Without output: json nothing is parsed, so nothing is checked. Add `output: json`.",
|
|
211
342
|
});
|
|
212
343
|
}
|
|
344
|
+
// The step's own agent. `delegate:` and `on-fail:` are checked below and
|
|
345
|
+
// the step's target was not, which is the reference that stops the run
|
|
346
|
+
// rather than degrading it: the runner reaches the step, finds nothing to
|
|
347
|
+
// run and fails it with `agent "<name>" not found in workspace`. A
|
|
348
|
+
// scheduled flow does that at 3am, three times, before anyone looks.
|
|
349
|
+
if (agentNames && step.agent && !step.subflow && !agentNames.has(step.agent)) {
|
|
350
|
+
warnings.push({
|
|
351
|
+
step: i,
|
|
352
|
+
line: step.line,
|
|
353
|
+
level: "error",
|
|
354
|
+
message: `[[${step.agent}]] is not an agent in this workspace`,
|
|
355
|
+
detail: "This step has nothing to run. Every run of this flow fails here. Check the spelling " +
|
|
356
|
+
"against `agents/<name>/agent.md` — the name is the folder's, not the file's heading — " +
|
|
357
|
+
"or add the agent.",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// A `verify:` written as a sentence. Two dialects share the key: a line
|
|
361
|
+
// beginning `contains:`, `not-contains:`, `matches:`, `file:` or
|
|
362
|
+
// `judge:` is an assertion, and ANYTHING else is handed to a shell. So
|
|
363
|
+
// `verify: the reply names the clone it made` runs a program called
|
|
364
|
+
// `the`, exits 127, and fails a step that did its job perfectly — every
|
|
365
|
+
// run, with an error about bash that says nothing about the flow. The
|
|
366
|
+
// fix is one word: the same sentence behind `judge:` is graded by a
|
|
367
|
+
// model, which is what the author meant.
|
|
368
|
+
if (step.verify && looksLikeProse(step.verify)) {
|
|
369
|
+
warnings.push({
|
|
370
|
+
step: i,
|
|
371
|
+
line: step.line,
|
|
372
|
+
level: "error",
|
|
373
|
+
message: `verify: reads as a sentence, so it will be run as a shell command`,
|
|
374
|
+
detail: `\`${step.verify.slice(0, 60)}${step.verify.length > 60 ? "…" : ""}\` has no assertion ` +
|
|
375
|
+
"prefix, so the runtime executes it: the first word becomes the program and the step " +
|
|
376
|
+
"fails with `command not found`. Put `judge: ` in front to have a model grade the " +
|
|
377
|
+
"claim, or write it as a shell command that exits 0 when the claim holds.",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
213
380
|
// A step's OTHER agent references — the ones no parser validates,
|
|
214
381
|
// because they are options rather than the step's target. Both fail
|
|
215
382
|
// silently and at the worst possible moment: a misspelled `delegate:`
|
|
@@ -249,7 +416,8 @@ export function lintFlow(flow, known) {
|
|
|
249
416
|
});
|
|
250
417
|
}
|
|
251
418
|
// `when:` tests the previous results, which the first group doesn't have.
|
|
252
|
-
|
|
419
|
+
// `when: rows of` reads a file instead, which may well exist already.
|
|
420
|
+
if (inFirstGroup && step.when && whenRowsPath(step.when) === null) {
|
|
253
421
|
warnings.push({
|
|
254
422
|
step: i,
|
|
255
423
|
line: step.line,
|
|
@@ -258,6 +426,23 @@ export function lintFlow(flow, known) {
|
|
|
258
426
|
"always be skipped. Move it after the step whose output it depends on.",
|
|
259
427
|
});
|
|
260
428
|
}
|
|
429
|
+
// A `[[flow:x]]` step is spliced in as the other flow's own steps, and
|
|
430
|
+
// only its `?` and its instruction survive the splice. Everything else
|
|
431
|
+
// written under it was dropped without a word — a `when:` that never
|
|
432
|
+
// gated, a `!` that never asked.
|
|
433
|
+
if (step.subflow) {
|
|
434
|
+
const dropped = SUBFLOW_DROPPED.filter(([field]) => step[field] !== undefined).map(([, name]) => name);
|
|
435
|
+
if (dropped.length) {
|
|
436
|
+
warnings.push({
|
|
437
|
+
step: i,
|
|
438
|
+
line: step.line,
|
|
439
|
+
message: `"flow:${step.subflow}" has ${dropped.join(", ")}, which a nested flow ignores`,
|
|
440
|
+
detail: "A nested flow runs as its own steps, with their own options; what is written under the " +
|
|
441
|
+
"[[flow:…]] line is not applied to them. Put the option on the steps inside that flow, " +
|
|
442
|
+
"or make this an agent step.",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
261
446
|
});
|
|
262
447
|
// The same agent twice in one parallel group gets two identical contexts and
|
|
263
448
|
// no way to divide the work — almost always a drag that landed wrong.
|
|
@@ -280,6 +465,43 @@ export function lintFlow(flow, known) {
|
|
|
280
465
|
});
|
|
281
466
|
}
|
|
282
467
|
}
|
|
468
|
+
// A gate parks its whole group, and on approval every step in it starts
|
|
469
|
+
// together. So `5. [[editor]]` beside `5! [[publisher]]` does not mean
|
|
470
|
+
// "edit, then ask, then publish": the editor waits for the approval too,
|
|
471
|
+
// and the publisher runs alongside it, reading files the editor has not
|
|
472
|
+
// written yet. medium-desk's publisher did exactly that on 2026-09-28
|
|
473
|
+
// (run-mukdjeh8-cr66): post.json was still empty when it started.
|
|
474
|
+
const gated = (s) => Boolean(s.approve || s.ask || s.waitFor === "event");
|
|
475
|
+
const gates = steps.filter(gated);
|
|
476
|
+
const others = steps.filter((s) => !gated(s));
|
|
477
|
+
if (gates.length && others.length) {
|
|
478
|
+
const name = (s) => (s.subflow ? `flow:${s.subflow}` : s.agent);
|
|
479
|
+
warnings.push({
|
|
480
|
+
step: flow.steps.indexOf(gates[0]),
|
|
481
|
+
line: gates[0].line,
|
|
482
|
+
message: `"${name(gates[0])}" waits for a person but shares number ${group} with "${others.map(name).join('", "')}"`,
|
|
483
|
+
detail: "Steps with the same number run together. The gate holds all of them until it is " +
|
|
484
|
+
"approved, then they start at once — so the gated step cannot use what the others " +
|
|
485
|
+
`produce, and the approver sees none of it. To run the gated step after them, number it ${group + 1}.`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// A number with nothing under it is harmless to the runner, which only
|
|
490
|
+
// orders groups, but it is almost always one step numbered twice: 3, 3, 4,
|
|
491
|
+
// 5, 5!, 7 is a renumbering that missed the 5! that was meant to be 6.
|
|
492
|
+
const numbers = [...byGroup.keys()].sort((a, b) => a - b);
|
|
493
|
+
for (let i = 1; i < numbers.length; i++) {
|
|
494
|
+
if (numbers[i] - numbers[i - 1] < 2)
|
|
495
|
+
continue;
|
|
496
|
+
const missing = numbers[i - 1] + 1 === numbers[i] - 1 ? `${numbers[i - 1] + 1}` : `${numbers[i - 1] + 1} to ${numbers[i] - 1}`;
|
|
497
|
+
const shared = [...byGroup.entries()].filter(([, s]) => s.length > 1).map(([g]) => g);
|
|
498
|
+
warnings.push({
|
|
499
|
+
step: null,
|
|
500
|
+
message: `step numbers go from ${numbers[i - 1]} to ${numbers[i]}; nothing is numbered ${missing}`,
|
|
501
|
+
detail: "The same number means \"run together\", so a gap usually means a step kept an old number " +
|
|
502
|
+
"after a renumbering" +
|
|
503
|
+
(shared.length ? ` — ${shared.map((g) => `number ${g}`).join(", ")} has more than one step; check whether one of them belongs in the gap.` : "."),
|
|
504
|
+
});
|
|
283
505
|
}
|
|
284
506
|
return warnings;
|
|
285
507
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type FlowStep } from "./store.ts";
|
|
2
|
+
import { type Groups } from "./arrange.ts";
|
|
3
|
+
import { type RemovalImpact } from "./step-removal.ts";
|
|
4
|
+
/** A flow's steps as groups of indices into its parsed steps — what
|
|
5
|
+
* reorderFlowSteps and the arrange helpers take. */
|
|
6
|
+
export declare function flowGroups(steps: FlowStep[]): Groups;
|
|
7
|
+
/** The step options a pattern writes. Everything else in a step stays the
|
|
8
|
+
* author's (and updateFlowStep's) business. */
|
|
9
|
+
export declare const PATTERN_OPTION_KEYS: readonly ["ask", "wait", "on-fail", "each", "max", "loop", "until", "verify", "case", "else", "when", "retry", "approve"];
|
|
10
|
+
export type PatternOptionKey = (typeof PATTERN_OPTION_KEYS)[number];
|
|
11
|
+
export type PatternOptions = Partial<Record<PatternOptionKey, string | null>>;
|
|
12
|
+
export type PatternEdit =
|
|
13
|
+
/** Chain (at.rail: its own group there) or Parallel (at.column: join
|
|
14
|
+
* the group in that column). Rail 0 is before every group. */
|
|
15
|
+
{
|
|
16
|
+
op: "insert";
|
|
17
|
+
target: string;
|
|
18
|
+
subflow?: boolean;
|
|
19
|
+
instruction?: string;
|
|
20
|
+
at: {
|
|
21
|
+
rail: number;
|
|
22
|
+
} | {
|
|
23
|
+
column: number;
|
|
24
|
+
};
|
|
25
|
+
options?: PatternOptions;
|
|
26
|
+
}
|
|
27
|
+
/** Router: a triage step, then one group of case:/else: branches. */
|
|
28
|
+
| {
|
|
29
|
+
op: "router";
|
|
30
|
+
router: string;
|
|
31
|
+
instruction?: string;
|
|
32
|
+
rail: number;
|
|
33
|
+
cases: {
|
|
34
|
+
value: string;
|
|
35
|
+
target: string;
|
|
36
|
+
}[];
|
|
37
|
+
else?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Fan-out, evaluator loop, ask, wait, rescue: options on one step. */
|
|
40
|
+
| {
|
|
41
|
+
op: "options";
|
|
42
|
+
step: number;
|
|
43
|
+
set: PatternOptions;
|
|
44
|
+
}
|
|
45
|
+
/** Approval gate: the `!` marker. */
|
|
46
|
+
| {
|
|
47
|
+
op: "approve";
|
|
48
|
+
step: number;
|
|
49
|
+
on: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Delete one step (removeStep). */
|
|
52
|
+
| {
|
|
53
|
+
op: "remove";
|
|
54
|
+
step: number;
|
|
55
|
+
}
|
|
56
|
+
/** Copy one step beside itself (duplicateStep). */
|
|
57
|
+
| {
|
|
58
|
+
op: "duplicate";
|
|
59
|
+
step: number;
|
|
60
|
+
}
|
|
61
|
+
/** Copied step markdown, placed (pasteSteps). */
|
|
62
|
+
| {
|
|
63
|
+
op: "paste";
|
|
64
|
+
text: string;
|
|
65
|
+
at: {
|
|
66
|
+
rail: number;
|
|
67
|
+
} | {
|
|
68
|
+
column: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** A new step, placed; the file, the new step's index and its column. */
|
|
72
|
+
declare function insertFlowStepLF(raw: string, o: {
|
|
73
|
+
target: string;
|
|
74
|
+
subflow?: boolean;
|
|
75
|
+
instruction?: string;
|
|
76
|
+
at: {
|
|
77
|
+
rail: number;
|
|
78
|
+
} | {
|
|
79
|
+
column: number;
|
|
80
|
+
};
|
|
81
|
+
options?: PatternOptions;
|
|
82
|
+
}): {
|
|
83
|
+
text: string;
|
|
84
|
+
index: number;
|
|
85
|
+
column: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Paste copied steps (stepSource output, one block or several) into a flow.
|
|
89
|
+
* Steps copied with the same group number stay parallel with each other.
|
|
90
|
+
* `{ rail }` places the pasted groups as their own consecutive groups there
|
|
91
|
+
* (0 is before every group); `{ column }` puts every pasted step in that
|
|
92
|
+
* existing group. Groups renumber; prose stays where it was. Returns the
|
|
93
|
+
* file and the pasted steps' indices in it. Refused when the paste does not
|
|
94
|
+
* parse as those steps, or when `check` would gain an error.
|
|
95
|
+
*/
|
|
96
|
+
declare function pasteStepsLF(raw: string, text: string, at: {
|
|
97
|
+
rail: number;
|
|
98
|
+
} | {
|
|
99
|
+
column: number;
|
|
100
|
+
}): {
|
|
101
|
+
text: string;
|
|
102
|
+
indices: number[];
|
|
103
|
+
};
|
|
104
|
+
export declare const setStepOptions: (raw: string, index: number, set: PatternOptions) => string;
|
|
105
|
+
export declare const setStepApprove: (raw: string, index: number, on: boolean) => string;
|
|
106
|
+
export declare function insertFlowStep(...[raw, o]: Parameters<typeof insertFlowStepLF>): ReturnType<typeof insertFlowStepLF>;
|
|
107
|
+
export declare const removeStepImpact: (raw: string, index: number) => RemovalImpact;
|
|
108
|
+
export declare const removeStep: (raw: string, index: number) => string;
|
|
109
|
+
export declare const stepSource: (raw: string, index: number) => string;
|
|
110
|
+
export declare const duplicateStep: (raw: string, index: number) => string;
|
|
111
|
+
export declare function pasteSteps(...[raw, text, at]: Parameters<typeof pasteStepsLF>): ReturnType<typeof pasteStepsLF>;
|
|
112
|
+
export declare const applyPatternEdit: (raw: string, edit: PatternEdit) => string;
|
|
113
|
+
/** The frontmatter list fields an agent's team lives in: whom it delegates
|
|
114
|
+
* to, whom it consults, and what it may do (`ask` among them). */
|
|
115
|
+
export declare const TEAM_LIST_KEYS: readonly ["subagents", "agents", "tools"];
|
|
116
|
+
export type TeamListKey = (typeof TEAM_LIST_KEYS)[number];
|
|
117
|
+
/** The names a frontmatter list holds, read the way the file says them. */
|
|
118
|
+
export declare function frontmatterList(raw: string, key: string): string[];
|
|
119
|
+
/**
|
|
120
|
+
* Add `name` to, or remove it from, the list under `key`, touching only the
|
|
121
|
+
* lines that list is on. Flow style (`key: [a, b]`) stays flow style, block
|
|
122
|
+
* style stays block style, a trailing `# comment` stays put, and the other
|
|
123
|
+
* items keep their spelling. A list emptied by a removal is written
|
|
124
|
+
* `key: []` rather than deleted: the file still says "none", which for
|
|
125
|
+
* `tools:` is not the same as saying nothing.
|
|
126
|
+
*/
|
|
127
|
+
export declare function editFrontmatterList(raw: string, key: string, name: string, action: "add" | "remove"): string;
|
|
128
|
+
/** A YAML scalar for a one-line value: plain when plain reads back as the
|
|
129
|
+
* same string, double-quoted (JSON is valid YAML) when it would not. */
|
|
130
|
+
export declare function yamlScalar(value: string): string;
|
|
131
|
+
/** Set one scalar key — `description:`, `model:`. Replaces its line (and the
|
|
132
|
+
* indented lines of a `>`/`|` block under it); absent, it goes right after
|
|
133
|
+
* `name:`, or last. */
|
|
134
|
+
export declare function setFrontmatterScalar(raw: string, key: string, value: string): string;
|
|
135
|
+
/** A new agent file: the template with the answers to "what is it for"
|
|
136
|
+
* and "which model" written into it, the rest as the template wrote it. */
|
|
137
|
+
export declare function newAgentFile(template: string, o: {
|
|
138
|
+
description?: string;
|
|
139
|
+
model?: string;
|
|
140
|
+
}): string;
|
|
141
|
+
export {};
|