@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.
Files changed (149) hide show
  1. package/dist/src/agent-activity.d.ts +5 -0
  2. package/dist/src/agent-activity.js +31 -0
  3. package/dist/src/agent-import.js +2 -2
  4. package/dist/src/agent-tools.js +2 -2
  5. package/dist/src/agents-md.d.ts +5 -1
  6. package/dist/src/agents-md.js +42 -18
  7. package/dist/src/alerts.d.ts +60 -0
  8. package/dist/src/alerts.js +161 -0
  9. package/dist/src/browser-pod.js +4 -0
  10. package/dist/src/capture.d.ts +23 -0
  11. package/dist/src/capture.js +80 -0
  12. package/dist/src/compare.d.ts +57 -0
  13. package/dist/src/compare.js +116 -0
  14. package/dist/src/completions.js +26 -3
  15. package/dist/src/deploy.d.ts +36 -1
  16. package/dist/src/deploy.js +237 -13
  17. package/dist/src/egress.d.ts +16 -1
  18. package/dist/src/evals.d.ts +27 -2
  19. package/dist/src/evals.js +62 -19
  20. package/dist/src/flow-lint.d.ts +14 -0
  21. package/dist/src/flow-lint.js +35 -0
  22. package/dist/src/flow-patterns.js +29 -14
  23. package/dist/src/frontmatter-fields.d.ts +27 -0
  24. package/dist/src/frontmatter-fields.js +60 -3
  25. package/dist/src/frontmatter-guard.d.ts +19 -0
  26. package/dist/src/frontmatter-guard.js +67 -0
  27. package/dist/src/frontmatter-schema.d.ts +2 -2
  28. package/dist/src/frontmatter-schema.js +33 -2
  29. package/dist/src/frontmatter-text.d.ts +31 -0
  30. package/dist/src/frontmatter-text.js +102 -0
  31. package/dist/src/history.d.ts +4 -0
  32. package/dist/src/history.js +15 -0
  33. package/dist/src/hooks.js +1 -1
  34. package/dist/src/insights.d.ts +30 -0
  35. package/dist/src/insights.js +152 -0
  36. package/dist/src/issues.d.ts +140 -0
  37. package/dist/src/issues.js +302 -0
  38. package/dist/src/judge-verdict.d.ts +19 -0
  39. package/dist/src/judge-verdict.js +54 -0
  40. package/dist/src/library.js +1 -1
  41. package/dist/src/limits.d.ts +8 -0
  42. package/dist/src/limits.js +36 -5
  43. package/dist/src/linkable.js +1 -1
  44. package/dist/src/matter.d.ts +42 -0
  45. package/dist/src/matter.js +132 -0
  46. package/dist/src/model-credential.d.ts +14 -0
  47. package/dist/src/model-credential.js +40 -0
  48. package/dist/src/network.d.ts +12 -1
  49. package/dist/src/network.js +35 -8
  50. package/dist/src/notify.d.ts +16 -0
  51. package/dist/src/notify.js +17 -7
  52. package/dist/src/okf.js +1 -1
  53. package/dist/src/online-checks.d.ts +76 -0
  54. package/dist/src/online-checks.js +432 -0
  55. package/dist/src/operator.d.ts +277 -0
  56. package/dist/src/operator.js +176 -2
  57. package/dist/src/otel-config.d.ts +18 -0
  58. package/dist/src/otel-config.js +69 -0
  59. package/dist/src/otel.d.ts +38 -0
  60. package/dist/src/otel.js +85 -0
  61. package/dist/src/outcome.d.ts +40 -0
  62. package/dist/src/outcome.js +77 -0
  63. package/dist/src/package.js +9 -2
  64. package/dist/src/platform.d.ts +1 -1
  65. package/dist/src/prompt-layers.d.ts +30 -0
  66. package/dist/src/prompt-layers.js +77 -0
  67. package/dist/src/provider-health.js +2 -2
  68. package/dist/src/providers.d.ts +5 -0
  69. package/dist/src/providers.js +2 -2
  70. package/dist/src/run-container.d.ts +25 -2
  71. package/dist/src/run-container.js +61 -6
  72. package/dist/src/runner.d.ts +53 -2
  73. package/dist/src/runner.js +471 -102
  74. package/dist/src/runs-filter.d.ts +48 -0
  75. package/dist/src/runs-filter.js +73 -0
  76. package/dist/src/scan.d.ts +30 -1
  77. package/dist/src/scan.js +145 -14
  78. package/dist/src/starter.js +27 -2
  79. package/dist/src/step-exec.d.ts +39 -3
  80. package/dist/src/step-exec.js +118 -15
  81. package/dist/src/store.d.ts +144 -3
  82. package/dist/src/store.js +150 -7
  83. package/dist/src/tokens.d.ts +38 -0
  84. package/dist/src/tokens.js +57 -0
  85. package/dist/src/tool-names.js +1 -1
  86. package/dist/src/tool-programs.js +1 -1
  87. package/dist/src/trace-tree.d.ts +40 -0
  88. package/dist/src/trace-tree.js +156 -0
  89. package/dist/src/translator.d.ts +8 -0
  90. package/dist/src/translator.js +7 -2
  91. package/dist/src/trigger-log.d.ts +1 -1
  92. package/dist/src/workspace-check.d.ts +18 -0
  93. package/dist/src/workspace-check.js +34 -0
  94. package/package.json +2 -2
  95. package/src/agent-activity.ts +32 -0
  96. package/src/agent-import.ts +2 -2
  97. package/src/agent-tools.ts +2 -2
  98. package/src/agents-md.ts +39 -14
  99. package/src/alerts.ts +190 -0
  100. package/src/browser-pod.ts +3 -0
  101. package/src/capture.ts +88 -0
  102. package/src/compare.ts +146 -0
  103. package/src/completions.ts +27 -3
  104. package/src/deploy.ts +238 -12
  105. package/src/egress.ts +12 -2
  106. package/src/evals.ts +64 -19
  107. package/src/flow-lint.ts +36 -0
  108. package/src/flow-patterns.ts +26 -13
  109. package/src/frontmatter-fields.ts +83 -3
  110. package/src/frontmatter-guard.ts +70 -0
  111. package/src/frontmatter-schema.ts +33 -4
  112. package/src/frontmatter-text.ts +109 -0
  113. package/src/history.ts +13 -0
  114. package/src/hooks.ts +1 -1
  115. package/src/insights.ts +174 -0
  116. package/src/issues.ts +396 -0
  117. package/src/judge-verdict.ts +52 -0
  118. package/src/library.ts +1 -1
  119. package/src/limits.ts +38 -5
  120. package/src/linkable.ts +1 -1
  121. package/src/matter.ts +145 -0
  122. package/src/model-credential.ts +45 -0
  123. package/src/network.ts +35 -7
  124. package/src/notify.ts +22 -7
  125. package/src/okf.ts +1 -1
  126. package/src/online-checks.ts +450 -0
  127. package/src/operator.ts +213 -3
  128. package/src/otel-config.ts +67 -0
  129. package/src/otel.ts +100 -0
  130. package/src/outcome.ts +90 -0
  131. package/src/package.ts +8 -2
  132. package/src/platform.ts +1 -1
  133. package/src/prompt-layers.ts +90 -0
  134. package/src/provider-health.ts +2 -2
  135. package/src/providers.ts +7 -2
  136. package/src/run-container.ts +77 -8
  137. package/src/runner.ts +477 -79
  138. package/src/runs-filter.ts +88 -0
  139. package/src/scan.ts +153 -14
  140. package/src/starter.ts +27 -2
  141. package/src/step-exec.ts +168 -17
  142. package/src/store.ts +268 -9
  143. package/src/tokens.ts +68 -0
  144. package/src/tool-names.ts +1 -1
  145. package/src/tool-programs.ts +1 -1
  146. package/src/trace-tree.ts +187 -0
  147. package/src/translator.ts +13 -2
  148. package/src/trigger-log.ts +1 -0
  149. package/src/workspace-check.ts +41 -0
