@foldrun/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +27 -10
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/src/account.d.ts +45 -0
  5. package/dist/src/account.js +130 -0
  6. package/dist/src/api-tools.d.ts +8 -0
  7. package/dist/src/api-tools.js +22 -4
  8. package/dist/src/approvals.d.ts +104 -1
  9. package/dist/src/approvals.js +192 -2
  10. package/dist/src/browser-pod.d.ts +129 -0
  11. package/dist/src/browser-pod.js +284 -0
  12. package/dist/src/budget.d.ts +32 -0
  13. package/dist/src/budget.js +177 -0
  14. package/dist/src/catalog.js +3 -1
  15. package/dist/src/clock.d.ts +49 -0
  16. package/dist/src/clock.js +121 -0
  17. package/dist/src/completions.js +17 -8
  18. package/dist/src/confine.d.ts +22 -0
  19. package/dist/src/confine.js +224 -4
  20. package/dist/src/deploy.d.ts +33 -0
  21. package/dist/src/deploy.js +145 -6
  22. package/dist/src/egress.d.ts +26 -2
  23. package/dist/src/egress.js +8 -1
  24. package/dist/src/eol.d.ts +2 -0
  25. package/dist/src/eol.js +7 -0
  26. package/dist/src/evals.d.ts +9 -0
  27. package/dist/src/evals.js +26 -13
  28. package/dist/src/flow-lint.d.ts +12 -0
  29. package/dist/src/flow-lint.js +331 -1
  30. package/dist/src/flow-patterns.d.ts +141 -0
  31. package/dist/src/flow-patterns.js +568 -0
  32. package/dist/src/gitrepo.d.ts +18 -0
  33. package/dist/src/gitrepo.js +49 -0
  34. package/dist/src/host-env.d.ts +8 -0
  35. package/dist/src/host-env.js +48 -0
  36. package/dist/src/json-schema.d.ts +12 -0
  37. package/dist/src/json-schema.js +202 -0
  38. package/dist/src/kinds.js +1 -1
  39. package/dist/src/language.d.ts +17 -0
  40. package/dist/src/language.js +55 -0
  41. package/dist/src/layout.d.ts +69 -0
  42. package/dist/src/layout.js +200 -0
  43. package/dist/src/library.d.ts +9 -0
  44. package/dist/src/library.js +43 -4
  45. package/dist/src/limits.d.ts +84 -0
  46. package/dist/src/limits.js +287 -0
  47. package/dist/src/locale.d.ts +47 -0
  48. package/dist/src/locale.js +177 -0
  49. package/dist/src/mail.d.ts +88 -0
  50. package/dist/src/mail.js +245 -0
  51. package/dist/src/model-probe.d.ts +3 -0
  52. package/dist/src/model-probe.js +1 -0
  53. package/dist/src/notify.d.ts +132 -8
  54. package/dist/src/notify.js +360 -58
  55. package/dist/src/openapi.js +5 -3
  56. package/dist/src/operator.d.ts +201 -0
  57. package/dist/src/operator.js +251 -0
  58. package/dist/src/paths.d.ts +33 -0
  59. package/dist/src/paths.js +87 -0
  60. package/dist/src/platform.d.ts +144 -4
  61. package/dist/src/platform.js +27 -3
  62. package/dist/src/provider-health.d.ts +55 -0
  63. package/dist/src/provider-health.js +233 -0
  64. package/dist/src/providers.d.ts +481 -0
  65. package/dist/src/providers.js +1105 -6
  66. package/dist/src/refusal.d.ts +83 -0
  67. package/dist/src/refusal.js +281 -0
  68. package/dist/src/run-container.d.ts +206 -17
  69. package/dist/src/run-container.js +547 -80
  70. package/dist/src/runner.d.ts +179 -10
  71. package/dist/src/runner.js +1423 -169
  72. package/dist/src/runtime.d.ts +52 -0
  73. package/dist/src/runtime.js +493 -58
  74. package/dist/src/script-tools.d.ts +15 -2
  75. package/dist/src/script-tools.js +44 -4
  76. package/dist/src/secret-files.d.ts +6 -5
  77. package/dist/src/secret-files.js +24 -11
  78. package/dist/src/secret-health.d.ts +83 -0
  79. package/dist/src/secret-health.js +177 -0
  80. package/dist/src/secrets.d.ts +38 -1
  81. package/dist/src/secrets.js +197 -96
  82. package/dist/src/starter.d.ts +9 -0
  83. package/dist/src/starter.js +95 -11
  84. package/dist/src/step-exec.d.ts +106 -1
  85. package/dist/src/step-exec.js +558 -85
  86. package/dist/src/step-removal.d.ts +15 -0
  87. package/dist/src/step-removal.js +49 -0
  88. package/dist/src/storage.d.ts +23 -4
  89. package/dist/src/storage.js +125 -23
  90. package/dist/src/store.d.ts +492 -38
  91. package/dist/src/store.js +882 -69
  92. package/dist/src/subagents.d.ts +75 -0
  93. package/dist/src/subagents.js +149 -0
  94. package/dist/src/tar.js +2 -1
  95. package/dist/src/test-mode.d.ts +111 -0
  96. package/dist/src/test-mode.js +285 -0
  97. package/dist/src/tool-names.d.ts +13 -1
  98. package/dist/src/tool-names.js +38 -11
  99. package/dist/src/tool-programs.d.ts +26 -0
  100. package/dist/src/tool-programs.js +172 -0
  101. package/dist/src/tool-test.js +152 -33
  102. package/dist/src/translator.d.ts +8 -2
  103. package/dist/src/translator.js +71 -24
  104. package/dist/src/trigger-log.d.ts +43 -0
  105. package/dist/src/trigger-log.js +122 -0
  106. package/dist/src/triggers.d.ts +23 -0
  107. package/dist/src/triggers.js +101 -12
  108. package/dist/src/untrusted-fetch.d.ts +11 -0
  109. package/dist/src/untrusted-fetch.js +77 -0
  110. package/dist/src/web-actions.d.ts +91 -0
  111. package/dist/src/web-actions.js +199 -0
  112. package/dist/src/webhook.d.ts +19 -3
  113. package/dist/src/webhook.js +50 -11
  114. package/index.ts +3 -0
  115. package/package.json +5 -5
  116. package/src/account.ts +149 -0
  117. package/src/api-tools.ts +32 -4
  118. package/src/approvals.ts +253 -2
  119. package/src/browser-pod.ts +313 -0
  120. package/src/budget.ts +178 -0
  121. package/src/catalog.ts +3 -1
  122. package/src/clock.ts +149 -0
  123. package/src/completions.ts +17 -8
  124. package/src/confine.ts +224 -4
  125. package/src/deploy.ts +137 -6
  126. package/src/egress.ts +32 -4
  127. package/src/eol.ts +7 -0
  128. package/src/evals.ts +36 -11
  129. package/src/flow-lint.ts +371 -1
  130. package/src/flow-patterns.ts +597 -0
  131. package/src/gitrepo.ts +48 -0
  132. package/src/host-env.ts +51 -0
  133. package/src/json-schema.ts +184 -0
  134. package/src/kinds.ts +1 -1
  135. package/src/language.ts +62 -0
  136. package/src/layout.ts +228 -0
  137. package/src/library.ts +50 -4
  138. package/src/limits.ts +290 -0
  139. package/src/locale.ts +217 -0
  140. package/src/mail.ts +316 -0
  141. package/src/model-probe.ts +4 -0
  142. package/src/notify.ts +391 -44
  143. package/src/openapi.ts +5 -3
  144. package/src/operator.ts +300 -0
  145. package/src/paths.ts +83 -0
  146. package/src/platform.ts +158 -5
  147. package/src/provider-health.ts +275 -0
  148. package/src/providers.ts +1368 -6
  149. package/src/refusal.ts +320 -0
  150. package/src/run-container.ts +611 -76
  151. package/src/runner.ts +1463 -165
  152. package/src/runtime.ts +487 -55
  153. package/src/script-tools.ts +55 -6
  154. package/src/secret-files.ts +22 -10
  155. package/src/secret-health.ts +230 -0
  156. package/src/secrets.ts +191 -95
  157. package/src/starter.ts +96 -11
  158. package/src/step-exec.ts +548 -41
  159. package/src/step-removal.ts +59 -0
  160. package/src/storage.ts +149 -24
  161. package/src/store.ts +1227 -74
  162. package/src/subagents.ts +189 -0
  163. package/src/tar.ts +2 -1
  164. package/src/test-mode.ts +311 -0
  165. package/src/tool-names.ts +40 -11
  166. package/src/tool-programs.ts +179 -0
  167. package/src/tool-test.ts +159 -32
  168. package/src/translator.ts +74 -23
  169. package/src/trigger-log.ts +160 -0
  170. package/src/triggers.ts +102 -12
  171. package/src/untrusted-fetch.ts +88 -0
  172. package/src/web-actions.ts +237 -0
  173. package/src/webhook.ts +58 -11
  174. package/templates/hello/agents/curator/agent.md +1 -1
  175. package/templates/hello/agents/notetaker/agent.md +1 -1
