@foldrun/core 0.7.0 → 1.0.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.
@@ -352,7 +352,7 @@ export function browseEngineRefusal(engine, what) {
352
352
  }
353
353
  const BROWSE_SETTING_KEYS = [
354
354
  "engine", "user_agent", "device", "locale", "timezone", "cookies", "cookie_domain", "storage", "storage_origin", "identities", "headless", "version", "live",
355
- "allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key",
355
+ "allowed_domains", "deny", "boundaries", "init", "extensions", "webgpu", "ignore_https_errors", "state_key", "state",
356
356
  "video", "live_view", "vendor_session",
357
357
  // Deprecated spelling of vendor_session (read, and warned about).
358
358
  "session",
@@ -696,6 +696,12 @@ export function readBrowseSettings(raw) {
696
696
  settings[k] = list;
697
697
  continue;
698
698
  }
699
+ if (k === "state") {
700
+ if (typeof v !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(v))
701
+ return { settings, rest: left(rest), error: "web.browse.state is a plain name (letters, digits, dot, dash, underscore), like youtube — the login is kept in workspace/state/browser/<name>.json." };
702
+ settings.state = v;
703
+ continue;
704
+ }
699
705
  if (k === "state_key") {
700
706
  if (typeof v !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(v))
701
707
  return { settings, rest: left(rest), error: "web.browse.state_key must be the NAME of a vault secret (CAPITALS), never the key — store it with `foldrun secrets set NAME` and declare it under secrets:." };
@@ -762,6 +768,20 @@ export function readBrowseSettings(raw) {
762
768
  `would be sent to whatever site the call opens. Write \`cookie_domain: .example.com\`.`,
763
769
  };
764
770
  }
771
+ // A secret's cookies seeding a saved login: the login then sits in state/,
772
+ // a workspace file, so it must be encrypted with a key from the vault.
773
+ if (settings.state && settings.cookies && !settings.state_key) {
774
+ return {
775
+ settings,
776
+ rest: left(rest),
777
+ error: `web.browse.state beside cookies needs web.browse.state_key — the secret's session would be saved ` +
778
+ `to state/ where any step can read it. Store a key (\`foldrun secrets set BROWSER_STATE_KEY\`), ` +
779
+ `write \`state_key: BROWSER_STATE_KEY\` and declare it under secrets:.`,
780
+ };
781
+ }
782
+ if (settings.state && settings.storage) {
783
+ return { settings, rest: left(rest), error: "web.browse.state cannot be combined with storage: a secret's tokens would be written to state/." };
784
+ }
765
785
  // Storage is per origin, and seeding it into the wrong one would hand a
766
786
  // site another site's signed-in state — the same mistake `cookie_domain`
767
787
  // exists to prevent, one storage area over.
@@ -1,3 +1,4 @@
1
+ import { type HookRecord } from "./hooks.ts";
1
2
  import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
2
3
  import { type ApiSpec, type Effort } from "./store.ts";
3
4
  import type { EventExtra } from "./step-exec.ts";
@@ -52,6 +53,9 @@ export interface ContainerStepInput {
52
53
  /** `limits:` resolved host-side, and the SDK-name → foldrun-name map the
53
54
  * count reads (limits.ts) — enforced in the pod by the same hook. */
54
55
  limits?: Record<string, number>;
56
+ /** `hooks:` names, resolved inside the sandbox against its own roots. */
57
+ hookNames?: string[];
58
+ hookBase?: Record<string, unknown>;
55
59
  toolOwners?: Record<string, string>;
56
60
  /** tools: [search] — the directories to search, as paths INSIDE the
57
61
  * container (/workspace/…, /library/…). */
@@ -340,6 +344,7 @@ export declare function parseDriverLine(line: string): {
340
344
  operator?: OperatorEvent;
341
345
  subagent?: string;
342
346
  check?: boolean;
347
+ hook?: HookRecord;
343
348
  } | ({
344
349
  e: "done";
345
350
  } & ContainerStepOutcome) | null;
@@ -17,6 +17,7 @@
17
17
  //
18
18
  // The runner image bundles @foldrun/core itself, so the driver runs the same
19
19
  // executeStep the server would — one implementation, two homes.
20
+ import { isHookRecord } from "./hooks.js";
20
21
  import fs from "node:fs";
21
22
  import os from "node:os";
22
23
  import path from "node:path";
@@ -42,7 +43,9 @@ const cli = () => process.env.FOLDRUN_CONTAINER_CLI ?? "docker";
42
43
  // there: everything platform-owned (store.ts#isPlatformPath — also never
43
44
  // copied in), plus knowledge/, which goes in for reading and is dropped on
44
45
  // the way out, and .git/.
45
- const DENY_BACK = ["knowledge/", ".git/"];
46
+ // hooks/: a step must not rewrite the code that guards it — for this run (the
47
+ // code is read into memory before the model starts, hooks.ts) or the next.
48
+ const DENY_BACK = ["knowledge/", "hooks/", ".git/"];
46
49
  // A .git ANYWHERE in the tree, not just at the root. A step that clones a
47
50
  // repository into a shared area (files/site, say) was shipping the entire
48
51
  // object store back to the workspace, where the harvest then filed thousands
@@ -531,6 +534,7 @@ try {
531
534
  schema: input.schema,
532
535
  maxTurns: input.maxTurns,
533
536
  ...(input.limits && Object.keys(input.limits).length ? { limits: input.limits, toolOwners: input.toolOwners ?? {} } : {}),
537
+ ...(input.hookNames?.length ? { hookNames: input.hookNames, hookBase: input.hookBase ?? {} } : {}),
534
538
  // Slim browsing: the platform put this step on the slim image to browse
535
539
  // through the account's pod, and named the file the web tool logs its
536
540
  // reconnects to. A lost pod stops the step; what it wrote decides what
@@ -983,6 +987,7 @@ export function parseDriverLine(line) {
983
987
  ...(isOperatorEvent(parsed.operator) ? { operator: parsed.operator } : {}),
984
988
  ...(typeof parsed.subagent === "string" && /^[a-z0-9-]{1,64}$/.test(parsed.subagent) ? { subagent: parsed.subagent } : {}),
985
989
  ...(parsed.check === true ? { check: true } : {}),
990
+ ...(isHookRecord(parsed.hook) ? { hook: parsed.hook } : {}),
986
991
  };
987
992
  }
988
993
  if (parsed.e === "done") {
@@ -250,6 +250,7 @@ grantsOnly?: boolean): {
250
250
  FOLDRUN_BROWSER_WEBGPU?: string | undefined;
251
251
  FOLDRUN_BROWSER_IGNORE_HTTPS_ERRORS?: string | undefined;
252
252
  FOLDRUN_BROWSER_STATE_KEY?: string | undefined;
253
+ FOLDRUN_BROWSER_STATE?: string | undefined;
253
254
  FOLDRUN_BROWSER_VIDEO?: string | undefined;
254
255
  FOLDRUN_BROWSER_LIVE_VIEW?: string | undefined;
255
256
  };
@@ -2,6 +2,7 @@
2
2
  // group receives every prior result, labeled per agent. Optional steps ("2?")
3
3
  // fail without failing the flow. A direct agent run is a one-step ad-hoc
4
4
  // flow, so there is a single execution path.
5
+ import { hookNames } from "./hooks.js";
5
6
  import fs from "node:fs";
6
7
  import os from "node:os";
7
8
  import path from "node:path";
@@ -1214,6 +1215,7 @@ grantsOnly = false) {
1214
1215
  ...(browseSettings.webgpu ? { FOLDRUN_BROWSER_WEBGPU: "1" } : {}),
1215
1216
  ...(browseSettings.ignore_https_errors ? { FOLDRUN_BROWSER_IGNORE_HTTPS_ERRORS: "1" } : {}),
1216
1217
  ...(browseSettings.state_key ? { FOLDRUN_BROWSER_STATE_KEY: browseSettings.state_key } : {}),
1218
+ ...(browseSettings.state ? { FOLDRUN_BROWSER_STATE: browseSettings.state } : {}),
1217
1219
  ...(browseSettings.video ? { FOLDRUN_BROWSER_VIDEO: "1" } : {}),
1218
1220
  ...(browseSettings.live_view === false ? { FOLDRUN_BROWSER_LIVE_VIEW: "0" } : {}),
1219
1221
  };
@@ -2167,6 +2169,18 @@ stepIndex) {
2167
2169
  const limitOpts = Object.keys(stepLimits).length ? { limits: stepLimits, toolOwners: limitOwners } : {};
2168
2170
  if (limitOpts.limits)
2169
2171
  push("info", `limits: ${Object.entries(stepLimits).map(([k, n]) => `${k} ${n}`).join(", ")} — per step; a call past one is refused`);
2172
+ // `hooks:` — the account's, the workspace's, then the agent's own, each
2173
+ // once; resolved and run inside the step (hooks.ts, step-exec.ts).
2174
+ const stepHookNames = [...new Set([
2175
+ ...hookNames(readAgentsMd(accountDir(tenant))?.data?.hooks),
2176
+ ...hookNames(readAgentsMd(workspaceRootOf(agentDir))?.data?.hooks),
2177
+ ...hookNames(front.hooks),
2178
+ ])];
2179
+ const hookOpts = stepHookNames.length
2180
+ ? { hookNames: stepHookNames, hookBase: { run_id: runId ?? null, step: stepIndex ?? null, agent: step.agent, workspace: path.basename(workspaceRootOf(agentDir)) } }
2181
+ : {};
2182
+ if (stepHookNames.length)
2183
+ push("info", `hooks: ${stepHookNames.join(", ")}`);
2170
2184
  const isolation = process.env.FOLDRUN_RUN_ISOLATION;
2171
2185
  if (isolatedRun()) {
2172
2186
  // The isolated path: the whole loop — model, built-in tools, scripts —
@@ -2325,6 +2339,7 @@ stepIndex) {
2325
2339
  schema: resolvedSchema ?? undefined,
2326
2340
  maxTurns: step.maxTurns,
2327
2341
  ...limitOpts,
2342
+ ...hookOpts,
2328
2343
  translator: keyName === MODEL_KEY_NAME ? primaryTranslator : secondTranslator,
2329
2344
  // Outward steps keep a browser of their own: an executor that
2330
2345
  // could browse from the slim image through the account's pod
@@ -2683,6 +2698,7 @@ stepIndex) {
2683
2698
  schema: resolvedSchema ?? undefined,
2684
2699
  maxTurns: step.maxTurns,
2685
2700
  ...limitOpts,
2701
+ ...hookOpts,
2686
2702
  stopRequested,
2687
2703
  // What the scripts saw, the verify sees: a flow can then check that
2688
2704
  // the proof a step left names THIS run, not one that came before —
@@ -263,6 +263,7 @@ only when they asked for exactly that deletion.
263
263
  | \`memory/\` | what past runs learned — agents write here themselves |
264
264
  | \`evals/\` | tests for agents, same idea as unit tests |
265
265
  | \`skills/\` | a procedure an agent can follow, named in \`skills:\` |
266
+ | \`hooks/\` | your code at every tool call, step start or step end (\`hook.md\` + \`run.mjs\`), turned on with \`hooks:\` — refuse, rewrite, add context; same on every model |
266
267
  | \`tools/\` | a folder tool: its definition and its code together |
267
268
  | \`scripts/\` | code the agents call, referenced by \`scripts:\` |
268
269
  | \`state/\` | what runs accumulate. Yours to read, never deployed over |
@@ -439,6 +440,7 @@ size: large # small | large | heavy — the sandbox it rents
439
440
  tools: [web, read, ask, my-folder-tool] # built-ins and your own tools/, one list; \`ask\` lets it ask you mid-step
440
441
  disallowedTools: [Bash] # subtract from what it would otherwise have
441
442
  skills: [house-style] # from skills/
443
+ hooks: [no-force-push] # from hooks/ or the library: code at every tool call
442
444
  scripts: [summarise.py] # from scripts/, each becomes a callable tool
443
445
  apis: [{ name: crm }] # an HTTP API, as a tool
444
446
  mcpServers: {} # an MCP server, as a tool
@@ -1,3 +1,4 @@
1
+ import { type HookDef, type HookRecord } from "./hooks.ts";
1
2
  import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
2
3
  import type { Effort } from "./store.ts";
3
4
  import type { TestEffect } from "./test-mode.ts";
@@ -40,6 +41,14 @@ export interface ExecOutcome {
40
41
  browserPod?: BrowserPodOutcome;
41
42
  }
42
43
  export interface ExecOptions {
44
+ /** `hooks:` resolved host-side (hooks.ts): run at this step's own dispatch,
45
+ * inside its sandbox. `hookBase` is what every hook event carries. */
46
+ hooks?: HookDef[];
47
+ /** Or the names (`hooks:` from the account, workspace and agent), resolved
48
+ * here against this step's own roots — in a sandbox those are not the
49
+ * host's paths. A name that resolves to nothing refuses the step. */
50
+ hookNames?: string[];
51
+ hookBase?: Record<string, unknown>;
43
52
  agentDir: string;
44
53
  workspaceRoot: string;
45
54
  libraryRoot: string;
@@ -151,6 +160,7 @@ export type EventExtra = {
151
160
  operator?: OperatorEvent;
152
161
  subagent?: string;
153
162
  check?: boolean;
163
+ hook?: HookRecord;
154
164
  };
155
165
  /** Conservative per-token rates for a model nothing else can price — an
156
166
  * unknown id on a gateway with no catalogue. Opus-class, so a ceiling
@@ -7,6 +7,7 @@
7
7
  // assembles the system prompt and builds MCP servers, then hands this the
8
8
  // results. That is what lets the same function run somewhere the vault, the
9
9
  // library and the account do not exist.
10
+ import { runHook, runToolCallHooks, matchesTool, hookLine, resolveHooks } from "./hooks.js";
10
11
  import fs from "node:fs";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
@@ -169,8 +170,49 @@ async function executeStepInner(opts, runQuery) {
169
170
  // `limits:` — this step's counts. A new step, a retry of one, or a
170
171
  // fan-out instance each start from zero.
171
172
  const counter = new CallCounter(opts.limits);
173
+ // Hooks (hooks.ts): the person's own code at this dispatch, same on every
174
+ // model. Each runs in the agent's directory with a bare environment.
175
+ let hooks = opts.hooks ?? [];
176
+ if (opts.hookNames?.length) {
177
+ const r = resolveHooks({ workspaceRoot, libraryRoot, names: opts.hookNames });
178
+ const bad = [...r.missing.map((n) => `no hook named ${n} in hooks/ or the library`), ...r.errors];
179
+ if (bad.length) {
180
+ emit("error", `hooks: ${bad.join("; ")} — the step is refused rather than run without its hooks`);
181
+ return { status: "failed", result: null, conclusion: null, costUsd: null, usage: null };
182
+ }
183
+ hooks = [...hooks, ...r.hooks];
184
+ }
185
+ const hookBase = { ...(opts.hookBase ?? {}) };
186
+ const callHook = (hook, event) => runHook(hook, event, { cwd: agentDir, env: opts.env });
187
+ const record = (r, via) => emit(r.did === "error" || r.did === "deny" ? "error" : "info", hookLine(r), { hook: r, ...(via ? { subagent: via } : {}) });
188
+ // step.start — a gate: context it returns goes in front of the task; a
189
+ // hook that fails refuses the step rather than run it unguarded.
190
+ let prompt = opts.prompt;
191
+ for (const hook of hooks.filter((h) => h.on === "step.start")) {
192
+ const a = await callHook(hook, { ...hookBase, event: "step.start", hook: hook.name, prompt });
193
+ if (!a.ok) {
194
+ record({ hook: hook.name, event: "step.start", did: "error", reason: `${a.failure}, so the step is refused`, ms: a.ms });
195
+ return { status: "failed", result: null, conclusion: null, costUsd: null, usage: null };
196
+ }
197
+ // Exit 2 is a deny on every event (runHook). On a gate that means the
198
+ // step does not run — it used to come back ok with no context, and the
199
+ // step went ahead as if the hook had agreed.
200
+ if (a.decision === "deny") {
201
+ record({ hook: hook.name, event: "step.start", did: "deny", reason: `${a.reason}, so the step is refused`, ms: a.ms });
202
+ return { status: "failed", result: null, conclusion: null, costUsd: null, usage: null };
203
+ }
204
+ if (a.context) {
205
+ prompt = `${prompt}\n\n${a.context}`;
206
+ record({ hook: hook.name, event: "step.start", did: "context", ms: a.ms });
207
+ }
208
+ else if (a.note) {
209
+ record({ hook: hook.name, event: "step.start", did: "note", reason: a.note, ms: a.ms });
210
+ }
211
+ }
212
+ const toolHooks = hooks.filter((h) => h.on === "tool.call");
213
+ const resultHooks = hooks.filter((h) => h.on === "tool.result");
172
214
  const q = runQuery({
173
- prompt: opts.prompt,
215
+ prompt,
174
216
  options: {
175
217
  cwd: agentDir,
176
218
  abortController: abort,
@@ -272,6 +314,46 @@ async function executeStepInner(opts, runQuery) {
272
314
  return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: refusal.message } };
273
315
  }
274
316
  }
317
+ // tool.call hooks — after foldrun's own checks, so a hook never
318
+ // sees a call that was going to be refused anyway, and handed the
319
+ // input as the tool would get it (the workspace/ prefix expanded).
320
+ if (toolHooks.length) {
321
+ const input = (verdict?.updatedInput ?? hookInput.tool_input ?? {});
322
+ // A sub-agent's call says so: a hook that guards the step's own
323
+ // model differently from its delegates can tell them apart.
324
+ const v = await runToolCallHooks(toolHooks, { tool: hookInput.tool_name, input, base: via ? { ...hookBase, subagent: via } : hookBase }, callHook);
325
+ for (const r of v.records)
326
+ record(r, via);
327
+ const refundCall = () => { if (counter.active)
328
+ counter.refund(limitKeysFor(hookInput.tool_name, hookInput.tool_input, opts.toolOwners)); };
329
+ if (v.decision === "deny") {
330
+ refundCall();
331
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: v.reason } };
332
+ }
333
+ if (v.input) {
334
+ // A rewritten call is answered "allow", and an allow skips
335
+ // canUseTool — where the shell and path confinement live. So
336
+ // the rewrite goes through the same checks here: a hook can
337
+ // narrow what the agent does, never widen it.
338
+ const again = hookInput.tool_name === "Bash"
339
+ ? checkBash(String(v.input.command ?? ""))
340
+ : isFilesystemTool(hookInput.tool_name)
341
+ ? checkPaths(hookInput.tool_name, v.input, { agentDir, workspaceRoot, libraryRoot })
342
+ : { ok: true };
343
+ const granted = opts.allowed.includes(hookInput.tool_name) || opts.mcpNames.some((n) => hookInput.tool_name.startsWith(`mcp__${n}__`));
344
+ // The sub-agent guard read the ORIGINAL input: a rewrite of a
345
+ // delegation could otherwise swap subagent_type for one this
346
+ // step never declared (or the SDK's general-purpose).
347
+ const delegation = guardSubagent ? guardSubagent({ agentId, agentType, tool: hookInput.tool_name, input: v.input }) : null;
348
+ if (!again.ok || !granted || delegation) {
349
+ refundCall();
350
+ const why = !granted ? `Tool ${hookInput.tool_name} is not enabled for this agent.` : delegation ? `the rewritten call: ${delegation}` : `the rewritten call: ${again.reason}`;
351
+ emit("error", why, via ? { subagent: via } : undefined);
352
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: why } };
353
+ }
354
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: again.updatedInput ?? v.input } };
355
+ }
356
+ }
275
357
  if (!verdict?.updatedInput)
276
358
  return {};
277
359
  return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: verdict.updatedInput } };
@@ -281,12 +363,26 @@ async function executeStepInner(opts, runQuery) {
281
363
  // after its next tool call, as context — never as a change to what it
282
364
  // may do. A model that is only writing, calling no tools, hears it at
283
365
  // the next call it makes.
284
- ...(opts.inbox
366
+ ...(opts.inbox || resultHooks.length
285
367
  ? {
286
368
  PostToolUse: [{
287
369
  hooks: [async (hookInput) => {
288
370
  if (hookInput.hook_event_name !== "PostToolUse")
289
371
  return {};
372
+ // tool.result hooks watch: the call has happened, so one
373
+ // that fails is recorded and the step goes on.
374
+ for (const hook of resultHooks) {
375
+ if (!matchesTool(hook.tool, hookInput.tool_name))
376
+ continue;
377
+ const response = hookInput.tool_response;
378
+ const a = await callHook(hook, { ...hookBase, event: "tool.result", hook: hook.name, tool: { name: hookInput.tool_name, input: hookInput.tool_input ?? {} }, result: response ?? null });
379
+ if (!a.ok)
380
+ record({ hook: hook.name, event: "tool.result", did: "error", tool: hookInput.tool_name, reason: a.failure, ms: a.ms });
381
+ else if (a.note)
382
+ record({ hook: hook.name, event: "tool.result", did: "note", tool: hookInput.tool_name, reason: a.note, ms: a.ms });
383
+ }
384
+ if (!opts.inbox)
385
+ return {};
290
386
  // A sub-agent's call: the message is for the step's own
291
387
  // model, and draining it here would hand it to a context
292
388
  // that ends with the delegation — and lose it.
@@ -614,6 +710,21 @@ async function executeStepInner(opts, runQuery) {
614
710
  status = "failed";
615
711
  emit("error", "the model stream ended without a result — the model process likely died (out of memory?)");
616
712
  }
713
+ // step.end hooks watch the reply, and may rewrite it. One that fails is
714
+ // recorded and the reply stands: the work it describes is done.
715
+ if (status === "completed") {
716
+ for (const hook of hooks.filter((h) => h.on === "step.end")) {
717
+ const a = await callHook(hook, { ...hookBase, event: "step.end", hook: hook.name, reply: texts.join("\n").trim() });
718
+ if (!a.ok)
719
+ record({ hook: hook.name, event: "step.end", did: "error", reason: a.failure, ms: a.ms });
720
+ else if (a.reply !== undefined) {
721
+ texts.splice(0, texts.length, a.reply);
722
+ record({ hook: hook.name, event: "step.end", did: "reply", ms: a.ms });
723
+ }
724
+ else if (a.note)
725
+ record({ hook: hook.name, event: "step.end", did: "note", reason: a.note, ms: a.ms });
726
+ }
727
+ }
617
728
  const result = texts.join("\n").trim() || null;
618
729
  // A step's reply is not one message: the model narrates ("Now let me read
619
730
  // the outputs…"), calls tools, and answers last. Joining those and reading
@@ -20,7 +20,7 @@ import { type WireFormat, type AuthShape } from "./providers.ts";
20
20
  * core needs to know about which one it's in.
21
21
  */
22
22
  export { singleWorkspace };
23
- export declare const WORKSPACE_DIRS: readonly ["agents", "flows", "evals", "skills", "memory", "knowledge", "tools", "scripts", "state"];
23
+ export declare const WORKSPACE_DIRS: readonly ["agents", "flows", "evals", "skills", "memory", "knowledge", "tools", "scripts", "hooks", "state"];
24
24
  export declare const STORAGE_DIR = "storage";
25
25
  /** What this directory was called before. Read-only: nothing writes here any
26
26
  * more, but installs created before the rename have bytes under it, and a
@@ -893,6 +893,16 @@ export interface RunEvent {
893
893
  * detail), `output: json`, a schema. What the runner reads to decline a
894
894
  * retry of an outward step whose tools already ran. */
895
895
  check?: boolean;
896
+ /** Set on the line a hook wrote — a deny, a rewrite, context, a failure
897
+ * (hooks.ts). The run page and the Hooks page read these. */
898
+ hook?: {
899
+ hook: string;
900
+ event: string;
901
+ did: string;
902
+ tool?: string;
903
+ reason?: string;
904
+ ms: number;
905
+ };
896
906
  }
897
907
  /** A question an agent asked a person while its step ran (tools: [ask]). */
898
908
  export interface StepQuestion {
package/dist/src/store.js CHANGED
@@ -74,6 +74,7 @@ export const WORKSPACE_DIRS = [
74
74
  "knowledge",
75
75
  "tools",
76
76
  "scripts",
77
+ "hooks",
77
78
  "state",
78
79
  ];
79
80
  const IN_WORKSPACE_DIR = new RegExp(`^(${WORKSPACE_DIRS.join("|")})/`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldrun/core",
3
- "version": "0.7.0",
3
+ "version": "1.0.0",
4
4
  "description": "Agents are just folders. The format, the runner, the checks.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,6 +15,23 @@
15
15
  "default": "./dist/src/*.js"
16
16
  }
17
17
  },
18
+ "foldrun": {
19
+ "publicApi": [
20
+ ".",
21
+ "./git-hook",
22
+ "./agents-md",
23
+ "./browser-pod",
24
+ "./flow-patterns",
25
+ "./history",
26
+ "./hooks",
27
+ "./kinds",
28
+ "./layout",
29
+ "./limits",
30
+ "./providers",
31
+ "./subagents",
32
+ "./tool-names"
33
+ ]
34
+ },
18
35
  "main": "./dist/index.js",
19
36
  "types": "./dist/index.d.ts",
20
37
  "files": [