@foldrun/core 2.4.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-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- 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/alerts.d.ts +60 -0
- package/dist/src/alerts.js +161 -0
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/capture.d.ts +23 -0
- package/dist/src/capture.js +80 -0
- package/dist/src/compare.d.ts +57 -0
- package/dist/src/compare.js +116 -0
- package/dist/src/completions.js +26 -3
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +237 -13
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +27 -2
- package/dist/src/evals.js +62 -19
- 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 +60 -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 +33 -2
- package/dist/src/frontmatter-text.d.ts +31 -0
- package/dist/src/frontmatter-text.js +102 -0
- package/dist/src/history.d.ts +4 -0
- package/dist/src/history.js +15 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/insights.d.ts +30 -0
- package/dist/src/insights.js +152 -0
- package/dist/src/issues.d.ts +140 -0
- package/dist/src/issues.js +302 -0
- package/dist/src/judge-verdict.d.ts +19 -0
- package/dist/src/judge-verdict.js +54 -0
- package/dist/src/library.js +1 -1
- package/dist/src/limits.d.ts +8 -0
- package/dist/src/limits.js +36 -5
- package/dist/src/linkable.js +1 -1
- package/dist/src/matter.d.ts +42 -0
- package/dist/src/matter.js +132 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/network.d.ts +12 -1
- package/dist/src/network.js +35 -8
- package/dist/src/notify.d.ts +16 -0
- package/dist/src/notify.js +17 -7
- package/dist/src/okf.js +1 -1
- package/dist/src/online-checks.d.ts +76 -0
- package/dist/src/online-checks.js +432 -0
- package/dist/src/operator.d.ts +277 -0
- package/dist/src/operator.js +176 -2
- package/dist/src/otel-config.d.ts +18 -0
- package/dist/src/otel-config.js +69 -0
- package/dist/src/otel.d.ts +38 -0
- package/dist/src/otel.js +85 -0
- package/dist/src/outcome.d.ts +40 -0
- package/dist/src/outcome.js +77 -0
- package/dist/src/package.js +9 -2
- package/dist/src/platform.d.ts +1 -1
- package/dist/src/prompt-layers.d.ts +30 -0
- package/dist/src/prompt-layers.js +77 -0
- package/dist/src/provider-health.js +2 -2
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +25 -2
- package/dist/src/run-container.js +61 -6
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +471 -102
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +30 -1
- package/dist/src/scan.js +145 -14
- package/dist/src/starter.js +27 -2
- package/dist/src/step-exec.d.ts +39 -3
- package/dist/src/step-exec.js +118 -15
- package/dist/src/store.d.ts +144 -3
- package/dist/src/store.js +150 -7
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +57 -0
- package/dist/src/tool-names.js +1 -1
- package/dist/src/tool-programs.js +1 -1
- package/dist/src/trace-tree.d.ts +40 -0
- package/dist/src/trace-tree.js +156 -0
- package/dist/src/translator.d.ts +8 -0
- package/dist/src/translator.js +7 -2
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/workspace-check.d.ts +18 -0
- package/dist/src/workspace-check.js +34 -0
- package/package.json +2 -2
- package/src/agent-activity.ts +32 -0
- package/src/agent-import.ts +2 -2
- package/src/agent-tools.ts +2 -2
- package/src/agents-md.ts +39 -14
- package/src/alerts.ts +190 -0
- package/src/browser-pod.ts +3 -0
- package/src/capture.ts +88 -0
- package/src/compare.ts +146 -0
- package/src/completions.ts +27 -3
- package/src/deploy.ts +238 -12
- package/src/egress.ts +12 -2
- package/src/evals.ts +64 -19
- package/src/flow-lint.ts +36 -0
- package/src/flow-patterns.ts +26 -13
- package/src/frontmatter-fields.ts +83 -3
- package/src/frontmatter-guard.ts +70 -0
- package/src/frontmatter-schema.ts +33 -4
- package/src/frontmatter-text.ts +109 -0
- package/src/history.ts +13 -0
- package/src/hooks.ts +1 -1
- package/src/insights.ts +174 -0
- package/src/issues.ts +396 -0
- package/src/judge-verdict.ts +52 -0
- package/src/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +145 -0
- package/src/model-credential.ts +45 -0
- package/src/network.ts +35 -7
- package/src/notify.ts +22 -7
- package/src/okf.ts +1 -1
- package/src/online-checks.ts +450 -0
- package/src/operator.ts +213 -3
- package/src/otel-config.ts +67 -0
- package/src/otel.ts +100 -0
- package/src/outcome.ts +90 -0
- package/src/package.ts +8 -2
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +2 -2
- package/src/providers.ts +7 -2
- package/src/run-container.ts +77 -8
- package/src/runner.ts +477 -79
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +153 -14
- package/src/starter.ts +27 -2
- package/src/step-exec.ts +168 -17
- package/src/store.ts +268 -9
- package/src/tokens.ts +68 -0
- package/src/tool-names.ts +1 -1
- package/src/tool-programs.ts +1 -1
- package/src/trace-tree.ts +187 -0
- package/src/translator.ts +13 -2
- package/src/trigger-log.ts +1 -0
- package/src/workspace-check.ts +41 -0
package/dist/src/evals.js
CHANGED
|
@@ -32,14 +32,17 @@
|
|
|
32
32
|
// wrong.
|
|
33
33
|
import { accountModelEnv, withModelEnv } from "./model-env.js";
|
|
34
34
|
import { sdkEnv } from "./step-exec.js";
|
|
35
|
+
import { platformModelEnv } from "./runner.js";
|
|
35
36
|
import fs from "node:fs";
|
|
36
37
|
import path from "node:path";
|
|
37
38
|
import { deployedCommit } from "./deploy.js";
|
|
38
39
|
import { latestRevisionId } from "./history.js";
|
|
39
|
-
import matter from "
|
|
40
|
+
import matter from "./matter.js";
|
|
40
41
|
import { spawn } from "node:child_process";
|
|
42
|
+
import { judgeVerdict, JUDGE_VERDICT_INSTRUCTION, parseJudge } from "./judge-verdict.js";
|
|
43
|
+
import { readAgentsMdKeys } from "./agents-md.js";
|
|
41
44
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
42
|
-
import { workspaceDir, readRun, writeWorkspaceFile, resolveModel, resolveEffort, assertSafeName, } from "./store.js";
|
|
45
|
+
import { workspaceDir, accountDir, readRun, runVerdict, writeWorkspaceFile, resolveModel, resolveEffort, assertSafeName, } from "./store.js";
|
|
43
46
|
import { startFlowRun, loadFlow } from "./runner.js";
|
|
44
47
|
import { platform, platformQueues } from "./platform.js";
|
|
45
48
|
const ASSERTION_TYPES = ["contains", "not-contains", "matches", "file", "run", "judge"];
|
|
@@ -85,7 +88,9 @@ function readScalar(lines, start, inline, indent) {
|
|
|
85
88
|
const first = inline.trim().replace(/^["']|["']$/g, "");
|
|
86
89
|
return { value: [first, ...parts].join(" ").trim(), end: i };
|
|
87
90
|
}
|
|
88
|
-
|
|
91
|
+
/** `defaults`: the workspace's `judge:` (AGENTS.md), used where the eval
|
|
92
|
+
* names no model or effort of its own. */
|
|
93
|
+
export function parseEval(file, raw, defaults = {}) {
|
|
89
94
|
const { data, content } = matter(raw);
|
|
90
95
|
const cases = [];
|
|
91
96
|
let current = null;
|
|
@@ -137,8 +142,8 @@ export function parseEval(file, raw) {
|
|
|
137
142
|
file,
|
|
138
143
|
agent: typeof data.agent === "string" ? data.agent : null,
|
|
139
144
|
flow: typeof data.flow === "string" ? data.flow : null,
|
|
140
|
-
model: resolveModel(data.model ?? "fast"),
|
|
141
|
-
effort: resolveEffort(data.effort ?? "low"),
|
|
145
|
+
model: resolveModel(data.model ?? defaults.model ?? "fast"),
|
|
146
|
+
effort: resolveEffort(data.effort ?? defaults.effort ?? "low"),
|
|
142
147
|
trigger: data.trigger === "manual" ? "manual" : "deploy",
|
|
143
148
|
live: data.live === true,
|
|
144
149
|
inputs: data.inputs === true,
|
|
@@ -153,7 +158,17 @@ export function listEvals(tenant, workspace) {
|
|
|
153
158
|
.readdirSync(dir)
|
|
154
159
|
.filter((f) => f.endsWith(".md"))
|
|
155
160
|
.sort()
|
|
156
|
-
.map((f) => parseEval(f, fs.readFileSync(path.join(dir, f), "utf8")));
|
|
161
|
+
.map((f) => parseEval(f, fs.readFileSync(path.join(dir, f), "utf8"), judgeDefaults(tenant, workspace)));
|
|
162
|
+
}
|
|
163
|
+
/** `judge:` from AGENTS.md, field by field: the workspace's model (or
|
|
164
|
+
* effort), else the account's — so a workspace that sets only an effort
|
|
165
|
+
* keeps the account's model (Codex debate: the form says so). */
|
|
166
|
+
export function judgeDefaults(tenant, workspace) {
|
|
167
|
+
const ws = parseJudge(readAgentsMdKeys(workspaceDir(tenant, workspace)).judge);
|
|
168
|
+
const account = parseJudge(readAgentsMdKeys(accountDir(tenant)).judge);
|
|
169
|
+
const model = ws.model ?? account.model;
|
|
170
|
+
const effort = ws.effort ?? account.effort;
|
|
171
|
+
return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) };
|
|
157
172
|
}
|
|
158
173
|
// ---------- promoting a run ----------
|
|
159
174
|
/** An error a route can turn into the right status without knowing HTTP. */
|
|
@@ -198,16 +213,27 @@ function layoutTask(task) {
|
|
|
198
213
|
* regressions file when there is one, so a flow collects its cases in one
|
|
199
214
|
* place instead of one file per press.
|
|
200
215
|
*/
|
|
201
|
-
export function promoteRunToEval(tenant, workspace, runId,
|
|
216
|
+
export function promoteRunToEval(tenant, workspace, runId,
|
|
217
|
+
/** `should`: what a run that went wrong should have done, in a sentence —
|
|
218
|
+
* the judge's claim for the case (FOL-72 step 10). */
|
|
219
|
+
opts = {}) {
|
|
202
220
|
const run = readRun(tenant, workspace, runId);
|
|
203
221
|
if (!run)
|
|
204
222
|
throw refuse(`run ${runId} not found`, 404);
|
|
205
|
-
if (run.status !== "completed") {
|
|
206
|
-
throw refuse(`only a
|
|
223
|
+
if (run.status !== "completed" && run.status !== "failed") {
|
|
224
|
+
throw refuse(`only a finished run can become an eval — this one is ${run.status}`, 409);
|
|
225
|
+
}
|
|
226
|
+
// A run that went wrong is the test worth having most — but its own answer
|
|
227
|
+
// is the wrong one to hold the next run to, so it needs to be told what
|
|
228
|
+
// was right (FOL-72 step 10).
|
|
229
|
+
const should = oneLine(opts.should ?? "");
|
|
230
|
+
const wentWrong = run.status === "failed" || ["BAD", "BLOCKED"].includes(runVerdict(run) ?? "");
|
|
231
|
+
if (wentWrong && !should && !(opts.expect && opts.expect.length)) {
|
|
232
|
+
throw refuse("this run went wrong, so its answer is not the one to keep — say what it should have done (`should`, a sentence for the judge) or give `expect` lines", 409);
|
|
207
233
|
}
|
|
208
234
|
const produced = run.steps.filter((s) => (s.result ?? "").trim());
|
|
209
235
|
const last = produced[produced.length - 1];
|
|
210
|
-
if (!last)
|
|
236
|
+
if (!last && !should && !(opts.expect && opts.expect.length))
|
|
211
237
|
throw refuse("this run produced no result to hold the next one to", 409);
|
|
212
238
|
if (run.steps.length === 0 || !run.steps[0].instruction?.trim()) {
|
|
213
239
|
throw refuse("this run has no task on record", 409);
|
|
@@ -239,6 +265,9 @@ export function promoteRunToEval(tenant, workspace, runId, opts = {}) {
|
|
|
239
265
|
return `${m[1]}: ${m[2]}`;
|
|
240
266
|
});
|
|
241
267
|
}
|
|
268
|
+
else if (should) {
|
|
269
|
+
expect = [`judge: ${should.slice(0, 400)}`];
|
|
270
|
+
}
|
|
242
271
|
else {
|
|
243
272
|
const conclusion = oneLine(run.summary?.trim() || (last.result ?? "")).slice(0, 200);
|
|
244
273
|
expect = [`judge: reaches the same conclusion as the recorded run: ${conclusion}`];
|
|
@@ -351,12 +380,14 @@ function shell(command, cwd) {
|
|
|
351
380
|
child.on("error", (err) => resolve({ code: null, out: String(err) }));
|
|
352
381
|
});
|
|
353
382
|
}
|
|
354
|
-
// The judge gets the rubric and the output, and
|
|
383
|
+
// The judge gets the rubric and the output, and ends with one word.
|
|
355
384
|
// Constrained output beats asking for a score: "PASS" or "FAIL" can't be
|
|
356
|
-
// misread, where "7/10" needs a threshold nobody agreed on.
|
|
385
|
+
// misread, where "7/10" needs a threshold nobody agreed on. The word comes
|
|
386
|
+
// LAST, after the reasoning: asked for it first, the judge committed before
|
|
387
|
+
// it had looked, then argued the other way — "FAIL … which meets the
|
|
388
|
+
// criterion" (vaza evals, 21 Oct 2026).
|
|
357
389
|
async function judge(rubric, output, model, effort, env) {
|
|
358
|
-
const prompt = `You are grading one piece of output against one criterion.
|
|
359
|
-
`PASS or FAIL on the first line, then one short sentence of reason.\n\n` +
|
|
390
|
+
const prompt = `You are grading one piece of output against one criterion. ${JUDGE_VERDICT_INSTRUCTION}\n\n` +
|
|
360
391
|
`Criterion: ${rubric}\n\n<output>\n${output.slice(0, 20000)}\n</output>`;
|
|
361
392
|
let text = "";
|
|
362
393
|
const q = query({
|
|
@@ -382,7 +413,19 @@ async function judge(rubric, output, model, effort, env) {
|
|
|
382
413
|
}
|
|
383
414
|
return text.trim();
|
|
384
415
|
}
|
|
385
|
-
|
|
416
|
+
/**
|
|
417
|
+
* The judge's environment: this process's, with the platform's model
|
|
418
|
+
* credential read live. The process's own CLAUDE_CODE_OAUTH_TOKEN is the copy
|
|
419
|
+
* it was handed at boot; the box refreshes the login every four hours and the
|
|
420
|
+
* old token is then revoked, so a judge on the boot copy failed with "OAuth
|
|
421
|
+
* token revoked" from the first refresh until the pod restarted, while the
|
|
422
|
+
* agent steps (which read the live file) kept working (2026-10-21).
|
|
423
|
+
*/
|
|
424
|
+
export function judgeProcessEnv() {
|
|
425
|
+
const live = Object.fromEntries(Object.entries(platformModelEnv()).filter(([, v]) => v !== undefined));
|
|
426
|
+
return sdkEnv({ ...process.env, ...live });
|
|
427
|
+
}
|
|
428
|
+
async function checkAssertion(assertion, output, agentDir, model, effort, judgeEnv = () => ({ env: judgeProcessEnv() })) {
|
|
386
429
|
const hay = output.toLowerCase();
|
|
387
430
|
switch (assertion.type) {
|
|
388
431
|
case "contains": {
|
|
@@ -424,9 +467,9 @@ async function checkAssertion(assertion, output, agentDir, model, effort, judgeE
|
|
|
424
467
|
catch (err) {
|
|
425
468
|
return { assertion, passed: false, detail: `judge not run — ${err instanceof Error ? err.message : String(err)}` };
|
|
426
469
|
}
|
|
427
|
-
const
|
|
428
|
-
const passed =
|
|
429
|
-
return { assertion, passed, detail
|
|
470
|
+
const text = await withModelEnv(m, (env) => judge(assertion.value, output, model, effort, env));
|
|
471
|
+
const { passed, detail } = judgeVerdict(text);
|
|
472
|
+
return { assertion, passed, detail };
|
|
430
473
|
}
|
|
431
474
|
}
|
|
432
475
|
}
|
|
@@ -495,7 +538,7 @@ export async function runEval(tenant, workspace, info) {
|
|
|
495
538
|
for (const a of cheap)
|
|
496
539
|
assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort));
|
|
497
540
|
if (assertions.every((r) => r.passed)) {
|
|
498
|
-
const judgeEnv = () => accountModelEnv(tenant, workspace,
|
|
541
|
+
const judgeEnv = () => accountModelEnv(tenant, workspace, judgeProcessEnv);
|
|
499
542
|
for (const a of judges)
|
|
500
543
|
assertions.push(await checkAssertion(a, output, agentDir, info.model, info.effort, judgeEnv));
|
|
501
544
|
}
|
package/dist/src/flow-lint.d.ts
CHANGED
|
@@ -25,4 +25,18 @@ export interface KnownNames {
|
|
|
25
25
|
* rule is skipped rather than guessed at. */
|
|
26
26
|
outwardAgents?: string[];
|
|
27
27
|
}
|
|
28
|
+
/** A `verify:` that is a claim in English rather than a command. Conservative
|
|
29
|
+
* on purpose, the same bargain as REFERS_BACK: it must have no assertion
|
|
30
|
+
* prefix, nothing shell-shaped anywhere in it, and enough words to be a
|
|
31
|
+
* sentence — so a bare `./check.sh` or a one-word command is left alone and
|
|
32
|
+
* the warning only fires where the author plainly wrote prose. */
|
|
33
|
+
/**
|
|
34
|
+
* Why a `matches:` pattern will not compile, or null when it does. The
|
|
35
|
+
* runtime tests it as a JavaScript RegExp with the `i` flag (step-exec.ts,
|
|
36
|
+
* evals.ts), and a pattern that throws fails its check on every run with
|
|
37
|
+
* "invalid regular expression". The usual cause is another dialect: on
|
|
38
|
+
* 2026-10-21 Python's inline `(?m)` broke five eval checks on vaza, and
|
|
39
|
+
* nothing said so before they ran.
|
|
40
|
+
*/
|
|
41
|
+
export declare function regexProblem(pattern: string): string | null;
|
|
28
42
|
export declare function lintFlow(flow: FlowInfo, known?: KnownNames): FlowWarning[];
|
package/dist/src/flow-lint.js
CHANGED
|
@@ -54,6 +54,27 @@ const LOOKS_LIKE_SHELL = /&&|\|\||\s\|\s|\$\(|\$\{|`|\s[<>]\s|\d?>[>&]?\s|^\[|^[
|
|
|
54
54
|
* prefix, nothing shell-shaped anywhere in it, and enough words to be a
|
|
55
55
|
* sentence — so a bare `./check.sh` or a one-word command is left alone and
|
|
56
56
|
* the warning only fires where the author plainly wrote prose. */
|
|
57
|
+
/**
|
|
58
|
+
* Why a `matches:` pattern will not compile, or null when it does. The
|
|
59
|
+
* runtime tests it as a JavaScript RegExp with the `i` flag (step-exec.ts,
|
|
60
|
+
* evals.ts), and a pattern that throws fails its check on every run with
|
|
61
|
+
* "invalid regular expression". The usual cause is another dialect: on
|
|
62
|
+
* 2026-10-21 Python's inline `(?m)` broke five eval checks on vaza, and
|
|
63
|
+
* nothing said so before they ran.
|
|
64
|
+
*/
|
|
65
|
+
export function regexProblem(pattern) {
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(pattern, "i");
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const why = err instanceof Error ? err.message.replace(/^Invalid regular expression: /, "") : String(err);
|
|
72
|
+
const inline = /\(\?[a-z]+\)/.test(pattern)
|
|
73
|
+
? " — JavaScript has no inline flags like (?m); write (^|\\n) for the start of a line"
|
|
74
|
+
: "";
|
|
75
|
+
return `is not a valid JavaScript regular expression (${why})${inline}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
57
78
|
function looksLikeProse(verify) {
|
|
58
79
|
const v = verify.trim();
|
|
59
80
|
if (!v || VERIFY_ASSERTION.test(v))
|
|
@@ -377,6 +398,20 @@ export function lintFlow(flow, known) {
|
|
|
377
398
|
"claim, or write it as a shell command that exits 0 when the claim holds.",
|
|
378
399
|
});
|
|
379
400
|
}
|
|
401
|
+
// A `verify: matches:` the runtime cannot compile fails the step on
|
|
402
|
+
// every run, whatever the agent did.
|
|
403
|
+
const verifyRe = step.verify ? /^matches:\s*(.*)$/.exec(step.verify.trim()) : null;
|
|
404
|
+
// The same normalisation checkVerify applies (step-exec.ts): one quote
|
|
405
|
+
// off each end, so the pattern checked is the pattern that runs.
|
|
406
|
+
const reProblem = verifyRe ? regexProblem(verifyRe[1].trim().replace(/^["']|["']$/g, "")) : null;
|
|
407
|
+
if (reProblem) {
|
|
408
|
+
warnings.push({
|
|
409
|
+
step: i,
|
|
410
|
+
line: step.line,
|
|
411
|
+
message: `verify: matches: ${reProblem}`,
|
|
412
|
+
detail: "The runtime tests matches: as a JavaScript regular expression, case-insensitive. One that does not compile fails this step on every run.",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
380
415
|
// A step's OTHER agent references — the ones no parser validates,
|
|
381
416
|
// because they are options rather than the step's target. Both fail
|
|
382
417
|
// silently and at the worst possible moment: a misspelled `delegate:`
|
|
@@ -24,6 +24,7 @@ import { joinGroup, splitToRail } from "./arrange.js";
|
|
|
24
24
|
import { lintFlow } from "./flow-lint.js";
|
|
25
25
|
import { toLF, asRaw } from "./eol.js";
|
|
26
26
|
import { removalImpact, stepLabel } from "./step-removal.js";
|
|
27
|
+
import { keySpan, InlineBlockError } from "./frontmatter-text.js";
|
|
27
28
|
// ---------- flow files ----------
|
|
28
29
|
/** A flow's steps as groups of indices into its parsed steps — what
|
|
29
30
|
* reorderFlowSteps and the arrange helpers take. */
|
|
@@ -437,14 +438,16 @@ function splitComment(value) {
|
|
|
437
438
|
const m = value.match(/^(.*?)(\s+#.*)$/);
|
|
438
439
|
return m ? { value: m[1], comment: m[2] } : { value, comment: "" };
|
|
439
440
|
}
|
|
441
|
+
/** Where a key is written — at the top or inside its job block
|
|
442
|
+
* (frontmatter-text.ts) — with the text after its colon and its indent. */
|
|
440
443
|
function keyLine(lines, close, key) {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
444
|
+
const where = keySpan(lines.slice(1, close), key);
|
|
445
|
+
if (!where)
|
|
446
|
+
return null;
|
|
447
|
+
const index = where.at + 1;
|
|
448
|
+
// Quoted or not, as keySpan matched it.
|
|
449
|
+
const m = lines[index].slice(where.indent.length).match(new RegExp(`^["']?${key.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")}["']?\\s*:(.*)$`));
|
|
450
|
+
return m ? { index, rest: m[1], indent: where.indent } : null;
|
|
448
451
|
}
|
|
449
452
|
/** Block-style item lines under a key: ` - name`, until anything else. */
|
|
450
453
|
function blockItems(lines, from, close) {
|
|
@@ -467,7 +470,17 @@ export function frontmatterList(raw, key) {
|
|
|
467
470
|
const b = frontBounds(lines);
|
|
468
471
|
if (!b)
|
|
469
472
|
return [];
|
|
470
|
-
|
|
473
|
+
let k;
|
|
474
|
+
try {
|
|
475
|
+
k = keyLine(lines, b.close, key);
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
// A block these text edits do not handle (frontmatter-text.ts) is the
|
|
479
|
+
// writer's to refuse; a read of it is empty, not an error.
|
|
480
|
+
if (err instanceof InlineBlockError)
|
|
481
|
+
return [];
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
471
484
|
if (!k)
|
|
472
485
|
return [];
|
|
473
486
|
const v = splitComment(k.rest).value.trim();
|
|
@@ -491,10 +504,12 @@ export function editFrontmatterList(raw, key, name, action) {
|
|
|
491
504
|
const b = frontBounds(lines);
|
|
492
505
|
if (!b)
|
|
493
506
|
return action === "remove" ? raw : ["---", `${key}: [${name}]`, "---", ...lines].join(eol);
|
|
507
|
+
// Located first, so a key in a block these edits do not handle is refused
|
|
508
|
+
// (InlineBlockError) rather than read as an empty list.
|
|
509
|
+
const k = keyLine(lines, b.close, key);
|
|
494
510
|
const has = frontmatterList(raw, key).includes(name);
|
|
495
511
|
if ((action === "add") === has)
|
|
496
512
|
return raw;
|
|
497
|
-
const k = keyLine(lines, b.close, key);
|
|
498
513
|
if (!k) {
|
|
499
514
|
lines.splice(b.close, 0, `${key}: [${name}]`);
|
|
500
515
|
return lines.join(eol);
|
|
@@ -505,12 +520,12 @@ export function editFrontmatterList(raw, key, name, action) {
|
|
|
505
520
|
// Flow style, or a lone scalar (read as a one-item list): that one line.
|
|
506
521
|
const tokens = v.startsWith("[") ? v.replace(/^\[|\]$/g, "").split(",").map((t) => t.trim()).filter(Boolean) : [v];
|
|
507
522
|
const next = action === "add" ? [...tokens, name] : tokens.filter((t) => itemName(t) !== name);
|
|
508
|
-
lines[k.index] = `${key}: [${next.join(", ")}]${comment}`;
|
|
523
|
+
lines[k.index] = `${k.indent}${key}: [${next.join(", ")}]${comment}`;
|
|
509
524
|
return lines.join(eol);
|
|
510
525
|
}
|
|
511
526
|
const items = blockItems(lines, k.index, b.close);
|
|
512
527
|
if (action === "add") {
|
|
513
|
-
const indent = items.length ? lines[items[0]].match(/^(\s*)-/)[1] :
|
|
528
|
+
const indent = items.length ? lines[items[0]].match(/^(\s*)-/)[1] : `${k.indent} `;
|
|
514
529
|
lines.splice(items.length ? items[items.length - 1] + 1 : k.index + 1, 0, `${indent}- ${name}`);
|
|
515
530
|
return lines.join(eol);
|
|
516
531
|
}
|
|
@@ -518,7 +533,7 @@ export function editFrontmatterList(raw, key, name, action) {
|
|
|
518
533
|
for (const i of [...drop].reverse())
|
|
519
534
|
lines.splice(i, 1);
|
|
520
535
|
if (drop.length === items.length)
|
|
521
|
-
lines[k.index] = `${key}: []${comment}`;
|
|
536
|
+
lines[k.index] = `${k.indent}${key}: []${comment}`;
|
|
522
537
|
return lines.join(eol);
|
|
523
538
|
}
|
|
524
539
|
const YAML_WORDS = /^(true|false|yes|no|on|off|null|~|y|n)$/i;
|
|
@@ -546,10 +561,10 @@ export function setFrontmatterScalar(raw, key, value) {
|
|
|
546
561
|
const { value: v, comment } = splitComment(k.rest);
|
|
547
562
|
let end = k.index + 1;
|
|
548
563
|
if (/^\s*[|>][-+]?\d*\s*$/.test(v) || !v.trim()) {
|
|
549
|
-
while (end < b.close && /^\s
|
|
564
|
+
while (end < b.close && (lines[end].match(/^\s*/)?.[0].length ?? 0) > k.indent.length && /\S/.test(lines[end]))
|
|
550
565
|
end++;
|
|
551
566
|
}
|
|
552
|
-
lines.splice(k.index, end - k.index, `${line}${comment}`);
|
|
567
|
+
lines.splice(k.index, end - k.index, `${k.indent}${line}${comment}`);
|
|
553
568
|
return lines.join(eol);
|
|
554
569
|
}
|
|
555
570
|
const name = keyLine(lines, b.close, "name");
|
|
@@ -15,13 +15,40 @@ export interface FieldSpec {
|
|
|
15
15
|
legacy?: boolean;
|
|
16
16
|
/** One line: what it is. The completion's hint. */
|
|
17
17
|
help: string;
|
|
18
|
+
/** Which of the four jobs it does, when it does one (set from JOB_FIELDS
|
|
19
|
+
* below, not by hand): the block it may also be written in. */
|
|
20
|
+
job?: Job;
|
|
18
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The four jobs of running agents — governance, guardrail, observability,
|
|
24
|
+
* evaluation (the owner's choice of words, 21 Oct 2026). A field that does
|
|
25
|
+
* one may be written flat, as always, or inside a block of that name:
|
|
26
|
+
*
|
|
27
|
+
* guardrail:
|
|
28
|
+
* limits: {repeat: 5}
|
|
29
|
+
* network: [api.example.com]
|
|
30
|
+
*
|
|
31
|
+
* reads exactly as `limits:` and `network:` at the top. matter.ts folds the
|
|
32
|
+
* blocks as it parses, so every reader sees flat keys; check reports a key
|
|
33
|
+
* set both ways (config/conflict) or in the wrong block.
|
|
34
|
+
*/
|
|
35
|
+
export type Job = "governance" | "guardrail" | "observability" | "evaluation";
|
|
36
|
+
export declare const JOBS: readonly {
|
|
37
|
+
name: Job;
|
|
38
|
+
title: string;
|
|
39
|
+
what: string;
|
|
40
|
+
}[];
|
|
19
41
|
export type SchemaKind = "agent" | "agents-md" | "flow" | "tool" | "skill" | "eval" | "hook";
|
|
20
42
|
/** What can start a flow. store.ts re-exports it; parseFlow and the editor
|
|
21
43
|
* read this one. */
|
|
22
44
|
export declare const FLOW_TRIGGERS: readonly ["manual", "schedule", "webhook", "email", "flow", "once", "watch", "storage"];
|
|
23
45
|
/** The effort levels; store.ts re-exports it with the aliases beside. */
|
|
24
46
|
export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
47
|
+
/** The fields of one kind that do this job, in schema order. */
|
|
48
|
+
export declare function fieldsOfJob(kind: SchemaKind, job: Job): string[];
|
|
49
|
+
/** Every key any kind lets a block hold, per job — what matter.ts may lift
|
|
50
|
+
* out of a block without knowing which kind of file it is reading. */
|
|
51
|
+
export declare const FOLDABLE: Readonly<Record<Job, ReadonlySet<string>>>;
|
|
25
52
|
export declare const FRONTMATTER: Readonly<Record<SchemaKind, readonly FieldSpec[]>>;
|
|
26
53
|
/** Which schema a workspace-relative path is read with, or null. */
|
|
27
54
|
export declare function schemaKindOf(p: string): SchemaKind | null;
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
// in the docs.
|
|
16
16
|
//
|
|
17
17
|
// A new field is added here first, then read.
|
|
18
|
+
export const JOBS = [
|
|
19
|
+
{ name: "governance", title: "Governance", what: "who answers for it and what it may spend, decide and ship" },
|
|
20
|
+
{ name: "guardrail", title: "Guardrail", what: "what it may reach and call, and what stops it" },
|
|
21
|
+
{ name: "observability", title: "Observability", what: "what is recorded, and who is told" },
|
|
22
|
+
{ name: "evaluation", title: "Evaluation", what: "how its work is judged" },
|
|
23
|
+
];
|
|
18
24
|
/** What can start a flow. store.ts re-exports it; parseFlow and the editor
|
|
19
25
|
* read this one. */
|
|
20
26
|
export const FLOW_TRIGGERS = ["manual", "schedule", "webhook", "email", "flow", "once", "watch", "storage"];
|
|
@@ -24,6 +30,7 @@ const LOCALE = { validated: true };
|
|
|
24
30
|
const AGENT = [
|
|
25
31
|
{ key: "name", type: "string", help: "kebab-case; the folder is the identity" },
|
|
26
32
|
{ key: "description", type: "string", help: "what it does and when — other agents and people read it" },
|
|
33
|
+
{ key: "owner", type: "string", help: "the person accountable for it — an email or a name; else the workspace's, else the account's" },
|
|
27
34
|
{ key: "model", type: "string", help: "fast | default | max, an alias, or a full model id" },
|
|
28
35
|
{ key: "effort", type: "string", values: EFFORT_LEVELS, help: "how long it thinks" },
|
|
29
36
|
{ key: "size", type: "string", values: ["small", "large", "heavy"], help: "the sandbox it rents; anything else becomes large" },
|
|
@@ -59,6 +66,7 @@ const AGENTS_MD = [
|
|
|
59
66
|
{ key: "mdagent_version", type: ["string", "number"], legacy: true, help: "older spelling of foldrun_version" },
|
|
60
67
|
{ key: "name", type: "string", help: "the workspace's name" },
|
|
61
68
|
{ key: "description", type: "string", help: "shown in the workspace list" },
|
|
69
|
+
{ key: "owner", type: "string", help: "the person accountable for every agent and flow here that names none" },
|
|
62
70
|
{ key: "provider", type: "object", help: "the model endpoint every agent here uses unless it names its own" },
|
|
63
71
|
{ key: "runtime", type: "object", help: "packages for agents that declare none" },
|
|
64
72
|
{ key: "web", type: "object", validated: true, help: "web.browse defaults" },
|
|
@@ -75,17 +83,25 @@ const AGENTS_MD = [
|
|
|
75
83
|
{ key: "budget", type: ["number", "string"], help: "USD per month, or N/day | N/week | N/month" },
|
|
76
84
|
{ key: "priority", type: "string", values: ["high", "normal", "low"], help: "account: where its runs stand in the queue" },
|
|
77
85
|
{ key: "concurrency", type: ["number", "string"], help: "account: runs at once" },
|
|
86
|
+
{ key: "alerts", type: "object", validated: true, help: "thresholds that tell notify: — failure_rate (30%), min_runs, credits_day, run_minutes, stale_days, cost_spike, runs_per_hour, new_host" },
|
|
87
|
+
{ key: "capture", type: "string", values: ["full", "metadata"], help: "what runs record of each tool call: full (its input and output, secrets masked) or metadata (name, size and time only)" },
|
|
88
|
+
{ key: "network_enforce", type: "boolean", help: "true: a site off a step's network: list is refused, not only named in the trace. The account's or the workspace's true is enough" },
|
|
89
|
+
{ key: "telemetry", type: "object", validated: true, help: "send each run as an OpenTelemetry trace — otlp: <https …/v1/traces>, headers: { authorization: ${SECRET} }" },
|
|
90
|
+
{ key: "deploy_block", type: "string", values: ["none", "secrets", "security"], help: "refuse a deploy on scanner findings: secrets (a key written in) or security (any security warning)" },
|
|
91
|
+
{ key: "judge", type: "object", validated: true, help: "the model every judge: here grades with — {model: fast | default | max, effort}; an eval's own model: wins" },
|
|
78
92
|
{ key: "schedule", type: ["string", "object"], validated: true, legacy: true, help: "refused: only a flow has a clock" },
|
|
79
93
|
];
|
|
80
94
|
const FLOW = [
|
|
81
95
|
{ key: "name", type: "string", help: "the flow's name" },
|
|
82
96
|
{ key: "description", type: "string", help: "what it is for" },
|
|
97
|
+
{ key: "owner", type: "string", help: "the person accountable for it — an email or a name; else the workspace's, else the account's" },
|
|
83
98
|
{ key: "trigger", type: "string", values: FLOW_TRIGGERS, help: "what starts it; any other value never fires" },
|
|
84
99
|
{ key: "schedule", type: "string", help: "cron, when trigger: schedule" },
|
|
85
100
|
{ key: "timezone", type: "string", validated: true, help: "the zone its schedule reads in" },
|
|
86
101
|
{ key: "model", type: "string", help: "every step's model unless the step says" },
|
|
87
102
|
{ key: "effort", type: "string", values: EFFORT_LEVELS, help: "every step's effort unless the step says" },
|
|
88
103
|
{ key: "overlap", type: "string", validated: true, help: "skip | queue" },
|
|
104
|
+
{ key: "issues", type: "string", values: ["stay-open", "auto-close"], help: "what a good run does to this flow's open issues: stay-open (the default — a person resolves them) or auto-close" },
|
|
89
105
|
{ key: "priority", type: "string", validated: true, help: "high | normal | low" },
|
|
90
106
|
{ key: "catchup", type: "string", validated: true, help: "last | none" },
|
|
91
107
|
{ key: "on", type: "string", validated: true, help: "trigger: flow — completed | failed | blocked | any" },
|
|
@@ -166,10 +182,51 @@ const SAID_WHAT_IT_IS = [
|
|
|
166
182
|
{ key: "kind", type: "string", validated: true, legacy: true, help: "no longer read — the path says what this is" },
|
|
167
183
|
{ key: "type", type: "string", validated: true, legacy: true, help: "no longer read — the path says what this is" },
|
|
168
184
|
];
|
|
185
|
+
/** Which fields each job holds, per kind of file. One table: the blocks'
|
|
186
|
+
* help, the fold, the editor's completions and check all read it. */
|
|
187
|
+
const JOB_FIELDS = {
|
|
188
|
+
agent: {
|
|
189
|
+
governance: ["owner", "budget"],
|
|
190
|
+
guardrail: ["limits", "network", "hooks", "disallowedTools", "permissionMode", "secrets"],
|
|
191
|
+
},
|
|
192
|
+
flow: {
|
|
193
|
+
governance: ["owner", "approvers", "approve_within", "budget", "priority"],
|
|
194
|
+
guardrail: ["throttle", "debounce", "overlap", "disable_after", "live"],
|
|
195
|
+
observability: ["issues", "sla"],
|
|
196
|
+
},
|
|
197
|
+
"agents-md": {
|
|
198
|
+
governance: ["owner", "budget", "priority", "concurrency", "deploy_block"],
|
|
199
|
+
guardrail: ["limits", "network", "network_enforce", "hooks"],
|
|
200
|
+
observability: ["alerts", "telemetry", "notify", "capture"],
|
|
201
|
+
evaluation: ["judge"],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
/** The fields of one kind that do this job, in schema order. */
|
|
205
|
+
export function fieldsOfJob(kind, job) {
|
|
206
|
+
return [...(JOB_FIELDS[kind]?.[job] ?? [])];
|
|
207
|
+
}
|
|
208
|
+
/** Every key any kind lets a block hold, per job — what matter.ts may lift
|
|
209
|
+
* out of a block without knowing which kind of file it is reading. */
|
|
210
|
+
export const FOLDABLE = Object.fromEntries(JOBS.map(({ name }) => [name, new Set(Object.values(JOB_FIELDS).flatMap((byJob) => byJob?.[name] ?? []))]));
|
|
211
|
+
/** A kind's fields with their jobs set, and a block field per job it has. */
|
|
212
|
+
function withJobs(kind, fields) {
|
|
213
|
+
const byJob = JOB_FIELDS[kind] ?? {};
|
|
214
|
+
const jobOf = new Map();
|
|
215
|
+
for (const [job, keys] of Object.entries(byJob))
|
|
216
|
+
for (const k of keys)
|
|
217
|
+
jobOf.set(k, job);
|
|
218
|
+
const tagged = fields.map((f) => (jobOf.has(f.key) ? { ...f, job: jobOf.get(f.key) } : f));
|
|
219
|
+
const blocks = JOBS.filter((j) => (byJob[j.name]?.length ?? 0) > 0).map((j) => ({
|
|
220
|
+
key: j.name,
|
|
221
|
+
type: "object",
|
|
222
|
+
help: `${j.title} — ${j.what}: ${byJob[j.name].join(", ")}, as one block`,
|
|
223
|
+
}));
|
|
224
|
+
return [...tagged, ...blocks];
|
|
225
|
+
}
|
|
169
226
|
export const FRONTMATTER = {
|
|
170
|
-
agent: [...AGENT, ...SAID_WHAT_IT_IS],
|
|
171
|
-
"agents-md": AGENTS_MD,
|
|
172
|
-
flow: [...FLOW, ...SAID_WHAT_IT_IS],
|
|
227
|
+
agent: withJobs("agent", [...AGENT, ...SAID_WHAT_IT_IS]),
|
|
228
|
+
"agents-md": withJobs("agents-md", AGENTS_MD),
|
|
229
|
+
flow: withJobs("flow", [...FLOW, ...SAID_WHAT_IT_IS]),
|
|
173
230
|
tool: [...TOOL, ...SAID_WHAT_IT_IS.filter((f) => f.key !== "type")],
|
|
174
231
|
skill: [...SKILL, ...SAID_WHAT_IT_IS],
|
|
175
232
|
eval: [...EVAL, ...SAID_WHAT_IT_IS],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A write the guard refused. 409: the file is fine, this edit is not. */
|
|
2
|
+
export declare class UnsafeEditError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
constructor(why: string);
|
|
5
|
+
}
|
|
6
|
+
/** What a changed key must read back as: a value (null: absent), or a test
|
|
7
|
+
* of what it reads as, for a writer whose output form is its own (a canvas
|
|
8
|
+
* list, a scalar YAML may type). */
|
|
9
|
+
export type Expect = unknown | ((value: unknown) => boolean);
|
|
10
|
+
/**
|
|
11
|
+
* `after` if it is `before` with only `changed` keys different and each
|
|
12
|
+
* `expect`ed key reading back as expected; throws UnsafeEditError otherwise.
|
|
13
|
+
* Checked on every call — an edit that changed nothing must still have done
|
|
14
|
+
* what it said — and refused when `before` cannot be read, since nothing
|
|
15
|
+
* then could be compared.
|
|
16
|
+
*/
|
|
17
|
+
export declare function guardEdit(before: string, after: string, changed: readonly string[], expect?: Record<string, Expect>): string;
|
|
18
|
+
/** The front of `text` as a run reads it, or null when it does not parse. */
|
|
19
|
+
export declare function readFront(text: string): Record<string, unknown> | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The last word on every frontmatter edit made as text (agents-md.ts
|
|
2
|
+
// patchFrontmatter, network.ts setNetworkSetting, the canvas's list and
|
|
3
|
+
// scalar edits): the result is read back the way a run reads it, and the
|
|
4
|
+
// edit is refused — the file left as it was — unless it parses, set what it
|
|
5
|
+
// said it set, left everything else as it was, and made no key both flat and
|
|
6
|
+
// in its block. The text writers handle the shapes people write; this makes
|
|
7
|
+
// a shape they do not handle a refusal, never a broken or doubled file
|
|
8
|
+
// (Codex debate, 21 Oct 2026).
|
|
9
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10
|
+
import { configMatter, jobFold } from "./matter.js";
|
|
11
|
+
/** A write the guard refused. 409: the file is fine, this edit is not. */
|
|
12
|
+
export class UnsafeEditError extends Error {
|
|
13
|
+
status = 409;
|
|
14
|
+
constructor(why) {
|
|
15
|
+
super(`not saved — ${why}. Edit the file itself, or write its blocks one key per line`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const front = (text) => configMatter(text).data;
|
|
19
|
+
/**
|
|
20
|
+
* `after` if it is `before` with only `changed` keys different and each
|
|
21
|
+
* `expect`ed key reading back as expected; throws UnsafeEditError otherwise.
|
|
22
|
+
* Checked on every call — an edit that changed nothing must still have done
|
|
23
|
+
* what it said — and refused when `before` cannot be read, since nothing
|
|
24
|
+
* then could be compared.
|
|
25
|
+
*/
|
|
26
|
+
export function guardEdit(before, after, changed, expect = {}) {
|
|
27
|
+
let was;
|
|
28
|
+
try {
|
|
29
|
+
was = front(before);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new UnsafeEditError("the frontmatter does not parse as it is, so this edit could not be checked — fix the file first");
|
|
33
|
+
}
|
|
34
|
+
let now;
|
|
35
|
+
try {
|
|
36
|
+
now = front(after);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new UnsafeEditError(`the result would not parse (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
|
40
|
+
}
|
|
41
|
+
const conflictsBefore = jobFold(was)?.conflicts ?? new Map();
|
|
42
|
+
for (const [k, block] of jobFold(now)?.conflicts ?? new Map()) {
|
|
43
|
+
if (!conflictsBefore.has(k))
|
|
44
|
+
throw new UnsafeEditError(`\`${k}\` would be written both at the top and inside \`${block}:\``);
|
|
45
|
+
}
|
|
46
|
+
for (const [k, want] of Object.entries(expect)) {
|
|
47
|
+
const ok = typeof want === "function" ? want(now[k]) : isDeepStrictEqual(now[k], want === null ? undefined : want);
|
|
48
|
+
if (!ok)
|
|
49
|
+
throw new UnsafeEditError(`\`${k}\` would not read back as written`);
|
|
50
|
+
}
|
|
51
|
+
for (const k of new Set([...Object.keys(was), ...Object.keys(now)])) {
|
|
52
|
+
if (changed.includes(k))
|
|
53
|
+
continue;
|
|
54
|
+
if (!isDeepStrictEqual(was[k], now[k]))
|
|
55
|
+
throw new UnsafeEditError(`\`${k}\` would change too`);
|
|
56
|
+
}
|
|
57
|
+
return after;
|
|
58
|
+
}
|
|
59
|
+
/** The front of `text` as a run reads it, or null when it does not parse. */
|
|
60
|
+
export function readFront(text) {
|
|
61
|
+
try {
|
|
62
|
+
return front(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -2,8 +2,8 @@ import { type SchemaKind } from "./frontmatter-fields.ts";
|
|
|
2
2
|
export { FRONTMATTER, schemaKindOf, type FieldSpec, type FieldType, type SchemaKind } from "./frontmatter-fields.ts";
|
|
3
3
|
export interface FieldProblem {
|
|
4
4
|
key: string;
|
|
5
|
-
rule: "config/unknown-field" | "config/wrong-type" | "config/invalid-value" | "config/missing-field";
|
|
6
|
-
level: "warn" | "info";
|
|
5
|
+
rule: "config/unknown-field" | "config/wrong-type" | "config/invalid-value" | "config/missing-field" | "config/conflict" | "config/misplaced";
|
|
6
|
+
level: "error" | "warn" | "info";
|
|
7
7
|
message: string;
|
|
8
8
|
}
|
|
9
9
|
/**
|