@@ -1,4 +1,7 @@
1
+ import type { BrowserPodOutcome } from "./browser-pod.ts";
1
2
  import { singleWorkspace } from "./paths.ts";
3
+ import type { TestEffect } from "./test-mode.ts";
4
+ import type { OperatorEvent } from "./operator.ts";
2
5
  /**
3
6
  * The frontmatter of one markdown file, as the runtime reads it. Exported
4
7
  * for the CLI: `foldrun probe` used to import gray-matter itself, which is
@@ -53,9 +56,10 @@ export declare function workspaceDir(tenant: string, workspace: string): string;
53
56
  * The account root — where `library/`, account secrets and the account-wide
54
57
  * AGENTS.md live.
55
58
  *
56
- * On a laptop there is no account, so it is the workspace's parent: `my-desk/`
57
- * sits beside `library/` and an `AGENTS.md` covering everything there. Same
58
- * rule libraryDir uses, so the two never disagree about where "up" is.
59
+ * On a laptop it is wherever `singleAccountRoot` says: the workspace's parent
60
+ * for a flat `my-desk/`, and the account root two levels up for a workspace
61
+ * inside `my-account/workspaces/`. Same rule libraryDir uses, so the two never
62
+ * disagree about where "up" is.
59
63
  */
60
64
  export declare function accountDir(tenant: string): string;
61
65
  /**
@@ -110,6 +114,10 @@ export declare function assertCanonicalCase(rel: string): void;
110
114
  export declare const PLATFORM_FILES: string[];
111
115
  /** True for anything the platform, not the author or the agent, writes. */
112
116
  export declare function isPlatformPath(rel: string): boolean;