package/src/matter.ts ADDED
@@ -0,0 +1,145 @@
1
+ // gray-matter, without its cache.
2
+ //
3
+ // gray-matter keeps every input it is handed in a module-level cache keyed by
4
+ // the text, and it stores the entry BEFORE it parses. When the YAML does not
5
+ // parse, the first call throws — and the entry stays, unparsed. The next call
6
+ // with the same text returns it: no error, `data` empty. So in any process
7
+ // that read a file twice, a broken frontmatter was an error once and valid
8
+ // forever after.
9
+ //
10
+ // That is how one agent file took the worker down on 20 Oct 2026. The source
11
+ // API's save check parsed it first (and threw, caught), the write's own
12
+ // "reject files whose frontmatter doesn't parse" then hit the cache and let it
13
+ // through, and the worker, parsing it fresh at boot, threw on it out of
14
+ // listWorkspaces and never came up — for every account on the box.
15
+ //
16
+ // Passing an options object skips the cache entirely (gray-matter only reads
17
+ // and writes it when there are none), so every call here parses the text it
18
+ // was given. The cost is a parse per call, which is what every caller already
19
+ // believed it was paying. Import this, never "gray-matter" itself: a test in
20
+ // tests/matter.test.ts and foldrun-web's consistency test hold every repo to
21
+ // it.
22
+
23
+ import grayMatter from "gray-matter";
24
+ import { JOBS, FOLDABLE, type Job } from "./frontmatter-fields.ts";
25
+
26
+ type Options = NonNullable<Parameters<typeof grayMatter>[1]>;
27
+
28
+ /** Where the four job blocks put things, on the parsed data — out of sight
29
+ * of every reader that spreads it or writes it out as JSON. */
30
+ const FOLD = Symbol.for("foldrun.jobBlocks");
31
+
32
+ export interface JobFold {
33
+ /** A key lifted out of a block, and the block it was in. */
34
+ folded: Map<string, Job>;
35
+ /** A key set both at the top and inside a block: the top one is kept, and
36
+ * check refuses the file (config/conflict). */
37
+ conflicts: Map<string, Job>;
38
+ }
39
+
40
+ /** What the fold did to this data, or null when it had no blocks. */
41
+ export function jobFold(data: unknown): JobFold | null {
42
+ return data && typeof data === "object" ? ((data as Record<symbol, JobFold | undefined>)[FOLD] ?? null) : null;
43
+ }
44
+
45
+ /**
46
+ * The four job blocks (`governance:`, `guardrail:`, `observability:`,
47
+ * `evaluation:`) read as the flat keys they hold (frontmatter-fields.ts
48
+ * JOB_FIELDS). Only a key some config kind lets that block hold is lifted;
49
+ * anything else stays in the block for check to report. A key set both ways
50
+ * keeps its top-level value and is recorded. Nothing parsed is changed: new
51
+ * objects are built (YAML aliases share nodes — deleting from one would
52
+ * delete from the other).
53
+ */
54
+ function foldJobs(data: Record<string, unknown>): Record<string, unknown> {
55
+ let fold: JobFold | null = null;
56
+ let out: Record<string, unknown> | null = null;
57
+ for (const { name } of JOBS) {
58
+ const block = data[name];
59
+ if (!block || typeof block !== "object" || Array.isArray(block) || block instanceof Date) continue;
60
+ const inner = block as Record<string, unknown>;
61
+ const lift = Object.keys(inner).filter((k) => FOLDABLE[name].has(k));
62
+ if (!lift.length) continue;
63
+ out ??= { ...data };
64
+ fold ??= { folded: new Map(), conflicts: new Map() };
65
+ const rest: Record<string, unknown> = {};
66
+ for (const [k, v] of Object.entries(inner)) if (!lift.includes(k)) rest[k] = v;
67
+ for (const k of lift) {
68
+ if (Object.prototype.hasOwnProperty.call(data, k)) fold.conflicts.set(k, name);
69
+ else {
70
+ out[k] = inner[k];
71
+ fold.folded.set(k, name);
72
+ }
73
+ }
74
+ if (Object.keys(rest).length) out[name] = rest;
75
+ else delete out[name];
76
+ }
77
+ if (!out || !fold) return data;
78
+ Object.defineProperty(out, FOLD, { value: fold, enumerable: false });
79
+ return out;
80
+ }
81
+
82
+ function matter(input: string, options?: Options) {
83
+ return grayMatter(input, options ?? {});
84
+ }
85
+ matter.stringify = grayMatter.stringify;
86
+ matter.test = grayMatter.test;
87
+
88
+ /**
89
+ * matter(), for the three files that hold configuration — an agent, a flow,
90
+ * AGENTS.md — with the four job blocks read as flat keys. Every reader of
91
+ * those files goes through this (tests/jobs.test.ts holds them to it);
92
+ * knowledge, memory, skills and every other document keep matter(), so a
93
+ * note's own `governance:` is its own (Codex debate, 21 Oct 2026).
94
+ */
95
+ export function configMatter(input: string, options?: Options) {
96
+ const file = grayMatter(input, options ?? {});
97
+ if (file.data && typeof file.data === "object" && !Array.isArray(file.data)) {
98
+ (file as { data: Record<string, unknown> }).data = foldJobs(file.data as Record<string, unknown>);
99
+ }
100
+ return file;
101
+ }
102
+
103
+ export default matter;
104
+
105
+ /** The first line of a YAML parse error, with where it is: what a person
106
+ * needs to fix the file, without the parser's excerpt. */
107
+ export function frontmatterErrorText(err: unknown): string {
108
+ const msg = err instanceof Error ? err.message : String(err);
109
+ return `frontmatter does not parse — ${msg.split("\n")[0].trim().replace(/:$/, "")}. Quote a value that contains a colon followed by a space.`;
110
+ }
111
+
112
+ /** The 1-based file line a YAML parse error points at, when it says. The
113
+ * text gray-matter hands the parser starts with the newline after the
114
+ * opening `---`, so the parser's 0-based line is the file's 1-based one. */
115
+ export function frontmatterErrorLine(err: unknown): number | undefined {
116
+ const line = (err as { mark?: { line?: unknown } } | null)?.mark?.line;
117
+ return typeof line === "number" ? line + 1 : undefined;
118
+ }
119
+
120
+ /** Frontmatter as the runtime reads a skill: strictly first, then once more
121
+ * with every unquoted value that holds a colon quoted (the commonest way a
122
+ * hand-written SKILL.md breaks, and one other skills clients forgive), and
123
+ * empty when even that fails. `lenient` says the second read was needed. */
124
+ export function parseLenient(raw: string): { data: Record<string, unknown>; lenient: boolean; failed: boolean } {
125
+ try {
126
+ return { data: matter(raw).data as Record<string, unknown>, lenient: false, failed: false };
127
+ } catch {
128
+ const fm = raw.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/);
129
+ if (fm) {
130
+ const fixed = fm[1].replace(
131
+ /^([A-Za-z0-9_-]+):[ \t]+(?!["'>|])(.*:.*)$/gm,
132
+ (_m, k: string, v: string) => `${k}: ${JSON.stringify(v)}`,
133
+ );
134
+ try {
135
+ // A function replacer, not a string: a fixed block containing `$&` or
136
+ // `$$` (a value with those in it) would otherwise be treated as a
137
+ // replacement pattern and splice the whole frontmatter into itself.
138
+ return { data: matter(raw.replace(fm[1], () => fixed)).data as Record<string, unknown>, lenient: true, failed: false };
139
+ } catch {
140
+ /* fall through */
141
+ }
142
+ }
143
+ return { data: {}, lenient: false, failed: true };
144
+ }
145
+ }
@@ -110,3 +110,48 @@ export function accountModelBlock(read: (name: string) => string | null): Record
110
110
  export function isClaudeLoginToken(value: string | null | undefined): boolean {
111
111
  return typeof value === "string" && /^sk-ant-oat\d*-/.test(value.trim());
112
112
  }
113
+
114
+ /**
115
+ * The variables a step's own model call is made with: the credential, the
116
+ * endpoint, the tier names a provider renames and its headers, and the
117
+ * egress proxy's name for the key. A declared secret with one of these
118
+ * names would replace the step's model settings in the sandbox, so `check`
119
+ * warns about it and the runner leaves it out (FOL-19).
120
+ */
121
+ export const MODEL_ENV_NAMES: readonly string[] = [
122
+ "ANTHROPIC_API_KEY",
123
+ "ANTHROPIC_AUTH_TOKEN",
124
+ "ANTHROPIC_BASE_URL",
125
+ "ANTHROPIC_CUSTOM_HEADERS",
126
+ "ANTHROPIC_MODEL",
127
+ "ANTHROPIC_SMALL_FAST_MODEL",
128
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
129
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
130
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
131
+ "CLAUDE_CODE_OAUTH_TOKEN",
132
+ "FOLDRUN_MODEL_KEY",
133
+ "FOLDRUN_MODEL_KEY_FALLBACK",
134
+ ];
135
+
136
+ /** How such a secret crosses into the sandbox instead; the driver maps it
137
+ * back for the agent's own tools (run-container.ts). */
138
+ export const DECLARED_ENV_PREFIX = "FOLDRUN_DECLARED_";
139
+
140
+ export function isModelEnvName(name: string): boolean {
141
+ return MODEL_ENV_NAMES.includes(name.trim());
142
+ }
143
+
144
+ /** Why a `secrets:` list names a model variable, or null. */
145
+ export function modelSecretCollision(names: unknown): string | null {
146
+ const list = (Array.isArray(names) ? names : typeof names === "string" ? names.split(",") : [])
147
+ .filter((n): n is string => typeof n === "string")
148
+ .map((n) => n.trim())
149
+ .filter(isModelEnvName);
150
+ if (!list.length) return null;
151
+ const which = list.map((n) => `\`${n}\``).join(", ");
152
+ return (
153
+ `${which} ${list.length === 1 ? "is a name" : "are names"} the step's own model call uses, so the model keeps its own value: ` +
154
+ "its script and API tools and `verify:` get the declared one, but its own shell sees the model's — " +
155
+ "give the secret another name (MY_ANTHROPIC_KEY), or run the model on it with `provider:` or `foldrun account model set`"
156
+ );
157
+ }
package/src/network.ts CHANGED
@@ -28,6 +28,8 @@
28
28
  // proxy (foldrun-platform egress-proxy.ts) holds the rule per request.
29
29
 
30
30
  /** How wide a step's network is. */
31
+ import { keySpan, dropEmptyBlock } from "./frontmatter-text.ts";
32
+
31
33
  export type NetworkLevel = "open" | "trusted" | "none";
32
34
 
33
35
  export const NETWORK_LEVELS: readonly NetworkLevel[] = ["open", "trusted", "none"];
@@ -268,13 +270,37 @@ export function resolveNetwork(args: {
268
270
  }
269
271
 
270
272
  /** One line for the run's trace. */
273
+ /** The sites a step reached, read from the egress lines on its trail
274
+ * (egress-proxy.ts formatDrain): `egress: CONNECT host:port …` and
275
+ * `egress: GET host/path → 200 …`. Lower-cased, port dropped, sorted. */
276
+ export function hostsInTrail(events: { type?: string; text?: string }[]): string[] {
277
+ const hosts = new Set<string>();
278
+ for (const e of events) {
279
+ // A refused or unreachable call reached nothing: the proxy says so on
280
+ // the same line (`… refused — …`, `… → unreachable: …`).
281
+ if (typeof e.text === "string" && /^egress: \S+ \S+ (?:refused|unreachable|→ unreachable)\b/.test(e.text)) continue;
282
+ const m = typeof e.text === "string" ? /^egress: (?:CONNECT ([^\s:]+)|[A-Z]+ ([^\s/:]+))/.exec(e.text) : null;
283
+ const host = (m?.[1] ?? m?.[2])?.toLowerCase();
284
+ if (host) hosts.add(host);
285
+ }
286
+ return [...hosts].sort();
287
+ }
288
+
289
+ /** `network_enforce: true` in the account's or the workspace's AGENTS.md:
290
+ * the steps here have a site off their list refused rather than only named.
291
+ * Either level switches it on; neither can switch off what the other, or the
292
+ * install (FOLDRUN_NETWORK_ENFORCE=1), switched on. */
293
+ export function networkEnforceWanted(account: unknown, workspace: unknown): boolean {
294
+ return account === true || workspace === true;
295
+ }
296
+
271
297
  export function networkLine(policy: NetworkPolicy, enforced: boolean | null): string {
272
298
  const how =
273
299
  enforced === null
274
300
  ? " — not enforced here (no sandbox network)"
275
301
  : enforced
276
302
  ? ""
277
- : " — report only on this install: a site off the list is allowed and named in the trace";
303
+ : " — report only: a site off the list is allowed and named in the trace (network_enforce: true in AGENTS.md refuses it)";
278
304
  if (policy.level === "open") return `network: open (${policy.from}) — any public site${how}`;
279
305
  const parts: string[] = [];
280
306
  if (policy.defaults) parts.push(`the trusted list (${TRUSTED_SITES.length} sites)`);
@@ -378,13 +404,15 @@ export function setNetworkSetting(raw: string, setting: { level?: NetworkLevel;
378
404
  if (lines[0]?.trim() !== "---") return line ? ["---", line, "---", ...lines].join(eol) : raw;
379
405
  const close = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
380
406
  if (close < 0) return raw;
381
- const at = lines.findIndex((l, i) => i > 0 && i < close && /^network\s*:/.test(l));
382
- if (at < 0) {
407
+ // Where network: is written — at the top or inside `guardrail:` — is
408
+ // where it is set (frontmatter-text.ts); written nowhere, it goes last.
409
+ const where = keySpan(lines.slice(1, close), "network");
410
+ if (!where) {
383
411
  if (line) lines.splice(close, 0, line);
384
412
  return lines.join(eol);
385
413
  }
386
- let end = at + 1;
387
- while (end < close && (/^\s+\S/.test(lines[end]) || /^\s*-\s/.test(lines[end]))) end++;
388
- lines.splice(at, end - at, ...(line ? [line] : []));
389
- return lines.join(eol);
414
+ const front = lines.slice(1, close);
415
+ front.splice(where.at, where.end - where.at, ...(line ? [`${where.indent}${line}`] : []));
416
+ const edited = !line && where.block !== null ? dropEmptyBlock(front, where.block) : front;
417
+ return [lines[0], ...edited, ...lines.slice(close)].join(eol);
390
418
  }
package/src/notify.ts CHANGED
@@ -372,14 +372,29 @@ export async function sendPlainNotification(
372
372
  workspace: string,
373
373
  msg: { event: string; headline: string; detail: string; flow?: string; runId?: string },
374
374
  ): Promise<boolean> {
375
+ return (await sendPlainNotificationOutcome(tenant, workspace, msg)).ok;
376
+ }
377
+
378
+ /**
379
+ * The same send, saying what became of it: `ok` when it arrived, `retrying`
380
+ * when a webhook's first attempt failed and the delivery log holds it for
381
+ * another (FOL-58 — a caller that would retry on its own must not start a
382
+ * second chain).
383
+ */
384
+ export async function sendPlainNotificationOutcome(
385
+ tenant: string,
386
+ workspace: string,
387
+ msg: { event: string; headline: string; detail: string; flow?: string; runId?: string },
388
+ ): Promise<{ ok: boolean; retrying: boolean }> {
389
+ const no = { ok: false, retrying: false };
375
390
  const config = notifyConfig(tenant, workspace);
376
- if (!config) return false;
391
+ if (!config) return no;
377
392
  // These follow `failed`. Each one IS a failure — a flow that stopped
378
393
  // running, a run that has stalled, a budget about to refuse work — that
379
394
  // happens to produce no failed run for the normal path to report. A
380
395
  // destination hearing about failures hears about these; naming one
381
396
  // explicitly also turns it on; leaving `failed` out silences them.
382
- if (!config.events.includes(msg.event) && !config.events.includes("failed")) return false;
397
+ if (!config.events.includes(msg.event) && !config.events.includes("failed")) return no;
383
398
 
384
399
  const body = {
385
400
  text: `${msg.headline} — ${msg.detail} · ${workspace}`,
@@ -392,7 +407,7 @@ export async function sendPlainNotification(
392
407
  try {
393
408
  if (config.email) {
394
409
  const mail = notifyMail(tenant);
395
- if (!mail) return false;
410
+ if (!mail) return no;
396
411
  const r = await sendMail({
397
412
  tenant,
398
413
  mail,
@@ -402,14 +417,14 @@ export async function sendPlainNotification(
402
417
  subject: `${msg.headline}`.slice(0, 160),
403
418
  text: `${msg.headline}\n\n${msg.detail}\n\nworkspace: ${workspace}\n${msg.flow ? `flow: ${msg.flow}\n` : ""}${msg.runId ? `run: ${msg.runId}\n` : ""}`,
404
419
  });
405
- return r.ok;
420
+ return { ok: r.ok, retrying: false };
406
421
  }
407
- if (!webhookTarget(tenant, workspace, config).url) return false;
422
+ if (!webhookTarget(tenant, workspace, config).url) return no;
408
423
  const r = await platform.deliverWebhook(tenant, workspace, { event: msg.event, body: JSON.stringify(body), retry: true });
409
- return r.ok;
424
+ return { ok: r.ok, retrying: r.retrying === true };
410
425
  } catch (err) {
411
426
  console.error(`[foldrun] notify (${msg.event}): ${tenant}/${workspace} →`, err instanceof Error ? err.message : err);
412
- return false;
427
+ return no;
413
428
  }
414
429
  }
415
430
 
package/src/okf.ts CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  import fs from "node:fs";
24
24
  import path from "node:path";
25
- import matter from "gray-matter";
25
+ import matter from "./matter.ts";
26
26
 
27
27
  export const OKF_VERSION = "0.2";
28
28