@foldrun/core 2.5.0 → 2.6.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/agent-import.js +2 -2
- package/dist/src/agent-tools.js +2 -2
- package/dist/src/agents-md.d.ts +5 -1
- package/dist/src/agents-md.js +42 -18
- package/dist/src/completions.js +25 -3
- package/dist/src/deploy.js +72 -7
- package/dist/src/evals.d.ts +22 -1
- package/dist/src/evals.js +43 -14
- package/dist/src/flow-lint.d.ts +14 -0
- package/dist/src/flow-lint.js +35 -0
- package/dist/src/flow-patterns.js +29 -14
- package/dist/src/frontmatter-fields.d.ts +27 -0
- package/dist/src/frontmatter-fields.js +51 -3
- package/dist/src/frontmatter-guard.d.ts +19 -0
- package/dist/src/frontmatter-guard.js +67 -0
- package/dist/src/frontmatter-schema.d.ts +2 -2
- package/dist/src/frontmatter-schema.js +30 -2
- package/dist/src/frontmatter-text.d.ts +31 -0
- package/dist/src/frontmatter-text.js +102 -0
- package/dist/src/judge-verdict.d.ts +19 -0
- package/dist/src/judge-verdict.js +54 -0
- package/dist/src/matter.d.ts +34 -0
- package/dist/src/matter.js +104 -0
- package/dist/src/network.d.ts +0 -1
- package/dist/src/network.js +10 -7
- package/dist/src/package.js +9 -2
- package/dist/src/provider-health.js +2 -2
- package/dist/src/run-container.d.ts +9 -0
- package/dist/src/run-container.js +4 -1
- package/dist/src/runner.js +119 -36
- package/dist/src/scan.d.ts +11 -0
- package/dist/src/scan.js +86 -8
- package/dist/src/starter.js +23 -0
- package/dist/src/step-exec.d.ts +10 -2
- package/dist/src/step-exec.js +12 -9
- package/dist/src/store.d.ts +17 -3
- package/dist/src/store.js +14 -10
- package/package.json +1 -1
- package/src/agent-import.ts +2 -2
- package/src/agent-tools.ts +2 -2
- package/src/agents-md.ts +39 -14
- package/src/completions.ts +26 -3
- package/src/deploy.ts +59 -7
- package/src/evals.ts +45 -13
- package/src/flow-lint.ts +36 -0
- package/src/flow-patterns.ts +26 -13
- package/src/frontmatter-fields.ts +74 -3
- package/src/frontmatter-guard.ts +70 -0
- package/src/frontmatter-schema.ts +30 -4
- package/src/frontmatter-text.ts +109 -0
- package/src/judge-verdict.ts +52 -0
- package/src/matter.ts +112 -0
- package/src/network.ts +10 -6
- package/src/package.ts +8 -2
- package/src/provider-health.ts +2 -2
- package/src/run-container.ts +11 -1
- package/src/runner.ts +118 -37
- package/src/scan.ts +86 -8
- package/src/starter.ts +23 -0
- package/src/step-exec.ts +21 -8
- package/src/store.ts +23 -10
package/dist/src/scan.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// The reason is required: an ignore without one ignores nothing and is itself
|
|
27
27
|
// reported, because "why is this allowed?" is the question a reviewer asks.
|
|
28
28
|
import crypto from "node:crypto";
|
|
29
|
-
import matter from "./matter.js";
|
|
29
|
+
import matter, { frontmatterErrorLine, frontmatterErrorText, parseLenient, configMatter } from "./matter.js";
|
|
30
30
|
import { refNames } from "./refs.js";
|
|
31
31
|
import { resolveNetwork } from "./network.js";
|
|
32
32
|
import { frontmatterProblems, schemaKindOf } from "./frontmatter-schema.js";
|
|
@@ -159,6 +159,32 @@ export const SCAN_RULES = [
|
|
|
159
159
|
help: "`effort: extreme`, `size: giant`, `trigger: sometimes`, a cron the scheduler cannot read: each is ignored or falls back, " +
|
|
160
160
|
"so the agent or flow quietly does something else. The message lists what the field takes.",
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
id: "config/conflict",
|
|
164
|
+
category: "config",
|
|
165
|
+
level: "error",
|
|
166
|
+
title: "A field set twice: at the top and inside its job block",
|
|
167
|
+
help: "`limits:` at the top and `limits:` inside `guardrail:` cannot both be meant. The top one would be read and the other " +
|
|
168
|
+
"quietly ignored, so the file is refused until one is removed.",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "config/misplaced",
|
|
172
|
+
category: "config",
|
|
173
|
+
level: "error",
|
|
174
|
+
title: "A field inside the wrong job block",
|
|
175
|
+
help: "Each of the four blocks (governance, guardrail, observability, evaluation) holds its own fields. A setting written in " +
|
|
176
|
+
"another block is not read — a guardrail under governance: guards nothing — so the file is refused until it is moved " +
|
|
177
|
+
"to the block the message names, or to the top. A word foldrun does not read at all is only a note.",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "config/invalid-frontmatter",
|
|
181
|
+
category: "config",
|
|
182
|
+
level: "warn",
|
|
183
|
+
title: "Frontmatter that YAML cannot read",
|
|
184
|
+
help: "A value with a colon followed by a space (`description: the format (Markdown, ::: directives)`) ends the YAML. " +
|
|
185
|
+
"foldrun reads a skill like that by quoting the value, but the dashboard editor and the source API refuse to save it, " +
|
|
186
|
+
"and other tools that read the file may not. Quote the value: `description: \"…\"`.",
|
|
187
|
+
},
|
|
162
188
|
{
|
|
163
189
|
id: "config/missing-field",
|
|
164
190
|
category: "config",
|
|
@@ -544,17 +570,35 @@ export function nearestField(key, known) {
|
|
|
544
570
|
return bestD <= allowed ? best : null;
|
|
545
571
|
}
|
|
546
572
|
function keyLine(lines, key) {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
573
|
+
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
574
|
+
const top = new RegExp(`^${k}\\s*:`);
|
|
575
|
+
const i = lines.findIndex((l) => top.test(l));
|
|
576
|
+
if (i >= 0)
|
|
577
|
+
return i + 1;
|
|
578
|
+
// A key inside one of the four job blocks: indented, under its header.
|
|
579
|
+
const inner = new RegExp(`^\\s+${k}\\s*:`);
|
|
580
|
+
let inBlock = false;
|
|
581
|
+
for (let n = 0; n < lines.length; n++) {
|
|
582
|
+
const l = lines[n];
|
|
583
|
+
if (/^(governance|guardrail|observability|evaluation)\s*:\s*(#.*)?$/.test(l))
|
|
584
|
+
inBlock = true;
|
|
585
|
+
else if (/^\S/.test(l))
|
|
586
|
+
inBlock = false;
|
|
587
|
+
else if (inBlock && inner.test(l))
|
|
588
|
+
return n + 1;
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
550
591
|
}
|
|
551
592
|
/** Read-only: gray-matter caches by content, so two files with the same
|
|
552
593
|
* text share one object, and writing to it would leak between them. */
|
|
553
|
-
function frontOf(content) {
|
|
594
|
+
function frontOf(content, filePath) {
|
|
554
595
|
if (!/^?---\r?\n/.test(content))
|
|
555
596
|
return null;
|
|
556
597
|
try {
|
|
557
|
-
|
|
598
|
+
// An agent, a flow or AGENTS.md reads its job blocks as flat keys, as
|
|
599
|
+
// the runtime does; every other document is read as written.
|
|
600
|
+
const kind = filePath ? schemaKindOf(filePath) : null;
|
|
601
|
+
return (kind === "agent" || kind === "flow" || kind === "agents-md" ? configMatter(content) : matter(content)).data;
|
|
558
602
|
}
|
|
559
603
|
catch {
|
|
560
604
|
return null; // a frontmatter YAML cannot read is check's finding, not ours
|
|
@@ -683,6 +727,14 @@ const VENDORED = /(?:^|\/)(?:node_modules|\.venv|venv|site-packages|__pycache__|
|
|
|
683
727
|
* since a key in a log here is a key in the workspace (seen live on
|
|
684
728
|
* 17 Oct 2026: zero-width characters in a CLI MCP log, reported as hidden
|
|
685
729
|
* text). */
|
|
730
|
+
/** The definition files a deploy refuses outright when their frontmatter does
|
|
731
|
+
* not parse (deploy.ts deployIssues): the workspace's own, by these paths.
|
|
732
|
+
* Everything else the platform reads frontmatter from — skills, and the
|
|
733
|
+
* account library's agents, tools and hooks — is the scanner's to report. */
|
|
734
|
+
export const STRICT_FRONTMATTER_PATH = /^(AGENTS\.md|agents\/[^/]+\/agent\.md|flows\/[^/]+\.md|tools\/[^/]+\.md|tools\/[^/]+\/tool\.md|evals\/[^/]+\.md|hooks\/[^/]+\/hook\.md|\.claude\/agents\/[^/]+\.md)$/;
|
|
735
|
+
/** A skill file in either layout the runtime discovers: <name>/SKILL.md, or
|
|
736
|
+
* the flat <name>.md, under skills/ or .agents/skills/, in the library too. */
|
|
737
|
+
const SKILL_FILE = /(?:^|\/)skills\/(?:[^/]+\/SKILL\.md|[^/]+\.md)$/;
|
|
686
738
|
const RUNTIME_WRITTEN = /(?:^|\/)(?:\.cache|\.npm|\.local|\.pnpm-store|\.yarn|\.foldrun)\//;
|
|
687
739
|
/** Text files only — a PNG read as UTF-8 is noise to every rule here. */
|
|
688
740
|
function isText(f) {
|
|
@@ -740,9 +792,35 @@ export function scanFiles(files, options = {}) {
|
|
|
740
792
|
scanContent(f, lines, push);
|
|
741
793
|
scanSupplyChain(f, lines, push);
|
|
742
794
|
const schema = schemaKindOf(f.path);
|
|
795
|
+
// Frontmatter YAML cannot read. Until 2026-10-21 nothing reported it for
|
|
796
|
+
// a skill or a library file: frontOf below reads it as "no frontmatter"
|
|
797
|
+
// and moves on, and the deploy's own refusal covers only the workspace's
|
|
798
|
+
// definition files. A vaza.ai skill with `description: … (Markdown,
|
|
799
|
+
// frontmatter, ::: directives) …` deployed clean, ran (the runtime quotes
|
|
800
|
+
// the value and retries), and was refused by the source API on its next
|
|
801
|
+
// save.
|
|
802
|
+
if ((schema || SKILL_FILE.test(f.path)) && !STRICT_FRONTMATTER_PATH.test(f.path) && /^\uFEFF?---\r?\n/.test(f.content)) {
|
|
803
|
+
let err = null;
|
|
804
|
+
try {
|
|
805
|
+
matter(f.content);
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
err = e;
|
|
809
|
+
}
|
|
810
|
+
if (err) {
|
|
811
|
+
const isSkill = schema === "skill" || SKILL_FILE.test(f.path);
|
|
812
|
+
const read = parseLenient(f.content);
|
|
813
|
+
const what = isSkill && !read.failed
|
|
814
|
+
? "foldrun reads this skill anyway by quoting the value, but the dashboard editor and the source API refuse to save it, and other skills clients may not load it"
|
|
815
|
+
: isSkill
|
|
816
|
+
? "the runtime reads it as empty, so the skill has no name or description and is never loaded"
|
|
817
|
+
: "the runtime cannot read this file";
|
|
818
|
+
push("config/invalid-frontmatter", f.path, frontmatterErrorLine(err), `config/invalid-frontmatter ${frontmatterErrorText(err)}`, `${frontmatterErrorText(err)} ${what === "the runtime cannot read this file" ? "The runtime cannot read this file" : what.replace(/^foldrun/, "foldrun").replace(/^the/, "The")}.`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
743
821
|
// A skill with no frontmatter at all is the clearest one that never
|
|
744
822
|
// loads: read it as empty, so its required fields are reported.
|
|
745
|
-
const data = schema || docKind(f.path) ? (frontOf(f.content) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
823
|
+
const data = schema || docKind(f.path) ? (frontOf(f.content, f.path) ?? (schema === "skill" && !/^\uFEFF?---\r?\n/.test(f.content) ? {} : null)) : null;
|
|
746
824
|
if (!data)
|
|
747
825
|
continue;
|
|
748
826
|
if (schema) {
|
|
@@ -770,7 +848,7 @@ export function scanFiles(files, options = {}) {
|
|
|
770
848
|
}
|
|
771
849
|
// project.md is the older name the runtime still reads (Codex review, FOL-58).
|
|
772
850
|
const workspaceMd = files.find((f) => f.path === "AGENTS.md") ?? files.find((f) => f.path === "project.md");
|
|
773
|
-
scanAgents(agents, outward, toolDefs, push, { account: options.accountNetwork, workspace: workspaceMd ? frontOf(workspaceMd.content)?.network : undefined });
|
|
851
|
+
scanAgents(agents, outward, toolDefs, push, { account: options.accountNetwork, workspace: workspaceMd ? frontOf(workspaceMd.content, "AGENTS.md")?.network : undefined });
|
|
774
852
|
for (const m of more.values()) {
|
|
775
853
|
findings.push({ rule: m.rule, level: m.level ?? scanRule(m.rule)?.level ?? "warn", file: m.file, message: `and ${m.n} more like this in this file, not listed`, fingerprint: m.hash.digest("hex").slice(0, 16), truncated: true });
|
|
776
854
|
}
|
package/dist/src/starter.js
CHANGED
|
@@ -55,6 +55,22 @@ foldrun_version: "0.1"
|
|
|
55
55
|
#
|
|
56
56
|
# The token is sent as a bearer credential. A gateway that wants it as a key
|
|
57
57
|
# header asks for that by name: headers: { x-api-key: \${THE_SECRET} }.
|
|
58
|
+
#
|
|
59
|
+
# The four jobs of running agents, each a block — or write the same keys at
|
|
60
|
+
# the top; one place or the other, never both:
|
|
61
|
+
#
|
|
62
|
+
# governance: # who answers for it, what it may spend and ship
|
|
63
|
+
# owner: you@example.com
|
|
64
|
+
# budget: 60/month
|
|
65
|
+
# deploy_block: secrets
|
|
66
|
+
# guardrail: # what agents may reach and call
|
|
67
|
+
# limits: {calls: 300, repeat: 5}
|
|
68
|
+
# network: trusted
|
|
69
|
+
# observability: # what is recorded, and who is told
|
|
70
|
+
# notify: {email: you@example.com, events: [failed]}
|
|
71
|
+
# alerts: {failure_rate: 30%}
|
|
72
|
+
# evaluation: # how work is judged
|
|
73
|
+
# judge: {model: fast}
|
|
58
74
|
---
|
|
59
75
|
|
|
60
76
|
# ${account}
|
|
@@ -476,6 +492,13 @@ runtime: # only if scripts need packages
|
|
|
476
492
|
You research a topic and report what you found...
|
|
477
493
|
\`\`\`
|
|
478
494
|
|
|
495
|
+
The fields that do one of the four jobs of running agents may be grouped
|
|
496
|
+
under it — \`governance:\` (owner, budget), \`guardrail:\` (limits, network,
|
|
497
|
+
hooks, disallowedTools, permissionMode, secrets), and in AGENTS.md also
|
|
498
|
+
\`observability:\` (alerts, telemetry, notify, capture) and \`evaluation:\`
|
|
499
|
+
(judge). A block reads exactly as the same keys at the top; a key written
|
|
500
|
+
both ways is refused (\`config/conflict\`). \`foldrun docs four-jobs\`.
|
|
501
|
+
|
|
479
502
|
Only \`name\` and the prose are required. Every other field widens or narrows
|
|
480
503
|
what the agent can reach, and the default is narrow.
|
|
481
504
|
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -104,6 +104,11 @@ export interface ExecOptions {
|
|
|
104
104
|
* `matches: re`, `file: path`, `judge: sentence`) — see checkVerify. */
|
|
105
105
|
verify?: string;
|
|
106
106
|
verifyEnv?: Record<string, string>;
|
|
107
|
+
/** The model `verify: judge:` grades with — the workspace's `judge:` from
|
|
108
|
+
* AGENTS.md, resolved; absent, the fast tier. */
|
|
109
|
+
judgeModel?: string;
|
|
110
|
+
/** And how long it thinks — `judge: {effort}`; absent, the SDK's default. */
|
|
111
|
+
judgeEffort?: Effort;
|
|
107
112
|
/** `output: json` — the reply must carry one JSON value; extracting it is
|
|
108
113
|
* part of finishing the step, and failing to is failing the step. */
|
|
109
114
|
output?: "json";
|
|
@@ -301,14 +306,17 @@ export declare function checkVerify(agentDir: string, verify: string, ctx: {
|
|
|
301
306
|
/** The step's own model environment, for `judge:` — it grades on the
|
|
302
307
|
* fast tier through the same credential the step rode. */
|
|
303
308
|
modelEnv?: Record<string, string | undefined>;
|
|
309
|
+
/** The judge's model and effort (the workspace's `judge:`). */
|
|
310
|
+
judgeModel?: string;
|
|
311
|
+
judgeEffort?: Effort;
|
|
304
312
|
/** Ends a shell check or a judge mid-way: the step's clock ran out, or a
|
|
305
313
|
* person asked the run to stop. */
|
|
306
314
|
signal?: AbortSignal;
|
|
307
315
|
}): Promise<VerifyVerdict>;
|
|
308
316
|
/**
|
|
309
317
|
* A toolless, fast-tier grading call: does the reply satisfy the sentence?
|
|
310
|
-
* Same shape as the eval judge
|
|
311
|
-
*
|
|
318
|
+
* Same shape as the eval judge: reasoning first, then the verdict alone on
|
|
319
|
+
* the last line (judge-verdict.ts says why).
|
|
312
320
|
*/
|
|
313
321
|
export declare function judgePrompt(rubric: string, reply: string, work: string): string;
|
|
314
322
|
/**
|
package/dist/src/step-exec.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// assembles the system prompt and builds MCP servers, then hands this the
|
|
8
8
|
// results. That is what lets the same function run somewhere the vault, the
|
|
9
9
|
// library and the account do not exist.
|
|
10
|
+
import { judgeVerdict, JUDGE_VERDICT_INSTRUCTION } from "./judge-verdict.js";
|
|
10
11
|
import { runHook, runToolCallHooks, matchesTool, hookLine, resolveHooks } from "./hooks.js";
|
|
11
12
|
import fs from "node:fs";
|
|
12
13
|
import os from "node:os";
|
|
@@ -901,6 +902,8 @@ async function executeStepInner(opts, runQuery) {
|
|
|
901
902
|
data,
|
|
902
903
|
modelEnv: opts.env,
|
|
903
904
|
signal: halt.signal,
|
|
905
|
+
...(opts.judgeModel ? { judgeModel: opts.judgeModel } : {}),
|
|
906
|
+
...(opts.judgeEffort ? { judgeEffort: opts.judgeEffort } : {}),
|
|
904
907
|
});
|
|
905
908
|
}
|
|
906
909
|
finally {
|
|
@@ -1045,17 +1048,17 @@ export async function checkVerify(agentDir, verify, ctx) {
|
|
|
1045
1048
|
// GOOD` on a reply that did (fix-desk, run-muwevg72-7mza): the joined
|
|
1046
1049
|
// text opened with the narration before it.
|
|
1047
1050
|
const reply = ctx.conclusion?.trim() ? ctx.conclusion : output;
|
|
1048
|
-
const
|
|
1049
|
-
const ok =
|
|
1050
|
-
return { ok, headline: ok ? "PASS" : "FAIL", detail
|
|
1051
|
+
const said = await judgeReply(value, reply, output, ctx.modelEnv ?? {}, ctx.signal, ctx.judgeModel, ctx.judgeEffort);
|
|
1052
|
+
const { passed: ok, detail } = judgeVerdict(said);
|
|
1053
|
+
return { ok, headline: ok ? "PASS" : "FAIL", detail };
|
|
1051
1054
|
}
|
|
1052
1055
|
}
|
|
1053
1056
|
return { ok: false, headline: `unknown check "${kind}"`, detail: "" };
|
|
1054
1057
|
}
|
|
1055
1058
|
/**
|
|
1056
1059
|
* A toolless, fast-tier grading call: does the reply satisfy the sentence?
|
|
1057
|
-
* Same shape as the eval judge
|
|
1058
|
-
*
|
|
1060
|
+
* Same shape as the eval judge: reasoning first, then the verdict alone on
|
|
1061
|
+
* the last line (judge-verdict.ts says why).
|
|
1059
1062
|
*/
|
|
1060
1063
|
export function judgePrompt(rubric, reply, work) {
|
|
1061
1064
|
const shown = reply.slice(0, 20_000);
|
|
@@ -1063,12 +1066,11 @@ export function judgePrompt(rubric, reply, work) {
|
|
|
1063
1066
|
? `Everything the step wrote while working, for context. "The reply" means only the reply block below, ` +
|
|
1064
1067
|
`never this:\n<work>\n${work.slice(-20_000)}\n</work>\n\n`
|
|
1065
1068
|
: "";
|
|
1066
|
-
return (`You are grading a reply against one requirement.
|
|
1067
|
-
`first word, then one sentence of reason.\n\nRequirement: ${rubric}\n\n` +
|
|
1069
|
+
return (`You are grading a reply against one requirement. ${JUDGE_VERDICT_INSTRUCTION}\n\nRequirement: ${rubric}\n\n` +
|
|
1068
1070
|
context +
|
|
1069
1071
|
`<reply>\n${shown}\n</reply>`);
|
|
1070
1072
|
}
|
|
1071
|
-
async function judgeReply(rubric, reply, work, env, signal) {
|
|
1073
|
+
async function judgeReply(rubric, reply, work, env, signal, model = "haiku", effort) {
|
|
1072
1074
|
const texts = [];
|
|
1073
1075
|
const abortController = new AbortController();
|
|
1074
1076
|
if (signal?.aborted)
|
|
@@ -1078,7 +1080,8 @@ async function judgeReply(rubric, reply, work, env, signal) {
|
|
|
1078
1080
|
const q = query({
|
|
1079
1081
|
prompt: judgePrompt(rubric, reply, work),
|
|
1080
1082
|
options: {
|
|
1081
|
-
model
|
|
1083
|
+
model,
|
|
1084
|
+
...(effort ? { effort } : {}),
|
|
1082
1085
|
systemPrompt: "You grade text against a stated requirement. Be strict and literal.",
|
|
1083
1086
|
tools: [],
|
|
1084
1087
|
settingSources: [],
|
package/dist/src/store.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { BrowserPodOutcome } from "./browser-pod.ts";
|
|
|
3
3
|
import { singleWorkspace } from "./paths.ts";
|
|
4
4
|
import type { TestEffect } from "./test-mode.ts";
|
|
5
5
|
import type { OperatorEvent } from "./operator.ts";
|
|
6
|
+
import { frontmatterErrorText } from "./matter.ts";
|
|
6
7
|
/**
|
|
7
8
|
* The frontmatter of one markdown file, as the runtime reads it. Exported
|
|
8
9
|
* for the CLI: `foldrun probe` used to import gray-matter itself, which is
|
|
@@ -640,9 +641,7 @@ export interface AgentAssets {
|
|
|
640
641
|
}
|
|
641
642
|
export declare function agentAssets(tenant: string, workspace: string, agent: string): AgentAssets;
|
|
642
643
|
export declare function listAgents(tenant: string, workspace: string): AgentInfo[];
|
|
643
|
-
|
|
644
|
-
* needs to fix the file, without the parser's excerpt. */
|
|
645
|
-
export declare function frontmatterErrorText(err: unknown): string;
|
|
644
|
+
export { frontmatterErrorText };
|
|
646
645
|
/** The option keys a step actually has. An indented `word: rest` that is
|
|
647
646
|
* NOT one of these is a line of the instruction — "Warning: do not
|
|
648
647
|
* publish" is prose that happens to contain a colon, and it used to be
|
|
@@ -1069,6 +1068,14 @@ export interface StepAttempt {
|
|
|
1069
1068
|
export declare function sumAttempts(tries: StepAttempt[]): Pick<StepAttempt, "costUsd" | "tokens" | "computeSecs" | "turns">;
|
|
1070
1069
|
export { type TokenUsage, type PromptLayerRecord, tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens, promptLayersChanged, promptCacheNote } from "./tokens.ts";
|
|
1071
1070
|
import { type TokenUsage, type PromptLayerRecord } from "./tokens.ts";
|
|
1071
|
+
/** The marker rescueStep puts in a rescue's instruction; how a rescue record
|
|
1072
|
+
* written before `rescueOf` existed is still recognised. */
|
|
1073
|
+
export declare const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
1074
|
+
/** Is this record an on-fail rescue rather than a step of the flow? */
|
|
1075
|
+
export declare function isRescueRecord(step: {
|
|
1076
|
+
rescueOf?: string;
|
|
1077
|
+
instruction?: string | null;
|
|
1078
|
+
}): boolean;
|
|
1072
1079
|
export interface StepRecord {
|
|
1073
1080
|
agent: string;
|
|
1074
1081
|
instruction: string;
|
|
@@ -1228,6 +1235,13 @@ export interface StepRecord {
|
|
|
1228
1235
|
* actually executed in. Carried steps provide context and are never
|
|
1229
1236
|
* billed again — the original run's ledger line already paid for them. */
|
|
1230
1237
|
carriedFrom?: string;
|
|
1238
|
+
/** Set on an on-fail rescue record (runner.ts rescueStep): the agent of the
|
|
1239
|
+
* failed step it took over. A rescue is not a step of the flow: a resumed
|
|
1240
|
+
* drive must not launch it as one of its group, and a re-run must not
|
|
1241
|
+
* copy it as work (21 Oct 2026: one rerun carried 77 fixers as pending
|
|
1242
|
+
* steps). Records written before this field are recognised by their
|
|
1243
|
+
* instruction's `<rescue>` block instead — see isRescueRecord. */
|
|
1244
|
+
rescueOf?: string;
|
|
1231
1245
|
/**
|
|
1232
1246
|
* Sandbox seconds this step rented, and how many of them were cold start.
|
|
1233
1247
|
* Set only on the isolated path — an in-process step rents nothing, so
|
package/dist/src/store.js
CHANGED
|
@@ -27,7 +27,7 @@ import { recordRevision, registerTreeReader } from "./history.js";
|
|
|
27
27
|
import { dataRoot, singleWorkspace, singleAccountRoot, entriesNoFollow } from "./paths.js";
|
|
28
28
|
import { platform } from "./platform.js";
|
|
29
29
|
import { toLF, asRaw } from "./eol.js";
|
|
30
|
-
import matter from "./matter.js";
|
|
30
|
+
import matter, { frontmatterErrorText, configMatter } from "./matter.js";
|
|
31
31
|
import { readAgentsMdKeys } from "./agents-md.js";
|
|
32
32
|
/**
|
|
33
33
|
* The frontmatter of one markdown file, as the runtime reads it. Exported
|
|
@@ -39,7 +39,7 @@ import { readAgentsMdKeys } from "./agents-md.js";
|
|
|
39
39
|
* the runtime, and there is nothing to fail to import.
|
|
40
40
|
*/
|
|
41
41
|
export function readFrontmatter(file) {
|
|
42
|
-
return
|
|
42
|
+
return configMatter(fs.readFileSync(file, "utf8")).data;
|
|
43
43
|
}
|
|
44
44
|
import { ownToolNames, legacyUseNames, retiredToolNames } from "./tool-names.js";
|
|
45
45
|
import { refNames } from "./refs.js";
|
|
@@ -1126,7 +1126,7 @@ export function listAgents(tenant, workspace) {
|
|
|
1126
1126
|
let data = {};
|
|
1127
1127
|
let frontmatterError;
|
|
1128
1128
|
try {
|
|
1129
|
-
({ data } =
|
|
1129
|
+
({ data } = configMatter(fs.readFileSync(path.join(dir, name, "agent.md"), "utf8")));
|
|
1130
1130
|
}
|
|
1131
1131
|
catch (err) {
|
|
1132
1132
|
frontmatterError = frontmatterErrorText(err);
|
|
@@ -1181,12 +1181,9 @@ export function listAgents(tenant, workspace) {
|
|
|
1181
1181
|
};
|
|
1182
1182
|
});
|
|
1183
1183
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
export
|
|
1187
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1188
|
-
return `frontmatter does not parse — ${msg.split("\n")[0].trim()}. Quote a value that contains a colon followed by a space.`;
|
|
1189
|
-
}
|
|
1184
|
+
// Kept here for callers that import it from the store; it lives in matter.ts
|
|
1185
|
+
// so the scanner can use it without the store.
|
|
1186
|
+
export { frontmatterErrorText };
|
|
1190
1187
|
/** The body of a file whose frontmatter would not parse: what follows the
|
|
1191
1188
|
* closing fence, so a flow's steps can still be read and shown. */
|
|
1192
1189
|
function bodyAfterFrontmatter(raw) {
|
|
@@ -1360,7 +1357,7 @@ export function parseFlow(file, raw) {
|
|
|
1360
1357
|
let content;
|
|
1361
1358
|
let frontError = null;
|
|
1362
1359
|
try {
|
|
1363
|
-
({ data, content } =
|
|
1360
|
+
({ data, content } = configMatter(raw));
|
|
1364
1361
|
}
|
|
1365
1362
|
catch (err) {
|
|
1366
1363
|
frontError = frontmatterErrorText(err);
|
|
@@ -2658,6 +2655,13 @@ export function sumAttempts(tries) {
|
|
|
2658
2655
|
}
|
|
2659
2656
|
export { tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens, promptLayersChanged, promptCacheNote } from "./tokens.js";
|
|
2660
2657
|
import { sumTokens } from "./tokens.js";
|
|
2658
|
+
/** The marker rescueStep puts in a rescue's instruction; how a rescue record
|
|
2659
|
+
* written before `rescueOf` existed is still recognised. */
|
|
2660
|
+
export const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
2661
|
+
/** Is this record an on-fail rescue rather than a step of the flow? */
|
|
2662
|
+
export function isRescueRecord(step) {
|
|
2663
|
+
return !!step.rescueOf || (step.instruction ?? "").includes(RESCUE_MARKER);
|
|
2664
|
+
}
|
|
2661
2665
|
// ---------------------------------------------------------------- captured calls
|
|
2662
2666
|
//
|
|
2663
2667
|
// What each tool call took and gave (FOL-54 step 4), beside the run under
|
package/package.json
CHANGED
package/src/agent-import.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import
|
|
18
|
+
import { configMatter } from "./matter.ts";
|
|
19
19
|
import {
|
|
20
20
|
assertSafeName,
|
|
21
21
|
listWorkspaceFiles,
|
|
@@ -99,7 +99,7 @@ export function importAgent(
|
|
|
99
99
|
let content = readWorkspaceFile(tenant, from, src);
|
|
100
100
|
const rel = src.slice(`agents/${agent}/`.length);
|
|
101
101
|
if (rel === "agent.md") {
|
|
102
|
-
front =
|
|
102
|
+
front = configMatter(content).data as Record<string, unknown>;
|
|
103
103
|
// The folder is the name flows address; a `name:` that disagrees with
|
|
104
104
|
// it is the first thing check would complain about.
|
|
105
105
|
if (name !== agent) content = content.replace(/^(---\r?\n(?:[\s\S]*?\r?\n)?)name:[^\n]*$/m, `$1name: ${name}`);
|
package/src/agent-tools.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
20
|
-
import
|
|
20
|
+
import { configMatter } from "./matter.ts";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
23
23
|
import { resolveModel, resolveEffort, type Effort } from "./store.ts";
|
|
@@ -51,7 +51,7 @@ export function gatherConsults(
|
|
|
51
51
|
missing.push(name);
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
|
-
const parsed =
|
|
54
|
+
const parsed = configMatter(fs.readFileSync(file, "utf8"));
|
|
55
55
|
consults.push({
|
|
56
56
|
name,
|
|
57
57
|
systemPrompt:
|
package/src/agents-md.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import matter from "./matter.ts";
|
|
12
|
+
import matter, { configMatter } from "./matter.ts";
|
|
13
|
+
import { keySpan, dropEmptyBlock } from "./frontmatter-text.ts";
|
|
14
|
+
import { guardEdit } from "./frontmatter-guard.ts";
|
|
15
|
+
import { JOBS, FOLDABLE } from "./frontmatter-fields.ts";
|
|
16
|
+
|
|
17
|
+
export { keySpan };
|
|
13
18
|
|
|
14
19
|
const FRONT = /^(---\r?\n)([\s\S]*?)(\r?\n---)(\r?\n|$)/;
|
|
15
20
|
|
|
@@ -19,7 +24,7 @@ export function readAgentsMdKeys(dir: string): Record<string, unknown> {
|
|
|
19
24
|
const file = path.join(dir, name);
|
|
20
25
|
if (!fs.existsSync(file)) continue;
|
|
21
26
|
try {
|
|
22
|
-
return (
|
|
27
|
+
return (configMatter(fs.readFileSync(file, "utf8")).data ?? {}) as Record<string, unknown>;
|
|
23
28
|
} catch {
|
|
24
29
|
return {};
|
|
25
30
|
}
|
|
@@ -37,8 +42,31 @@ function dumpKey(key: string, value: unknown): string[] {
|
|
|
37
42
|
|
|
38
43
|
/** The text of `content` with each key in `patch` replaced (or removed for
|
|
39
44
|
* null), everything else byte for byte as it was. A file with no
|
|
40
|
-
* frontmatter gets one.
|
|
45
|
+
* frontmatter gets one. Read back before it is returned (frontmatter-guard
|
|
46
|
+
* .ts): an edit that would not parse, would change another key or would
|
|
47
|
+
* double one into its block throws UnsafeEditError (409). */
|
|
41
48
|
export function patchFrontmatter(content: string, patch: Record<string, unknown | null>): string {
|
|
49
|
+
// The guard reads the result folded (matter.ts configMatter), so a patch
|
|
50
|
+
// that writes a whole job block is checked as the keys it holds: each
|
|
51
|
+
// field the block may hold may change, and each one it sets reads back.
|
|
52
|
+
const changed = new Set<string>();
|
|
53
|
+
const expect: Record<string, unknown> = {};
|
|
54
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
55
|
+
changed.add(key);
|
|
56
|
+
const job = JOBS.find((j) => j.name === key)?.name;
|
|
57
|
+
if (!job) {
|
|
58
|
+
expect[key] = value;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const k of FOLDABLE[job]) changed.add(k);
|
|
62
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
63
|
+
for (const [k, v] of Object.entries(value)) if (FOLDABLE[job].has(k)) expect[k] = v;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return guardEdit(content, patchFrontmatterText(content, patch), [...changed], expect);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function patchFrontmatterText(content: string, patch: Record<string, unknown | null>): string {
|
|
42
70
|
const m = content.match(FRONT);
|
|
43
71
|
let open = "---\n";
|
|
44
72
|
let body = "";
|
|
@@ -52,18 +80,15 @@ export function patchFrontmatter(content: string, patch: Record<string, unknown
|
|
|
52
80
|
}
|
|
53
81
|
let lines = body === "" ? [] : body.split("\n");
|
|
54
82
|
for (const [key, value] of Object.entries(patch)) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
// Where it is written now — at the top or inside its job block — is
|
|
84
|
+
// where it is edited; a key written nowhere is added at the top.
|
|
85
|
+
const where = keySpan(lines, key);
|
|
86
|
+
const replacement = value === null || value === undefined ? [] : dumpKey(key, value).map((l) => `${where?.indent ?? ""}${l}`);
|
|
87
|
+
if (!where) lines = [...lines, ...replacement];
|
|
88
|
+
else {
|
|
89
|
+
lines = [...lines.slice(0, where.at), ...replacement, ...lines.slice(where.end)];
|
|
90
|
+
if (!replacement.length && where.block !== null) lines = dropEmptyBlock(lines, where.block);
|
|
63
91
|
}
|
|
64
|
-
const replacement = value === null || value === undefined ? [] : dumpKey(key, value);
|
|
65
|
-
if (at === -1) lines = [...lines, ...replacement];
|
|
66
|
-
else lines = [...lines.slice(0, at), ...replacement, ...lines.slice(end)];
|
|
67
92
|
}
|
|
68
93
|
const front = lines.length ? `${open}${lines.join("\n")}${close}` : "";
|
|
69
94
|
if (!front) return rest.replace(/^(\r?\n)+/, "");
|
package/src/completions.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// renders them.
|
|
11
11
|
|
|
12
12
|
import { PROVIDERS } from "./providers.ts";
|
|
13
|
-
import { FRONTMATTER, type SchemaKind } from "./frontmatter-fields.ts";
|
|
13
|
+
import { FRONTMATTER, JOBS, fieldsOfJob, type SchemaKind } from "./frontmatter-fields.ts";
|
|
14
14
|
|
|
15
15
|
export interface Vocabulary {
|
|
16
16
|
agents: string[];
|
|
@@ -660,8 +660,31 @@ export function completionsAt(
|
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
-
//
|
|
664
|
-
|
|
663
|
+
// 5b. An indented line inside one of the four job blocks (governance,
|
|
664
|
+
// guardrail, observability, evaluation): that job's fields for this
|
|
665
|
+
// kind of file — the block takes nothing else.
|
|
666
|
+
const fmClose = text.indexOf("\n---", text.indexOf("---") + 3);
|
|
667
|
+
const insideFrontmatter = text.startsWith("---") && (fmClose === -1 || cursor <= fmClose);
|
|
668
|
+
const nested = line.match(/^(\s+)([a-zA-Z_]*)$/);
|
|
669
|
+
if (insideFrontmatter && nested) {
|
|
670
|
+
const header = before.slice(0, lineStart).split("\n").reverse().find((l) => /^\S/.test(l));
|
|
671
|
+
const job = JOBS.find((j) => new RegExp(`^${j.name}\\s*:\\s*$`).test(header ?? ""));
|
|
672
|
+
if (job) {
|
|
673
|
+
const schemaKind = /(^|\/)AGENTS\.md$/.test(path) || path === "project.md" ? "agents-md" : SCHEMA_OF[kind];
|
|
674
|
+
const keys = new Set(schemaKind ? fieldsOfJob(schemaKind, job.name) : []);
|
|
675
|
+
const partial = nested[2] ?? "";
|
|
676
|
+
return {
|
|
677
|
+
from: cursor - partial.length,
|
|
678
|
+
query: partial,
|
|
679
|
+
items: filter(fieldsFor(path, kind).filter((c) => keys.has(c.label.split(".")[0]!)), partial),
|
|
680
|
+
title: `${job.title} — ${job.what}`,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 6. An indented option under a flow step — the body's steps, never a
|
|
686
|
+
// line inside the frontmatter.
|
|
687
|
+
if (kind === "flow" && !insideFrontmatter && /^\s+[a-z]*$/.test(line) && line.trim().length >= 0) {
|
|
665
688
|
const partial = line.trim();
|
|
666
689
|
return {
|
|
667
690
|
from: cursor - partial.length,
|