117
+ /** What a deploy keeps whatever it ships — the rule above saveWorkspace's
118
+ * body. planDeploy reports removals through the same test, so a plan never
119
+ * lists a file the save would keep. */
120
+ export declare function keptOnDeploy(rel: string, shipped: ReadonlySet<string>): boolean;
113
121
  export declare function saveWorkspace(tenant: string, workspace: string, files: DeployFile[],
114
122
  /** Attribution for the revision this save records. A deploy passes the
115
123
  * commit, which becomes the revision's id. */
@@ -131,19 +139,25 @@ export interface McpSpec {
131
139
  url?: string;
132
140
  headers: Record<string, string>;
133
141
  }
134
- export type ToolDef = {
142
+ /** `outward: true` in a tool's frontmatter: this one sends, posts, buys,
143
+ * publishes or uploads. It changes nothing at run time — `check` uses it to
144
+ * insist the step that grants it has a verify or a gate. */
145
+ export type OutwardMark = {
146
+ outward?: boolean;
147
+ };
148
+ export type ToolDef = ({
135
149
  kind: "http";
136
150
  name: string;
137
151
  spec: ApiSpec;
138
- } | {
152
+ } & OutwardMark) | ({
139
153
  kind: "script";
140
154
  name: string;
141
155
  spec: Record<string, unknown>;
142
- } | {
156
+ } & OutwardMark) | ({
143
157
  kind: "mcp";
144
158
  name: string;
145
159
  spec: McpSpec;
146
- };
160
+ } & OutwardMark);
147
161
  /**
148
162
  * The program inside a single-file script tool: the first fenced block whose
149
163
  * language tag names a runnable language. By tag, not by position — a body
@@ -167,6 +181,13 @@ export declare function fencedCodeBlock(body: string): {
167
181
  start: number;
168
182
  end: number;
169
183
  } | null;
184
+ /**
185
+ * `outward: true` — this tool changes something that is not ours: it sends,
186
+ * posts, buys, publishes, uploads. The runtime does not treat it differently;
187
+ * `check` does, because the step that uses one is the step where a silent
188
+ * failure costs something that cannot be taken back. See `lintFlow`.
189
+ */
190
+ export declare function toolIsOutward(data: Record<string, unknown>): boolean;
170
191
  export declare function parseToolDef(data: Record<string, unknown>, fallbackName: string, body?: string): ToolDef | null;
171
192
  /**
172
193
  * Tools in a directory, in both shapes.
@@ -189,6 +210,19 @@ export declare function parseToolDef(data: Record<string, unknown>, fallbackName
189
210
  *
190
211
  * Flat files keep working unchanged — the shape is a migration, not a break.
191
212
  */
213
+ /**
214
+ * A folder tool's `requirements.txt` and `package.json`, as a `runtime:`
215
+ * block. Requirement lines only: comments and blanks are left out, and so
216
+ * are options — whole option lines (`-r`, `--index-url`, `-e`) and the ones
217
+ * a requirement carries (`--hash=…`). Options never reach the installer,
218
+ * which takes them from the same argv as its operands; their NAMES are kept
219
+ * in `ignored`, never their values (an index URL can carry a password), so
220
+ * the build log can say what was not applied. Anything else that is not a
221
+ * requirement is refused later by parseRuntime with a log line, the same as
222
+ * it would be in tool.md. `dependencies` only — a tool's dev dependencies
223
+ * are its author's, not its runtime's.
224
+ */
225
+ export declare function folderRuntime(folder: string): Record<string, unknown> | null;
192
226
  declare function readToolDir(dir: string, scope?: "workspace" | "account"): Record<string, ToolDef>;
193
227
  /** Definitions that exist on disk but could not be read, for the scopes an
194
228
  * agent draws on. Empty when everything parsed. */
@@ -211,7 +245,9 @@ export interface ApiSpec {
211
245
  openapi?: string;
212
246
  /** Which of the document's operations to expose — operationIds or
213
247
  * `"METHOD /path"` strings. Absent: every operation, capped at 60.
214
- * Present: only these, and the generic `call_<api>` tool is withheld. */
248
+ * Present: only these, and the generic `call_<api>` tool is withheld —
249
+ * always. If none of them resolve (document missing, moved, or an
250
+ * operation renamed) the API gets no tools that run: it fails closed. */
215
251
  operations?: string[];
216
252
  /** Calls per second this API may see from one step: `count` is the burst
217
253
  * the bucket holds, `perSec` the refill rate. `"5/s"` → {5, 5};
@@ -241,14 +277,52 @@ export interface AgentInfo {
241
277
  /** Names still written under the removed `use:` key. Nothing is granted
242
278
  * for them; `foldrun check` and the run log say what to write instead. */
243
279
  legacyUse: string[];
280
+ /** Retired built-in names still in `tools:` (files, bash) — granted, but
281
+ * `check` says what to write instead. */
282
+ retiredTools: string[];
244
283
  /** Colleagues this agent may consult mid-run (`agents:`) — each becomes a
245
284
  * consult_<name> tool. The team's edges, as the author drew them. */
246
285
  consults: string[];
286
+ /** Colleagues this agent may DELEGATE to (`subagents:`) — each runs with
287
+ * its own context and its own tools, no wider than this agent's. */
288
+ subagents: string[];
247
289
  /** The `skills:` allowlist, or null when the field is absent — which is
248
290
  * not the same thing: absent inherits every skill in scope, an empty
249
291
  * list withholds all of them. */
250
292
  skills: string[] | null;
251
293
  secrets: string[];
294
+ /** `budget:` — the most this agent may spend in ONE run, in USD, across
295
+ * every step it takes in it; null is no cap. Per run and nothing else:
296
+ * an agent has no calendar, the workspace and the account do. */
297
+ budget: number | null;
298
+ /** What is wrong with the `budget:` line, or null. */
299
+ budgetProblem: string | null;
300
+ /** `timezone:` — the calendar this agent works to, nearest-wins over its
301
+ * flow's, its workspace's and the account's. Null inherits. */
302
+ timezone: string | null;
303
+ /** What is wrong with the `timezone:` line, or null. */
304
+ timezoneProblem: string | null;
305
+ languageProblem: string | null;
306
+ /** region:/currency:/units:/calendar: values that cannot be read. */
307
+ localeProblems: string[];
308
+ /** `web:` values that cannot work. The run says
309
+ * so in its trail and carries on; check and deploy say so first. */
310
+ webProblems: string[];
311
+ /** `web:` that works but should change — the old `session:` key, a pinned
312
+ * user agent behind the runner image's Chrome (when this process knows
313
+ * that Chrome: FOLDRUN_RUNNER_ENGINES). Warnings, never refusals. */
314
+ webWarnings: string[];
315
+ /** `schedule:` written on an agent, which nothing runs — only a flow has
316
+ * a clock. The message, or null when there is no such line. */
317
+ scheduleProblem: string | null;
318
+ /** `limits:` — the agent's own per-step call limits, as readable. */
319
+ limits: Record<string, number>;
320
+ /** What is wrong with the `limits:` block's shape; key-vs-grant problems
321
+ * need the tool lists, and `check` adds them (limits.ts limitKeyProblems). */
322
+ limitProblems: string[];
323
+ /** Tools this agent declares inline rather than in `tools:` — `apis:`,
324
+ * `scripts:` and `mcpServers:` names — which a limit may also name. */
325
+ inlineTools: string[];
252
326
  }
253
327
  export declare const MODEL_TIERS: Record<string, string>;
254
328
  export type Tier = keyof typeof MODEL_TIERS;
@@ -280,6 +354,11 @@ export declare function parseApis(raw: unknown): ApiSpec[];
280
354
  export interface FlowStep {
281
355
  agent: string;
282
356
  instruction: string;
357
+ /** The instruction was written over more than one line and the parser
358
+ * joined them. It runs, but `foldrun check` says so: a wrapped
359
+ * instruction is one editor keystroke away from being a paragraph of
360
+ * prose the parser has to guess about. */
361
+ wrapped?: boolean;
283
362
  group: number;
284
363
  optional: boolean;
285
364
  /** Set when the step targets another flow (`[[flow:name]]`) instead of an agent. */
@@ -291,11 +370,14 @@ export interface FlowStep {
291
370
  effort?: string;
292
371
  /** Pause for a human before running this step. */
293
372
  approve?: boolean;
294
- /** Run only if the previous results contain this text (case-insensitive).
295
- * Independent: every matching `when:` step in a group runs. */
373
+ /** Run only if the previous group's result has a line beginning with
374
+ * this marker (case-insensitive). The previous group is the nearest
375
+ * earlier one that produced a result. Independent: every matching
376
+ * `when:` step in a group runs. */
296
377
  when?: string;
297
378
  /** Exclusive routing: of a group's `case:` steps, only the FIRST whose
298
- * text appears in the previous results runs; the rest are routed past. */
379
+ * marker leads a line of the previous group's result runs; the rest are
380
+ * routed past. */
299
381
  case?: string;
300
382
  /** The route of last resort: runs only when none of its group's `case:`
301
383
  * steps matched. */
@@ -357,9 +439,32 @@ export interface FlowStep {
357
439
  * cannot, and hands the value to the next group losslessly — the narrow
358
440
  * move the grammar ADR allowed instead of variables. */
359
441
  output?: "json";
442
+ /** `schema:` — the JSON Schema an `output: json` value must satisfy,
443
+ * written inline (one-line JSON, or a YAML block under the key) and
444
+ * checked where the reply is; a value that does not fit fails the step
445
+ * with the field named. */
446
+ schema?: Record<string, unknown> | boolean;
447
+ /** `schema: <path>` — the same, in a file under the workspace, read when
448
+ * the step runs. */
449
+ schemaPath?: string;
450
+ /** `parallel:` — how many of an `each:` step's instances run at once.
451
+ * Unset is all of them, which is what fan-out always did. */
452
+ parallel?: number;
453
+ /** `max_turns:` — the most model turns this step may take before it is
454
+ * stopped, beside `budget:` (money) and `timeout:` (time). */
455
+ maxTurns?: number;
456
+ /** `limits: {web.search: 10}` — this step's call limits, over the agent's
457
+ * (nearest wins per key). See limits.ts. */
458
+ limits?: Record<string, number>;
360
459
  /** 1-indexed line in the flow file. Diagnostics without a line make you
361
460
  * search; every real linter emits file:line. */
362
461
  line?: number;
462
+ /** What is wrong with an option's VALUE, in the author's words — `timeout:
463
+ * abc`, `retry: 9`, `each: columns`. The parser keeps the step (a run
464
+ * still has a shape) and the lint reports each of these as an error, so
465
+ * `foldrun check` fails on a value that would otherwise be read as
466
+ * something the author never wrote: `timeout: 15m` used to be one second. */
467
+ problems?: string[];
363
468
  }
364
469
  export interface FlowInfo {
365
470
  name: string;
@@ -390,11 +495,16 @@ export interface FlowInfo {
390
495
  * that crosses it is the last one that runs. Null: no cap of its own
391
496
  * (the workspace's monthly `budget:` still applies). */
392
497
  budget: number | null;
498
+ /** What is wrong with the `budget:` line, or null. */
499
+ budgetProblem: string | null;
393
500
  /** `trigger: flow` — start when this other flow of the workspace finishes,
394
501
  * with `on:` saying which endings count. The finished run's summary and
395
502
  * result become this run's task. */
396
503
  after: string | null;
397
- on: "completed" | "failed" | "any";
504
+ /** `blocked` is a completed run whose verdict is BLOCKED — the work
505
+ * refused itself. `completed` no longer includes those: a follow-up to a
506
+ * publish that did not happen should not run as if it had. */
507
+ on: "completed" | "failed" | "blocked" | "any";
398
508
  /** `trigger: once` — an ISO 8601 instant; fires the first tick at or after
399
509
  * it, once, then never again. */
400
510
  at: string | null;
@@ -412,6 +522,66 @@ export interface FlowInfo {
412
522
  /** `trigger: storage` — a path prefix under storage/; a file landing
413
523
  * there starts the flow with the path as its task. */
414
524
  path: string | null;
525
+ /** `idempotency:` — the name of the field that identifies a delivery, so
526
+ * the same one twice starts one run. A header on the request
527
+ * ("x-github-delivery"), or "body:<key>" for a top-level field of a JSON
528
+ * body. A name, never an expression: the value is read from the delivery,
529
+ * not computed from it. Null: every delivery is its own event. */
530
+ idempotency: string | null;
531
+ /** `debounce:` — seconds of quiet before a fired trigger becomes a run.
532
+ * A burst of deliveries, storage writes or watched changes collapses into
533
+ * one run once they stop. The task is the LAST event's, since that is the
534
+ * state the flow is reacting to. Null: every event starts a run. */
535
+ debounce: number | null;
536
+ /** `throttle:` — the least time between two runs of this flow. A trigger
537
+ * arriving inside the window is dropped, and the trace of the run that
538
+ * held the window says how many. Null: no floor. */
539
+ throttle: number | null;
540
+ /** `catchup:` — what a scheduled flow does about a fire it missed while
541
+ * the platform was not running. "last" is the behaviour that always
542
+ * applied: one make-up run for however many were missed. "none" skips
543
+ * them, for a flow whose moment has passed by the time anyone notices.
544
+ * Null reads as "last". */
545
+ catchup: "last" | "none" | null;
546
+ /** `disable_after:` — consecutive failed runs after which the platform
547
+ * stops firing this flow's schedule and says so. A nightly desk that has
548
+ * failed all week is not going to fix itself, and every one of those runs
549
+ * costs money and sends mail. Null: never quarantined. */
550
+ disableAfter: number | null;
551
+ /** `pause_when:` — a marker (DONE, NOTHING LEFT) that, leading a line of
552
+ * any step's reply in the flow's latest run, pauses its schedule: the
553
+ * work is finished, so firing again would only spend money saying so.
554
+ * Derived from that run, like disable_after, so a run that finds work
555
+ * again (a person clicking Run after adding some) lifts it by itself.
556
+ * Null: never paused. */
557
+ pauseWhen: string | null;
558
+ /** `sla:` — seconds a run of this flow is expected to take. A run still
559
+ * going well past it is reported once, as a notification, and keeps
560
+ * running: this is a smoke alarm, not a timeout. Null: no expectation. */
561
+ sla: number | null;
562
+ /** `approvers:` — the email addresses that may decide this flow's gates,
563
+ * or the single word "admins" for anyone at admin or above. Empty means
564
+ * the account's existing rule: any member who can run the workspace.
565
+ * Nobody may approve their own run's gate whatever this says. */
566
+ approvers: string[] | null;
567
+ /** `approve_within:` — how long a gate waits for a person before the run
568
+ * is rejected and fails. A gate with no expiry holds its run, and its
569
+ * place in the account's concurrency, for as long as nobody looks.
570
+ * Null: it waits indefinitely, which stays the default. */
571
+ approveWithin: number | null;
572
+ /** `live: true` — this flow's runs are real even where the platform would
573
+ * default them to test: in a preview workspace, and as an eval's subject.
574
+ * Only ever an opt-out of the safe default, never the other way round. */
575
+ live: boolean;
576
+ /** `description:` — one line on what this flow is for, shown in listings
577
+ * and the trace. Read here so a documented, scaffolded field means
578
+ * something rather than sitting in every file unread. */
579
+ description: string | null;
580
+ /** Flow-level frontmatter values the parser could not read — a bad
581
+ * `overlap:`/`priority:`/`catchup:`/`on:`/`signature:`, or a step line
582
+ * whose `[[target]]` is malformed. Errors, the way a bad step option is,
583
+ * rather than a silent default. */
584
+ frontProblems: string[];
415
585
  steps: FlowStep[];
416
586
  }
417
587
  export declare const FLOW_TRIGGERS: readonly ["manual", "schedule", "webhook", "email", "flow", "once", "watch", "storage"];
@@ -422,9 +592,25 @@ export interface AgentAssets {
422
592
  }
423
593
  export declare function agentAssets(tenant: string, workspace: string, agent: string): AgentAssets;
424
594
  export declare function listAgents(tenant: string, workspace: string): AgentInfo[];
595
+ /** The most fan-out instances `parallel:` may run at once — the fan-out's
596
+ * own hard cap, since more slots than items is no cap. */
597
+ export declare const PARALLEL_CAP = 20;
598
+ /** The most turns `max_turns:` may allow. A turn is one model reply, tool
599
+ * calls included; a step that needs more than this is not one step. */
600
+ export declare const MAX_TURNS_CAP = 500;
425
601
  /** "90s", "30m", "4h", "3d" — or a bare number of seconds. Clamped to 30
426
602
  * days: a wait is a pause in a flow, not a second scheduler. */
427
603
  export declare function parseWait(value: string): number | undefined;
604
+ /** The one duration grammar every key shares — `wait:`, `timeout:`, the
605
+ * flow's `throttle:`/`debounce:`/`sla:` — as whole seconds, at least 1.
606
+ * Undefined for anything that is not one: the caller says what the key
607
+ * was, because "not a duration" is only useful with the line it names. */
608
+ export declare function parseDuration(value: string): number | undefined;
609
+ /** `timeout:` — the same units `wait:` takes, and no ceiling: the platform
610
+ * sets no clock of its own, so the author's number stands as written.
611
+ * `timeout: 15m` was `Math.max(1, Number("15m") || 0)` — one second — and
612
+ * every step wearing it was cut off before its first tool call returned. */
613
+ export declare function parseTimeout(value: string): number | undefined;
428
614
  /** An ISO 8601 instant (YAML may already have parsed it into a Date), or
429
615
  * null for anything that is not one — a `once` flow with an unreadable
430
616
  * `at:` never fires, and `foldrun check` says so. */
@@ -437,7 +623,7 @@ export declare function parseInstant(raw: unknown): string | null;
437
623
  * removed a leading OR a trailing quote independently, so a value that
438
624
  * merely *ended* with one lost it — and
439
625
  *
440
- * verify: test "$(cat ../../storage/draft/published.run)" = "$FOLDRUN_RUN_ID"
626
+ * verify: test "$(cat workspace/storage/draft/published.run)" = "$FOLDRUN_RUN_ID"
441
627
  *
442
628
  * reached bash unbalanced, which exited 2 with "unexpected EOF" on every
443
629
  * run. The step could never pass, whatever the agent did, and nothing said
@@ -449,7 +635,55 @@ export declare function parseInstant(raw: unknown): string | null;
449
635
  * they are two pairs, not one wrapper.
450
636
  */
451
637
  export declare function unquote(raw: string): string;
638
+ /**
639
+ * Does this text carry the marker a `when:` is looking for?
640
+ *
641
+ * At the START OF A LINE, not anywhere in the prose. A plain substring
642
+ * search is the obvious implementation and it is wrong in a way that is
643
+ * very hard to see: an agent writing
644
+ *
645
+ * There are no BLOCKED items this week.
646
+ *
647
+ * opened a `when: BLOCKED` gate, because the word is right there. Saying a
648
+ * marker is absent necessarily names it, so the more carefully an agent
649
+ * explains itself the more likely it is to trip its own condition. It bit
650
+ * one desk on its first run and sat latent in another.
651
+ *
652
+ * A verdict is written as a headline — the line leads with it — so leading
653
+ * position is what distinguishes "this is my verdict" from "here is a word
654
+ * I am using in a sentence". Markdown decoration in front of it is fine: a
655
+ * heading, a bullet or bold is still a line that leads with the marker.
656
+ *
657
+ * The marker must also END at a boundary, so BLOCKED does not match
658
+ * BLOCKEDBY.
659
+ */
660
+ /**
661
+ * The path of a `when: rows of <path>` condition, or null when the condition
662
+ * is a marker. `rows of` is the phrase `each:` uses for the same file, so a
663
+ * flow reads a CSV one way in both. The whole phrase is required: a bare
664
+ * `when: ROWS` stays the marker it always was.
665
+ */
666
+ export declare function whenRowsPath(when: string): string | null;
667
+ export declare function markerPresent(text: string | null | undefined, marker: string): boolean;
452
668
  export declare function parseFlow(file: string, raw: string): FlowInfo;
669
+ /** A schema written out: JSON, or YAML (parsed as frontmatter is, by the
670
+ * same engine). The result must be an object — a schema is a document,
671
+ * and `schema: 3` or `schema: [a, b]` describes nothing. */
672
+ export declare function parseSchemaText(text: string): {
673
+ schema: Record<string, unknown> | boolean;
674
+ } | {
675
+ problem: string;
676
+ };
677
+ /** May this person decide a gate of this flow? `approvers:` is an allowlist
678
+ * of addresses plus the word "admins"; an empty list defers to whatever
679
+ * permission the caller already needed to reach the route.
680
+ *
681
+ * Self-approval is refused separately (see approvals.ts): who STARTED a run
682
+ * is a property of the run, not of the flow file. */
683
+ export declare function mayApprove(flow: Pick<FlowInfo, "approvers"> | null | undefined, who: {
684
+ email?: string | null;
685
+ role?: string | null;
686
+ }): boolean;
453
687
  /** A live run of this flow — the fact overlap: decisions are made on. */
454
688
  export declare function flowHasLiveRun(tenant: string, workspace: string, flowName: string): boolean;
455
689
  /**
@@ -460,41 +694,29 @@ export declare function flowHasLiveRun(tenant: string, workspace: string, flowNa
460
694
  * and the diff is exactly the lines that changed. A flow with no
461
695
  * frontmatter gains the smallest one that can carry the trigger.
462
696
  */
463
- export declare function setFlowTrigger(raw: string, opts: {
697
+ declare function setFlowTriggerLF(raw: string, opts: {
464
698
  trigger: FlowTrigger;
465
699
  schedule?: string;
466
700
  timezone?: string;
467
701
  }): string;
468
- /**
469
- * Rewrite one step's option lines — the board's step editor. Surgical, like
470
- * every flow rewrite: only the managed option lines of block `index` change;
471
- * the step line, its unmanaged options (approve stays the `!` marker's
472
- * business), other steps, comments and frontmatter survive verbatim.
473
- * `null` clears an option; values are clamped exactly as parseFlow reads
474
- * them, so what the editor writes is what the runner will do.
475
- */
476
- /**
477
- * Rewrite one step's instruction, and nothing else.
478
- *
479
- * The step line carries four things — the group number, the optional/approve
480
- * marker, the wikilink, and the instruction — and only the last is being
481
- * edited. Rebuilding the line from the parsed step would silently normalise
482
- * the other three (a "2?" becoming "2.", an en dash becoming an em dash), so
483
- * the prefix is taken verbatim from the file and only the tail replaced. A
484
- * flow's markdown stays the author's.
485
- */
486
- export declare function updateFlowStepInstruction(raw: string, index: number, instruction: string): string;
487
- export declare function updateFlowStep(raw: string, index: number, options: Partial<Record<"model" | "effort" | "retry" | "timeout" | "verify" | "when" | "case" | "else" | "loop" | "until" | "each" | "max", string | number | null>>): string;
488
- export declare function reorderFlowSteps(raw: string, groups: number[][]): string;
702
+ declare function updateFlowStepLF(raw: string, index: number, options: Partial<Record<"model" | "effort" | "retry" | "timeout" | "verify" | "when" | "case" | "else" | "loop" | "until" | "each" | "max" | "limits", string | number | null>>): string;
489
703
  /** Append a step as its own group at the end of the flow. */
490
- export declare function addFlowStep(raw: string, step: {
704
+ declare function addFlowStepLF(raw: string, step: {
491
705
  target: string;
492
706
  subflow?: boolean;
493
707
  instruction?: string;
494
708
  }): string;
495
- /** Drop one step, by its index in the flow's parsed steps. */
496
- export declare function removeFlowStep(raw: string, index: number): string;
709
+ export declare const setFlowTrigger: (raw: string, opts: Parameters<typeof setFlowTriggerLF>[1]) => string;
710
+ export declare const updateFlowStepInstruction: (raw: string, index: number, instruction: string) => string;
711
+ export declare const updateFlowStep: (raw: string, index: number, options: Parameters<typeof updateFlowStepLF>[2]) => string;
712
+ export declare const reorderFlowSteps: (raw: string, groups: number[][]) => string;
713
+ export declare const addFlowStep: (raw: string, step: Parameters<typeof addFlowStepLF>[1]) => string;
714
+ export declare const removeFlowStep: (raw: string, index: number) => string;
497
715
  export declare function listFlows(tenant: string, workspace: string): FlowInfo[];
716
+ /** One flow of a workspace by name, or null. The same read listFlows makes,
717
+ * named so callers that want a single flow — the approval rules, the
718
+ * runner's budget stamp — do not each re-implement the directory walk. */
719
+ export declare function readFlow(tenant: string, workspace: string, flowName: string): FlowInfo | null;
498
720
  export interface WorkspaceSummary {
499
721
  name: string;
500
722
  description: string;
@@ -642,10 +864,83 @@ export interface RunEvent {
642
864
  call?: string;
643
865
  ms?: number;
644
866
  err?: boolean;
867
+ /** Set on a test run's event that reports what the platform did instead
868
+ * of the real thing — a refused send, a redirected email, a diverted
869
+ * state write, a withheld secret. The run page lists these; the text is
870
+ * still the trace's line. See test-mode.ts. */
871
+ effect?: TestEffect;
872
+ /** Set when a person was involved mid-step: a question the agent asked
873
+ * (ask_person), its answer, or a message a person sent in (operator.ts). */
874
+ operator?: OperatorEvent;
875
+ /** Set on a tool event a SUB-AGENT made (`subagents:`): its name. The
876
+ * trace labels the call with it; cost stays the step's. */
877
+ subagent?: string;
878
+ /** Set on an error a failed check wrote — `verify:` (its headline and its
879
+ * detail), `output: json`, a schema. What the runner reads to decline a
880
+ * retry of an outward step whose tools already ran. */
881
+ check?: boolean;
645
882
  }
883
+ /** A question an agent asked a person while its step ran (tools: [ask]). */
884
+ export interface StepQuestion {
885
+ id: string;
886
+ question: string;
887
+ options?: string[];
888
+ askedAt: string;
889
+ answer?: string;
890
+ answeredAt?: string;
891
+ by?: string | null;
892
+ /** true when the wait ran out with no answer. */
893
+ unanswered?: boolean;
894
+ }
895
+ /** A message a person sent into a running step, and when the agent got it. */
896
+ export interface StepMessage {
897
+ text: string;
898
+ by?: string | null;
899
+ at: string;
900
+ deliveredAt: string;
901
+ }
902
+ /** One attempt of a step — see StepRecord.tries. */
903
+ export interface StepAttempt {
904
+ /** Which try, 1-based, in the order they ran: a retry is the next try,
905
+ * and so is the full re-run after a slim go the browser pod was lost
906
+ * under. Equal to the step's attempt (`attempts` at the time) unless
907
+ * `attempt` says otherwise. */
908
+ n: number;
909
+ /** The step's attempt this try belongs to, written only when it is not
910
+ * `n` — after a lost slim go, its full re-run is try 2 of attempt 1. */
911
+ attempt?: number;
912
+ /** `lost`: a slim go the account's browser pod went away under, which
913
+ * the step ran again on the full image within the same attempt — a row
914
+ * of its own (image slim), before the full go's. */
915
+ status: "completed" | "failed" | "lost";
916
+ costUsd: number | null;
917
+ tokens: {
918
+ input: number;
919
+ output: number;
920
+ } | null;
921
+ computeSecs: number | null;
922
+ startedAt: string;
923
+ finishedAt: string;
924
+ /** The last error event this attempt wrote, when it failed. */
925
+ error?: string;
926
+ /** The runner image this attempt ended on, and — when it browsed through
927
+ * the account's pod from slim — what happened to the pod: reconnects,
928
+ * the re-run on full, or why it was not re-run. */
929
+ image?: "full" | "slim";
930
+ browserPod?: string;
931
+ }
932
+ /** The step's totals across its attempts, from the rows. Null where no
933
+ * attempt reported the figure — an in-process step rents no sandbox, and
934
+ * billing reads null as zero, not as "unknown, charge something". */
935
+ export declare function sumAttempts(tries: StepAttempt[]): Pick<StepAttempt, "costUsd" | "tokens" | "computeSecs">;
646
936
  export interface StepRecord {
647
937
  agent: string;
648
938
  instruction: string;
939
+ /** Questions the agent asked a person mid-step (tools: [ask]), newest
940
+ * last. One without an answer (and not `unanswered`) is still open. */
941
+ questions?: StepQuestion[];
942
+ /** Messages a person sent into the running step, as delivered. */
943
+ messages?: StepMessage[];
649
944
  group: number;
650
945
  optional: boolean;
651
946
  /** Carried from the flow — see FlowStep. Both are resolved at run
@@ -662,6 +957,17 @@ export interface StepRecord {
662
957
  verify?: string;
663
958
  /** Attempts made so far, for the run trace. */
664
959
  attempts?: number;
960
+ /**
961
+ * One row per attempt, oldest first: what each cost and how it ended.
962
+ * `costUsd`, `tokens` and `computeSecs` on the step are the SUM of these
963
+ * rows — a retried step spent every attempt's money, and the run's spend,
964
+ * the flow's `budget:` and the platform's bill all read the step's
965
+ * figure, so the figure is the total. Before this the step carried only
966
+ * the last attempt's cost and a `retry: 2` step could spend three shares
967
+ * of the budget while reporting one. A loop cycle that re-runs the step
968
+ * appends here too, so its earlier cycles are not lost from the bill.
969
+ */
970
+ tries?: StepAttempt[];
665
971
  /** The sandbox this step is executing in, while it is: which executor,
666
972
  * its handle (a pod name), and how many of its output lines the driver
667
973
  * has already applied to this record. What a driver that inherits the
@@ -677,6 +983,27 @@ export interface StepRecord {
677
983
  /** A size class a retry moved the step up to (an evicted or OOM-killed
678
984
  * attempt), overriding the agent's own `size:` for the attempts after. */
679
985
  sizeUp?: "large" | "heavy";
986
+ /** Which runner image the step's sandbox ran: `full` (browsers in it) or
987
+ * `slim`, why, and `pod` when a browsing step ran slim and browsed
988
+ * through the account's browser pod. The last attempt's; absent where
989
+ * the executor has one image (docker, in-process). */
990
+ image?: {
991
+ variant: "full" | "slim";
992
+ why?: string;
993
+ pod?: boolean;
994
+ } | null;
995
+ /** Slim browsing through the account's pod (browser-pod.ts): reconnect
996
+ * tries and how many got through; when the pod was lost, why and what
997
+ * the step had written by then; and what happened — `fallback` (the
998
+ * step had only read and ran again on the full image) or `failure` (it
999
+ * had written, so it was not re-run). */
1000
+ browserPod?: (BrowserPodOutcome & {
1001
+ fallback?: string;
1002
+ failure?: string;
1003
+ }) | null;
1004
+ /** The slim go a lost browser pod ended, set aside while the step re-runs
1005
+ * on full; recordAttempt makes it a `lost` row of `tries` and clears it. */
1006
+ podLostTry?: Omit<StepAttempt, "n" | "attempt" | "startedAt"> | null;
680
1007
  /** When the step actually started and finished executing — not when the
681
1008
  * run reached it. A step's events approximate this (first and last), but
682
1009
  * a model can think for minutes before its first tool call, and a step
@@ -699,6 +1026,11 @@ export interface StepRecord {
699
1026
  max?: number;
700
1027
  /** Carried from the flow — see FlowStep. */
701
1028
  output?: "json";
1029
+ schema?: Record<string, unknown> | boolean;
1030
+ schemaPath?: string;
1031
+ parallel?: number;
1032
+ maxTurns?: number;
1033
+ limits?: Record<string, number>;
702
1034
  /** The parsed JSON an `output: json` step returned. Kept beside the prose
703
1035
  * result rather than instead of it: the reply is still what a person
704
1036
  * reads on the run page, and the data is what the next step computes on. */
@@ -824,6 +1156,20 @@ export interface RunRecord {
824
1156
  * simply gets its first line — which is still better than a status.
825
1157
  */
826
1158
  summary?: string | null;
1159
+ /**
1160
+ * What the run's work concluded, when its summary leads with one of the
1161
+ * four verdict words every desk ends on: GOOD, BAD, QUIET, BLOCKED.
1162
+ *
1163
+ * `status` says whether the machine ran — a step crashed, a budget ran
1164
+ * out. The verdict says what the agents found when it did. They are
1165
+ * different facts: a publisher that checks, sees the wrong change in
1166
+ * front of it and refuses to push has run perfectly, and its run is
1167
+ * `completed`. Until this field, that run was a green tick beside a line
1168
+ * reading BLOCKED (run-mu4sdg8z-1otr, 2026-09-17) — the one outcome a
1169
+ * person most needed to see, drawn as success. Null: the summary does
1170
+ * not lead with a verdict, and the run reads exactly as it always did.
1171
+ */
1172
+ verdict?: Verdict | null;
827
1173
  /** The flow's per-run `budget:` at the time this run started, so the cap
828
1174
  * a run was held to is on its record rather than read from a file that
829
1175
  * may since have changed. Null: none. */
@@ -832,6 +1178,26 @@ export interface RunRecord {
832
1178
  * the agents decided was worth remembering, listed so a person can review
833
1179
  * the lesson without diffing the folder. */
834
1180
  memoryWrites?: string[];
1181
+ /** Who asked for this run, when a person did: their email address. A
1182
+ * schedule, a webhook and a chained flow have no one, and leave it unset.
1183
+ * Kept so a gate can refuse the one approval nobody should be able to
1184
+ * give — your own. */
1185
+ startedBy?: string | null;
1186
+ /** When this run's gate stops waiting, stamped the moment it parks from
1187
+ * the flow's `approve_within:`. A gate nobody answers by then is rejected
1188
+ * and the run fails, rather than holding a concurrency slot forever. */
1189
+ approveBy?: string | null;
1190
+ /** A test run: every step runs for real, against the real files and the
1191
+ * real read APIs, but nothing outward happens — sends are refused or
1192
+ * sunk at the egress proxy, send-capable secrets never enter the
1193
+ * sandbox, and state/ and storage/ writes land under this run's own
1194
+ * directory. On the record, not in an env var, so a script cannot turn
1195
+ * it off; carried onto every rerun of this run. See test-mode.ts. */
1196
+ test?: boolean;
1197
+ /** Digests of the emailed approval links that have already decided a
1198
+ * step of this run — never the tokens. A link is good once; the second
1199
+ * click is a 409, not a second approval. Written by approvals.ts. */
1200
+ approvalLinksUsed?: string[];
835
1201
  steps: StepRecord[];
836
1202
  }
837
1203
  export declare function runFilePath(tenant: string, workspace: string, runId: string): string;
@@ -878,7 +1244,81 @@ export declare function writeRun(tenant: string, workspace: string, run: RunReco
878
1244
  export interface RunWithWorkspace extends RunRecord {
879
1245
  workspace: string;
880
1246
  }
1247
+ /**
1248
+ * Every run in the account, newest first, off the per-workspace index.
1249
+ *
1250
+ * A finished run comes back as the index knows it: id, flow, status,
1251
+ * summary, times, and each step's agent, group, status, cost, tokens,
1252
+ * seconds, skip reason and last error — with `instruction` empty, `events`
1253
+ * holding only that error, and `result` null. That is what every list and
1254
+ * every rollup reads (runCost, runFailure, runDurationSecs, the usage
1255
+ * window, the graph's stats). A run that is still live — queued, running,
1256
+ * or parked at a gate — is read whole, because the gate's question and its
1257
+ * events are what the approvals list shows. Anything wanting a finished
1258
+ * run's reply or trace opens it with readRun.
1259
+ *
1260
+ * This used to parse every run file of every workspace on every call, and
1261
+ * the dashboard calls it every twenty seconds per open tab.
1262
+ */
881
1263
  export declare function listAllRuns(tenant: string): RunWithWorkspace[];
1264
+ /** What a list knows about one step. */
1265
+ export interface RunSummaryStep {
1266
+ agent: string;
1267
+ group: number;
1268
+ status: StepRecord["status"];
1269
+ costUsd: number | null;
1270
+ tokens?: {
1271
+ input: number;
1272
+ output: number;
1273
+ } | null;
1274
+ computeSecs?: number | null;
1275
+ skipReason?: string;
1276
+ /** The last error event, for a failed step — what runFailure reads. */
1277
+ error?: string;
1278
+ item?: string;
1279
+ waitFor?: "event";
1280
+ ask?: string;
1281
+ /** The step's instruction, kept for a parked step so the approvals banner can show the question without the record. */
1282
+ instruction?: string;
1283
+ /** When a parked step last spoke — the moment the banner sorts by. */
1284
+ waitingSince?: string;
1285
+ startedAt?: string;
1286
+ finishedAt?: string;
1287
+ }
1288
+ /** What a list knows about one run. */
1289
+ export interface RunSummary {
1290
+ id: string;
1291
+ flow: string;
1292
+ status: RunRecord["status"];
1293
+ summary: string | null;
1294
+ startedAt: string;
1295
+ finishedAt: string | null;
1296
+ stopRequested?: boolean;
1297
+ test?: boolean;
1298
+ budgetUsd?: number | null;
1299
+ tags?: string[];
1300
+ startedBy?: string | null;
1301
+ parkedAt?: string | null;
1302
+ /** A step is parked for a person — what the approvals banner counts. */
1303
+ awaitingApproval: boolean;
1304
+ steps: RunSummaryStep[];
1305
+ }
1306
+ /** One run's row. */
1307
+ export declare function summarizeRun(run: RunRecord): RunSummary;
1308
+ /** A RunRecord with the shape a list reads and nothing else — see listAllRuns. */
1309
+ export declare function runFromSummary(s: RunSummary): RunRecord;
1310
+ /**
1311
+ * The workspace's runs as the index knows them, newest first — the call a
1312
+ * list makes. Reconciled against the directory on every read: rows for
1313
+ * records that are gone are dropped, records with no row or a moved mtime
1314
+ * are re-read, and the index is rewritten if anything changed. A missing
1315
+ * or unreadable index is a full scan that leaves one behind.
1316
+ */
1317
+ export declare function listRunSummaries(tenant: string, workspace: string): RunSummary[];
1318
+ /** Every workspace's runs as summaries, tagged with the workspace. */
1319
+ export declare function listAllRunSummaries(tenant: string): (RunSummary & {
1320
+ workspace: string;
1321
+ })[];
882
1322
  /**
883
1323
  * The status a person should see. "failed" and "someone stopped it" are
884
1324
  * different facts — the record keeps them apart (stopRequested), and the
@@ -943,6 +1383,18 @@ export declare function runFailure(run: RunRecord): {
943
1383
  * runFailure, which puts the reason for a failure on the list for the same
944
1384
  * reason — the sentence was in the record all along.
945
1385
  */
1386
+ export declare const VERDICTS: readonly ["GOOD", "BAD", "QUIET", "BLOCKED"];
1387
+ export type Verdict = (typeof VERDICTS)[number];
1388
+ /**
1389
+ * The verdict a headline leads with, or null. Leading, whole-word and
1390
+ * upper-case only — "GOOD — 6 links applied", "**BLOCKED**: nothing
1391
+ * pushed", "BAD: 3 pages dropped". A summary that merely contains the word
1392
+ * ("no BLOCKED items") has no verdict, for the reason markerPresent gives.
1393
+ */
1394
+ export declare function verdictOf(summary: string | null | undefined): Verdict | null;
1395
+ /** A run's verdict: recorded when it finished, else read from its summary,
1396
+ * so runs from before the field existed show one too. */
1397
+ export declare function runVerdict(run: Pick<RunRecord, "verdict" | "summary">): Verdict | null;
946
1398
  export declare function runSummary(run: RunRecord): string | null;
947
1399
  /**
948
1400
  * What one step concluded, in a line.
@@ -997,6 +1449,8 @@ export interface AgentStep {
997
1449
  * step-io-model, which is the pure copy the client can import. */
998
1450
  export declare function deniedPaths(step: Pick<StepRecord, "events">): number;
999
1451
  export declare function listAgentSteps(tenant: string, workspace: string, agent: string): AgentStep[];
1452
+ /** Every run of the workspace, whole, newest first. Parses every record;
1453
+ * a list that needs only what a list needs reads listRunSummaries. */
1000
1454
  export declare function listRuns(tenant: string, workspace: string): RunRecord[];
1001
1455
  /**
1002
1456
  * `provider:` — which endpoint the model calls run against, and the two