@foldrun/core 1.3.0 → 1.4.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/src/completions.js +23 -4
- package/dist/src/cron.d.ts +17 -0
- package/dist/src/cron.js +107 -0
- package/dist/src/frontmatter-fields.d.ts +27 -0
- package/dist/src/frontmatter-fields.js +197 -0
- package/dist/src/frontmatter-schema.d.ts +15 -0
- package/dist/src/frontmatter-schema.js +156 -0
- package/dist/src/scan.js +42 -35
- package/dist/src/store.d.ts +9 -2
- package/dist/src/store.js +5 -3
- package/package.json +1 -1
- package/src/completions.ts +24 -4
- package/src/cron.ts +120 -0
- package/src/frontmatter-fields.ts +225 -0
- package/src/frontmatter-schema.ts +167 -0
- package/src/scan.ts +43 -35
- package/src/store.ts +5 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Checking frontmatter against frontmatter-fields.ts: the values, not just
|
|
2
|
+
// the names. Node-side (it reads store's parsers); the browser takes the
|
|
3
|
+
// field list alone from frontmatter-fields.ts.
|
|
4
|
+
|
|
5
|
+
import { FRONTMATTER, type FieldType, type SchemaKind } from "./frontmatter-fields.ts";
|
|
6
|
+
import { resolveEffort, parseDuration, parseInstant, parseRate, STEP_OPTION_KEYS } from "./store.ts";
|
|
7
|
+
import { budgetProblem } from "./budget.ts";
|
|
8
|
+
import { cronProblem } from "./cron.ts";
|
|
9
|
+
|
|
10
|
+
export { FRONTMATTER, schemaKindOf, type FieldSpec, type FieldType, type SchemaKind } from "./frontmatter-fields.ts";
|
|
11
|
+
|
|
12
|
+
/** A value for a message: a scalar as written, anything else by its shape.
|
|
13
|
+
* Never String() of a map — its `toString` is whatever the YAML says. */
|
|
14
|
+
function shown(v: unknown): string {
|
|
15
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? String(v) : v instanceof Date ? v.toISOString() : Array.isArray(v) ? "a list" : "a map";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DURATION = (min: number, max: number, unit = "") => (v: unknown) => {
|
|
19
|
+
if (typeof v !== "string" && typeof v !== "number") return `is ${shown(v)}, not a duration — write seconds (900) or 30s, 15m, 4h, 3d`;
|
|
20
|
+
const s = parseDuration(String(v));
|
|
21
|
+
if (s === undefined) return `"${String(v)}" is not a duration — write seconds (900) or 30s, 15m, 4h, 3d`;
|
|
22
|
+
if (s < min || s > max) return `"${String(v)}" is outside ${unit || `${min}s to ${max}s`}`;
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Why a value is wrong, or null — per kind, per field: the rules a list of
|
|
27
|
+
* values cannot say (aliases, durations, cron, budgets). */
|
|
28
|
+
const CHECKS: Partial<Record<SchemaKind, Record<string, (value: unknown) => string | null>>> = {
|
|
29
|
+
"agent": {
|
|
30
|
+
effort: (v) => (resolveEffort(v) ? null : `"${String(v)}" is not an effort — low, medium, high, xhigh or max; the step runs at the model's default`),
|
|
31
|
+
apis: apisProblem,
|
|
32
|
+
budget: (v) => budgetProblem(v, ["run"]),
|
|
33
|
+
ask: (v) => askProblem(v),
|
|
34
|
+
},
|
|
35
|
+
"agents-md": {
|
|
36
|
+
budget: (v) => budgetProblem(v, ["day", "week", "month"]),
|
|
37
|
+
concurrency: (v) => (Number.isInteger(Number(v)) && Number(v) > 0 ? null : `${String(v)} is not a whole number of runs, 1 or more`),
|
|
38
|
+
},
|
|
39
|
+
"flow": {
|
|
40
|
+
schedule: cronProblem,
|
|
41
|
+
effort: (v) => (resolveEffort(v) ? null : `"${String(v)}" is not an effort — low, medium, high, xhigh or max`),
|
|
42
|
+
at: (v) => (parseInstant(v) ? null : `"${String(v)}" is not an ISO 8601 instant — a once flow with it never fires`),
|
|
43
|
+
url: (v) => (/^https?:\/\//i.test(String(v)) ? null : `"${String(v)}" is not an http(s) URL`),
|
|
44
|
+
every: DURATION(1, 30 * 86400, "1s to 30 days"),
|
|
45
|
+
debounce: DURATION(1, 30 * 86400, "1s to 30 days"),
|
|
46
|
+
throttle: DURATION(1, 30 * 86400, "1s to 30 days"),
|
|
47
|
+
sla: DURATION(1, 30 * 86400, "1s to 30 days"),
|
|
48
|
+
approve_within: DURATION(1, 30 * 86400, "1s to 30 days"),
|
|
49
|
+
disable_after: (v) => (Number(v) > 0 && Number(v) <= 100 ? null : `${String(v)} is not a count from 1 to 100; it is ignored`),
|
|
50
|
+
pause_when: (v) => (String(v).length <= 60 ? null : "is longer than 60 characters"),
|
|
51
|
+
budget: (v) => budgetProblem(v, ["run"]),
|
|
52
|
+
},
|
|
53
|
+
"tool": {
|
|
54
|
+
transport: (v) => (["http", "script", "mcp"].includes(String(v).toLowerCase()) ? null : `"${String(v)}" is not a transport — http, script or mcp`),
|
|
55
|
+
methods: methodsProblem,
|
|
56
|
+
rate: (v) => (parseRate(v) ? null : `"${shown(v)}" is not a rate — N/s, N/minute, N/hour, or a number per second, above 0`),
|
|
57
|
+
},
|
|
58
|
+
"eval": {
|
|
59
|
+
effort: (v) => (resolveEffort(v) ? null : `"${String(v)}" is not an effort — low, medium, high, xhigh or max; the judge runs at none`),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface FieldProblem {
|
|
64
|
+
key: string;
|
|
65
|
+
rule: "config/unknown-field" | "config/wrong-type" | "config/invalid-value" | "config/missing-field";
|
|
66
|
+
level: "warn" | "info";
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// YAML reads an unquoted 2026-11-01T09:00:00Z as a Date; the runtime takes
|
|
71
|
+
// it as the string it was written as, so it is one here too.
|
|
72
|
+
const typeOf = (v: unknown): FieldType | "null" =>
|
|
73
|
+
v === null || v === undefined ? "null" : v instanceof Date ? "string" : Array.isArray(v) ? "list" : typeof v === "object" ? "object" : (typeof v as FieldType);
|
|
74
|
+
|
|
75
|
+
/** Where a key that is not read in this kind of file IS read, for a message
|
|
76
|
+
* that says where it belongs. */
|
|
77
|
+
function belongsIn(key: string, kind: SchemaKind): string | null {
|
|
78
|
+
const where: [SchemaKind, string][] = [["agent", "an agent"], ["agents-md", "AGENTS.md"], ["flow", "a flow"], ["tool", "a tool"]];
|
|
79
|
+
const homes = where.filter(([k]) => k !== kind && FRONTMATTER[k].some((f) => f.key === key && !f.legacy)).map(([, label]) => label);
|
|
80
|
+
return homes.length ? homes.join(" or ") : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What is wrong with one file's frontmatter, by its schema: a key nothing
|
|
85
|
+
* reads (a near-miss names the field it meant), a value of the wrong type,
|
|
86
|
+
* a value outside the ones that do something, a required field missing.
|
|
87
|
+
* `near` is the typo matcher; scan.ts passes its own.
|
|
88
|
+
*/
|
|
89
|
+
export function frontmatterProblems(
|
|
90
|
+
kind: SchemaKind,
|
|
91
|
+
data: Record<string, unknown>,
|
|
92
|
+
near: (key: string, known: readonly string[]) => string | null,
|
|
93
|
+
): FieldProblem[] {
|
|
94
|
+
const fields = FRONTMATTER[kind];
|
|
95
|
+
const known = fields.map((f) => f.key);
|
|
96
|
+
const out: FieldProblem[] = [];
|
|
97
|
+
for (const [key, value] of Object.entries(data)) {
|
|
98
|
+
const spec = fields.find((f) => f.key === key);
|
|
99
|
+
const check = CHECKS[kind]?.[key];
|
|
100
|
+
if (!spec) {
|
|
101
|
+
const meant = near(key, known);
|
|
102
|
+
const home = belongsIn(key, kind);
|
|
103
|
+
if (meant && meant !== key) {
|
|
104
|
+
out.push({ key, rule: "config/unknown-field", level: "warn", message: `\`${key}:\` is not a field foldrun reads — did you mean \`${meant}:\`? As written it is ignored` });
|
|
105
|
+
} else if (kind === "agent" && STEP_OPTION_KEYS.has(key)) {
|
|
106
|
+
// `timeout: 3600` on an agent reads like a setting and is not one:
|
|
107
|
+
// it is an option of a flow's step.
|
|
108
|
+
out.push({ key, rule: "config/unknown-field", level: "warn", message: `\`${key}:\` is not read on an agent — it is a step option: indent it under the step in the flow (\` ${key}: ${shown(value)}\`). Here it is ignored` });
|
|
109
|
+
} else if (home) {
|
|
110
|
+
out.push({ key, rule: "config/unknown-field", level: "warn", message: `\`${key}:\` is read in ${home}, not in ${kind === "agents-md" ? "AGENTS.md" : kind === "eval" ? "an eval" : `a ${kind}`} — here it is ignored` });
|
|
111
|
+
} else {
|
|
112
|
+
out.push({ key, rule: "config/unknown-field", level: "info", message: `\`${key}:\` is not a field foldrun reads; it is ignored` });
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (spec.validated || value === null || value === undefined) continue;
|
|
117
|
+
const want = Array.isArray(spec.type) ? spec.type : [spec.type];
|
|
118
|
+
const got = typeOf(value);
|
|
119
|
+
// A string where a list is wanted is a one-item list wherever the
|
|
120
|
+
// runtime splits it, and the list types above say so.
|
|
121
|
+
if (!want.includes(got as FieldType)) {
|
|
122
|
+
out.push({ key, rule: "config/wrong-type", level: "warn", message: `\`${key}:\` should be ${want.map((t) => (t === "list" ? "a list" : t === "object" ? "a map of keys" : `a ${t}`)).join(" or ")}, not ${got === "list" ? "a list" : got === "object" ? "a map" : `a ${got}`} — as written it is not read` });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// A field's own check knows its aliases (`effort: quick` is low), so it
|
|
126
|
+
// decides alone; `values` alone is for a closed set with none.
|
|
127
|
+
// A check reads customer YAML: a value shaped to break it (a map whose
|
|
128
|
+
// `toString` is a string) is a finding, never a crash of the whole scan.
|
|
129
|
+
let checked: string | null = null;
|
|
130
|
+
try {
|
|
131
|
+
checked = check ? check(value) : null;
|
|
132
|
+
} catch {
|
|
133
|
+
checked = "holds a value that cannot be read";
|
|
134
|
+
}
|
|
135
|
+
const why = check
|
|
136
|
+
? checked
|
|
137
|
+
: spec.values && typeof value === "string" && !spec.values.includes(value.trim())
|
|
138
|
+
? `"${value}" is not one of ${spec.values.join(", ")} — as written it is ignored`
|
|
139
|
+
: null;
|
|
140
|
+
if (why) out.push({ key, rule: "config/invalid-value", level: "warn", message: `\`${key}:\` ${why.replace(/^\w+: /, "")}` });
|
|
141
|
+
}
|
|
142
|
+
if (kind === "skill" && !(typeof data.description === "string" && data.description.trim())) {
|
|
143
|
+
out.push({ key: "description", rule: "config/missing-field", level: "warn", message: "no `description:` — a skill without one is never loaded; the model chooses a skill by it" });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function askProblem(v: unknown): string | null {
|
|
149
|
+
const t = (v as { timeout?: unknown } | null)?.timeout;
|
|
150
|
+
return t === undefined ? null : DURATION(30, 86400, "30s to 24h")(t);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
154
|
+
function methodsProblem(v: unknown): string | null {
|
|
155
|
+
const list = Array.isArray(v) ? v.map(String) : String(v).split(",");
|
|
156
|
+
const bad = list.map((m) => m.trim()).filter((m) => m && !METHODS.includes(m.toUpperCase()));
|
|
157
|
+
return bad.length ? `${bad.join(", ")} — not a method; it is dropped. GET, POST, PUT, PATCH or DELETE` : null;
|
|
158
|
+
}
|
|
159
|
+
function apisProblem(v: unknown): string | null {
|
|
160
|
+
for (const api of Array.isArray(v) ? v : []) {
|
|
161
|
+
if (!api || typeof api !== "object") return "each entry is {name, base, …}";
|
|
162
|
+
const m = (api as { methods?: unknown }).methods;
|
|
163
|
+
const p = m === undefined ? null : methodsProblem(m);
|
|
164
|
+
if (p) return `${String((api as { name?: unknown }).name ?? "an api")}: methods: ${p}`;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
package/src/scan.ts
CHANGED
|
@@ -30,6 +30,7 @@ import crypto from "node:crypto";
|
|
|
30
30
|
import matter from "gray-matter";
|
|
31
31
|
import { refNames } from "./refs.ts";
|
|
32
32
|
import { resolveNetwork } from "./network.ts";
|
|
33
|
+
import { frontmatterProblems, schemaKindOf } from "./frontmatter-schema.ts";
|
|
33
34
|
|
|
34
35
|
export type ScanLevel = "error" | "warn" | "info";
|
|
35
36
|
export type ScanCategory = "secret" | "security" | "content" | "supply-chain" | "config";
|
|
@@ -176,10 +177,34 @@ export const SCAN_RULES: readonly ScanRule[] = [
|
|
|
176
177
|
id: "config/unknown-field",
|
|
177
178
|
category: "config",
|
|
178
179
|
level: "warn",
|
|
179
|
-
title: "A frontmatter field foldrun does not read
|
|
180
|
+
title: "A frontmatter field foldrun does not read here",
|
|
180
181
|
help:
|
|
181
182
|
"A misspelt field is not an error to YAML, so it is silently ignored: `tolls: [web]` leaves the agent with no tools. " +
|
|
182
|
-
"
|
|
183
|
+
"A near-miss names the field it meant; a field read in another kind of file says where it belongs (warnings). " +
|
|
184
|
+
"A field like nothing foldrun reads is a note, since another tool may own it.",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "config/wrong-type",
|
|
188
|
+
category: "config",
|
|
189
|
+
level: "warn",
|
|
190
|
+
title: "A frontmatter field holding the wrong kind of value",
|
|
191
|
+
help: "A list where one name was meant, a map where a word was meant: the runtime does not read it. The message says what the field takes.",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "config/invalid-value",
|
|
195
|
+
category: "config",
|
|
196
|
+
level: "warn",
|
|
197
|
+
title: "A frontmatter value that does nothing",
|
|
198
|
+
help:
|
|
199
|
+
"`effort: extreme`, `size: giant`, `trigger: sometimes`, a cron the scheduler cannot read: each is ignored or falls back, " +
|
|
200
|
+
"so the agent or flow quietly does something else. The message lists what the field takes.",
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "config/missing-field",
|
|
204
|
+
category: "config",
|
|
205
|
+
level: "warn",
|
|
206
|
+
title: "A required frontmatter field is missing",
|
|
207
|
+
help: "A skill with no description is never loaded: the model chooses skills by their descriptions.",
|
|
183
208
|
},
|
|
184
209
|
{
|
|
185
210
|
id: "check/ignore-needs-reason",
|
|
@@ -510,29 +535,6 @@ function unpinned(pkg: string): boolean {
|
|
|
510
535
|
|
|
511
536
|
// ── frontmatter ────────────────────────────────────────────────────────────
|
|
512
537
|
|
|
513
|
-
/** What foldrun reads in an agent's frontmatter — the fields documented in
|
|
514
|
-
* agents.md, the Claude Code subagent fields an import carries, and the
|
|
515
|
-
* older spellings check already teaches about. Used only to suggest the
|
|
516
|
-
* field a typo meant: an unknown key that is nothing like any of these is
|
|
517
|
-
* left alone, since another tool may own it. */
|
|
518
|
-
const AGENT_FIELDS = [
|
|
519
|
-
"name", "description", "model", "effort", "size", "tools", "disallowedTools", "skills", "hooks", "scripts",
|
|
520
|
-
"apis", "mcpServers", "agents", "subagents", "secrets", "limits", "network", "provider", "permissionMode",
|
|
521
|
-
"runtime", "ask", "web", "budget", "timezone", "language", "region", "currency", "units", "schedule", "color",
|
|
522
|
-
"memory", "foldrun_version", "use", "kind", "type", "notify", "triggers", "inputs", "outputs",
|
|
523
|
-
];
|
|
524
|
-
const FLOW_FIELDS = [
|
|
525
|
-
"name", "description", "trigger", "schedule", "timezone", "language", "region", "currency", "units", "model",
|
|
526
|
-
"effort", "overlap", "priority", "throttle", "debounce", "sla", "catchup", "pause_when", "disable_after",
|
|
527
|
-
"approvers", "approve_within", "notify", "at", "every", "budget", "limits", "size", "hooks", "inputs",
|
|
528
|
-
"webhook", "signing_secret", "signature", "idempotency", "filter", "after", "tags", "network", "secrets",
|
|
529
|
-
];
|
|
530
|
-
const TOOL_FIELDS = [
|
|
531
|
-
"name", "description", "transport", "type", "base", "methods", "headers", "query", "auth", "run", "args",
|
|
532
|
-
"command", "url", "env", "outward", "runtime", "secrets", "network", "timeout", "spec", "operations", "body",
|
|
533
|
-
"oauth", "limits", "input", "output", "schema", "kind",
|
|
534
|
-
];
|
|
535
|
-
|
|
536
538
|
function editDistance(a: string, b: string): number {
|
|
537
539
|
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array<number>(b.length).fill(0)]);
|
|
538
540
|
for (let j = 1; j <= b.length; j++) d[0]![j] = j;
|
|
@@ -721,7 +723,7 @@ function ignored(ig: Ignores, rule: string, line?: number): boolean {
|
|
|
721
723
|
|
|
722
724
|
// ── the scan ───────────────────────────────────────────────────────────────
|
|
723
725
|
|
|
724
|
-
type Push = (rule: string, file: string, line: number | undefined, text: string, message: string) => void;
|
|
726
|
+
type Push = (rule: string, file: string, line: number | undefined, text: string, message: string, level?: ScanLevel) => void;
|
|
725
727
|
|
|
726
728
|
/** Installed and built code: someone else's files, and a crypto library's
|
|
727
729
|
* constants are not this workspace's keys. */
|
|
@@ -743,14 +745,14 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
743
745
|
const seen = new Set<string>();
|
|
744
746
|
const ignoresByFile = new Map<string, Ignores>();
|
|
745
747
|
|
|
746
|
-
const push: Push = (rule, file, line, text, message) => {
|
|
748
|
+
const push: Push = (rule, file, line, text, message, level) => {
|
|
747
749
|
if (!enabled(rule)) return;
|
|
748
750
|
const ig = ignoresByFile.get(file);
|
|
749
751
|
if (ig && ignored(ig, rule, line)) return;
|
|
750
752
|
const key = `${rule}\0${file}\0${line ?? ""}\0${message}`;
|
|
751
753
|
if (seen.has(key)) return;
|
|
752
754
|
seen.add(key);
|
|
753
|
-
findings.push({ rule, level: scanRule(rule)?.level ?? "warn", file, ...(line ? { line } : {}), message, fingerprint: findingFingerprint(rule, file, text) });
|
|
755
|
+
findings.push({ rule, level: level ?? scanRule(rule)?.level ?? "warn", file, ...(line ? { line } : {}), message, fingerprint: findingFingerprint(rule, file, text) });
|
|
754
756
|
};
|
|
755
757
|
|
|
756
758
|
const outward = new Set(options.outwardTools ?? []);
|
|
@@ -764,15 +766,21 @@ export function scanFiles(files: ScanFile[], options: ScanOptions = {}): ScanFin
|
|
|
764
766
|
scanContent(f, lines, push);
|
|
765
767
|
scanSupplyChain(f, lines, push);
|
|
766
768
|
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
769
|
+
const schema = schemaKindOf(f.path);
|
|
770
|
+
// A skill with no frontmatter at all is the clearest one that never
|
|
771
|
+
// loads: read it as empty, so its required fields are reported.
|
|
772
|
+
const data = schema || docKind(f.path) ? (frontOf(f.content) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
770
773
|
if (!data) continue;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
774
|
+
if (schema) {
|
|
775
|
+
for (const p of frontmatterProblems(schema, data, nearestField)) {
|
|
776
|
+
// Fingerprint the key's own line, not the parsed value: a YAML alias
|
|
777
|
+
// can make the value circular, and JSON of it would throw.
|
|
778
|
+
const at = keyLine(lines, p.key);
|
|
779
|
+
push(p.rule, f.path, at, `${p.rule} ${at ? lines[at - 1] : p.key}`, p.message, p.level);
|
|
780
|
+
}
|
|
775
781
|
}
|
|
782
|
+
const kind = docKind(f.path);
|
|
783
|
+
if (!kind) continue;
|
|
776
784
|
if (kind === "agent") {
|
|
777
785
|
const rel = f.path.replace(/^library\//, "");
|
|
778
786
|
const name = /^agents\/([^/]+)\//.exec(rel)?.[1] ?? rel.replace(/^.*\/|\.md$/g, "");
|
package/src/store.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// An account is a named directory (per OKF: tenancy is a hosting concern).
|
|
22
22
|
|
|
23
23
|
import { expandPlatformVars } from "./host-env.ts";
|
|
24
|
+
import { EFFORT_LEVELS, FLOW_TRIGGERS } from "./frontmatter-fields.ts";
|
|
24
25
|
import type { BrowserPodOutcome } from "./browser-pod.ts";
|
|
25
26
|
import fs from "node:fs";
|
|
26
27
|
import path from "node:path";
|
|
@@ -1082,7 +1083,7 @@ export function resolveModel(value: unknown): string {
|
|
|
1082
1083
|
// model is which brain, effort is how long it thinks before answering. The
|
|
1083
1084
|
// five levels are the SDK's own union — we neither invent our own scale nor
|
|
1084
1085
|
// narrow theirs, so a level added upstream is one line here.
|
|
1085
|
-
export
|
|
1086
|
+
export { EFFORT_LEVELS } from "./frontmatter-fields.ts";
|
|
1086
1087
|
export type Effort = (typeof EFFORT_LEVELS)[number];
|
|
1087
1088
|
|
|
1088
1089
|
const EFFORT_ALIASES: Record<string, Effort> = {
|
|
@@ -1464,7 +1465,8 @@ export interface FlowInfo {
|
|
|
1464
1465
|
steps: FlowStep[];
|
|
1465
1466
|
}
|
|
1466
1467
|
|
|
1467
|
-
|
|
1468
|
+
// The list lives with every other frontmatter field (frontmatter-fields.ts).
|
|
1469
|
+
export { FLOW_TRIGGERS } from "./frontmatter-fields.ts";
|
|
1468
1470
|
export type FlowTrigger = (typeof FLOW_TRIGGERS)[number];
|
|
1469
1471
|
|
|
1470
1472
|
// Skills and memory files belonging to one agent — for the graph view and
|
|
@@ -1624,7 +1626,7 @@ const OPTION_RE = /^(\s+)([a-z_-]+):(.*)$/;
|
|
|
1624
1626
|
* publish" is prose that happens to contain a colon, and it used to be
|
|
1625
1627
|
* swallowed as an unknown option, silently, along with any typo of a real
|
|
1626
1628
|
* key. The consistency suite reads this list against the docs. */
|
|
1627
|
-
const STEP_OPTION_KEYS = new Set([
|
|
1629
|
+
export const STEP_OPTION_KEYS: ReadonlySet<string> = new Set([
|
|
1628
1630
|
"approve", "ask", "case", "delegate", "each", "effort", "else", "limits", "loop", "max", "max_turns", "model",
|
|
1629
1631
|
"on-fail", "onfail", "output", "parallel", "preview", "retry", "schema", "timeout", "until", "verify", "wait", "when",
|
|
1630
1632
|
]);
|