@foldrun/core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/agent-import.d.ts +21 -0
- package/dist/src/agent-import.js +116 -0
- package/dist/src/api-tools.js +14 -2
- package/dist/src/approvals.d.ts +8 -1
- package/dist/src/approvals.js +10 -5
- package/dist/src/browser-pod.d.ts +20 -1
- package/dist/src/browser-pod.js +58 -9
- package/dist/src/completions.js +1 -0
- package/dist/src/confine.d.ts +1 -0
- package/dist/src/confine.js +33 -4
- package/dist/src/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +9 -0
- package/dist/src/deploy.js +31 -3
- package/dist/src/evals.js +24 -4
- package/dist/src/host-env.d.ts +14 -0
- package/dist/src/host-env.js +31 -0
- package/dist/src/install-helper.d.ts +17 -0
- package/dist/src/install-helper.js +134 -0
- package/dist/src/install-tool.d.ts +128 -0
- package/dist/src/install-tool.js +133 -0
- package/dist/src/mail.js +3 -1
- package/dist/src/model-credential.d.ts +30 -0
- package/dist/src/model-credential.js +98 -0
- package/dist/src/model-env.d.ts +17 -0
- package/dist/src/model-env.js +57 -0
- package/dist/src/package.d.ts +67 -0
- package/dist/src/package.js +594 -0
- package/dist/src/platform.d.ts +5 -0
- package/dist/src/platform.js +1 -0
- package/dist/src/provider-health.js +3 -0
- package/dist/src/run-container.d.ts +6 -1
- package/dist/src/run-container.js +93 -20
- package/dist/src/runner.d.ts +239 -0
- package/dist/src/runner.js +380 -72
- package/dist/src/runtime.d.ts +77 -0
- package/dist/src/runtime.js +230 -5
- package/dist/src/script-tools.d.ts +16 -0
- package/dist/src/script-tools.js +50 -8
- package/dist/src/secrets.d.ts +5 -0
- package/dist/src/secrets.js +37 -6
- package/dist/src/starter.d.ts +12 -0
- package/dist/src/starter.js +36 -4
- package/dist/src/step-exec.d.ts +22 -2
- package/dist/src/step-exec.js +70 -7
- package/dist/src/storage.d.ts +7 -1
- package/dist/src/storage.js +14 -1
- package/dist/src/store.d.ts +24 -0
- package/dist/src/store.js +91 -10
- package/dist/src/subagents.js +1 -1
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- package/dist/src/tool-names.d.ts +3 -1
- package/dist/src/tool-names.js +4 -2
- package/dist/src/tool-test.js +8 -1
- package/dist/src/zip.d.ts +17 -0
- package/dist/src/zip.js +184 -0
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/agent-import.ts +154 -0
- package/src/api-tools.ts +16 -1
- package/src/approvals.ts +10 -3
- package/src/browser-pod.ts +46 -9
- package/src/completions.ts +1 -0
- package/src/confine.ts +36 -5
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +28 -3
- package/src/evals.ts +23 -2
- package/src/host-env.ts +32 -0
- package/src/install-helper.ts +143 -0
- package/src/install-tool.ts +154 -0
- package/src/mail.ts +3 -1
- package/src/model-credential.ts +112 -0
- package/src/model-env.ts +66 -0
- package/src/package.ts +664 -0
- package/src/platform.ts +6 -0
- package/src/provider-health.ts +3 -0
- package/src/run-container.ts +92 -19
- package/src/runner.ts +385 -72
- package/src/runtime.ts +235 -5
- package/src/script-tools.ts +48 -6
- package/src/secrets.ts +34 -6
- package/src/starter.ts +42 -4
- package/src/step-exec.ts +75 -10
- package/src/storage.ts +13 -0
- package/src/store.ts +91 -9
- package/src/subagents.ts +1 -1
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +4 -2
- package/src/tool-test.ts +8 -1
- package/src/zip.ts +189 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +3 -3
- package/templates/hello/agents/notetaker/agent.md +2 -2
package/dist/src/starter.js
CHANGED
|
@@ -40,6 +40,10 @@ foldrun_version: "0.1"
|
|
|
40
40
|
# that declares the same key replaces it *whole* — base_url and token never
|
|
41
41
|
# merge across scopes, so a provider block belongs entirely at one level.
|
|
42
42
|
#
|
|
43
|
+
# Your own model key needs no block: Settings → Model provider, or
|
|
44
|
+
# \`foldrun account model set anthropic --key …\` (an API key, not a login).
|
|
45
|
+
# A provider: block here or lower wins over it.
|
|
46
|
+
#
|
|
43
47
|
# provider:
|
|
44
48
|
# base_url: https://openrouter.ai/api
|
|
45
49
|
# token: \${OPENROUTER_API_KEY}
|
|
@@ -326,13 +330,14 @@ All of it, \`/api/workspaces/<workspace>\` unless noted:
|
|
|
326
330
|
| \`/runs/<id>/message\` | POST | tell a running step something; \`step\` picks one of a parallel group |
|
|
327
331
|
| \`/flows\` · \`/flows/<f>\` | GET POST · POST DELETE PATCH | list, create, edit |
|
|
328
332
|
| \`/flows/<f>/run\` | POST | start a flow |
|
|
329
|
-
| \`/agents\` · \`/agents/<a>/run\` | GET POST · POST | list agents, run one |
|
|
333
|
+
| \`/agents\` · \`/agents/<a>/run\` · \`/agents/import\` | GET POST · POST · GET POST | list agents, run one, copy one in from another workspace |
|
|
330
334
|
| \`/evals\` · \`/evals/<e>/run\` | GET POST · POST | list evals, run one |
|
|
331
335
|
| \`/tools/<t>/test\` | POST | exercise one tool alone |
|
|
332
336
|
| \`/storage\` · \`/storage/download\` · \`/storage/upload-url\` · \`/storage/preview\` | GET POST PUT DELETE · GET · POST · GET | workspace files, any type; \`preview\` returns one parsed for reading |
|
|
333
337
|
| \`/assets\` | POST | upload an asset |
|
|
334
338
|
| \`/hooks/<f>/rotate\` | POST | new webhook token for a flow |
|
|
335
339
|
| \`/notify/deliveries\` · \`/notify/deliveries/<id>/redeliver\` | GET · POST | every notify: webhook sent and each attempt; send one again |
|
|
340
|
+
| \`/export\` · \`/import\` | GET · POST | the workspace, a flow (\`?kind=flow&name=\`) or an agent as a .zip; take one in (\`?dryRun=1\` first, \`?overwrite=1\` to replace) |
|
|
336
341
|
| \`/restore\` | POST | the workspace's source back to a point in its history — \`dryRun\` first, then \`confirm\` |
|
|
337
342
|
| \`/vocabulary\` | GET | what this workspace's documents may say |
|
|
338
343
|
|
|
@@ -442,10 +447,11 @@ subagents: [researcher] # colleagues it may delegate a job to (own context,
|
|
|
442
447
|
secrets: [CRM_TOKEN] # keys the agent uses itself; a tool's own come with the tool
|
|
443
448
|
limits: {web.search: 40, crm: 20, calls: 300} # per step; a call past a limit is refused, never run
|
|
444
449
|
timezone: Australia/Sydney # the calendar it works to — IANA name or UTC+10
|
|
445
|
-
provider: {} # BYOK:
|
|
450
|
+
provider: {} # BYOK for this agent; account-wide: \`foldrun account model set\`
|
|
446
451
|
permissionMode: plan # plan first, act once approved
|
|
447
452
|
runtime: # only if scripts need packages
|
|
448
453
|
packages: [requests]
|
|
454
|
+
system: [ffmpeg] # Debian packages, apt-installed before the step
|
|
449
455
|
---
|
|
450
456
|
|
|
451
457
|
You research a topic and report what you found...
|
|
@@ -459,8 +465,12 @@ flow's, else the workspace's AGENTS.md, else the account's, else UTC. It sets
|
|
|
459
465
|
\`TZ\` and the date the agent is told, so a Sydney desk stamps the Sydney date.
|
|
460
466
|
|
|
461
467
|
\`runtime:\` installs pip/npm packages in the sandbox each agent runs in. Pin
|
|
462
|
-
versions the normal way (\`pandas>=2\`, \`lodash@^4\`).
|
|
463
|
-
|
|
468
|
+
versions the normal way (\`pandas>=2\`, \`lodash@^4\`). \`system:\` adds Debian
|
|
469
|
+
packages \u{2014} a program (\`ffmpeg\`), a library's headers for a pip package that
|
|
470
|
+
compiles (\`libcairo2-dev\`, \`python3-dev\`), \`build-essential\` \u{2014} installed with
|
|
471
|
+
apt before the step starts and cached, so later runs only unpack them. An agent that
|
|
472
|
+
finds it needs something mid-run installs it itself with the \`install\` tool
|
|
473
|
+
(apt, pip or npm), which every agent has; declaring is only a speed-up.
|
|
464
474
|
|
|
465
475
|
Link documents to each other with \`[[wikilinks]]\` — the name, not the path.
|
|
466
476
|
`,
|
|
@@ -482,3 +492,25 @@ expect:
|
|
|
482
492
|
},
|
|
483
493
|
];
|
|
484
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* A new workspace with nothing in it to delete: its AGENTS.md, the
|
|
497
|
+
* .gitignore that keeps its secret key out of git, and the CLAUDE.md a coding
|
|
498
|
+
* tool reads. No example agents or flow.
|
|
499
|
+
*
|
|
500
|
+
* The starter's researcher and writer were a demo, and a person making a
|
|
501
|
+
* workspace for real work met them as clutter — "why does a new workspace
|
|
502
|
+
* have agents?" (owner, 2026-10-05). The agents worth having are usually ones
|
|
503
|
+
* already written in another workspace, which `agents/import` copies in; the
|
|
504
|
+
* example is still one flag away (`foldrun new <name> --starter`).
|
|
505
|
+
*/
|
|
506
|
+
export function blankWorkspaceFiles(workspace) {
|
|
507
|
+
const keep = new Set(["AGENTS.md", ".gitignore", "CLAUDE.md"]);
|
|
508
|
+
return starterFiles(workspace)
|
|
509
|
+
.filter((f) => keep.has(f.path))
|
|
510
|
+
.map((f) => f.path === "AGENTS.md"
|
|
511
|
+
? {
|
|
512
|
+
...f,
|
|
513
|
+
content: f.content.replace("description: A starter workspace — edit the agents, then run the flow.", "description: A new workspace — add an agent or import one from another workspace, then write a flow."),
|
|
514
|
+
}
|
|
515
|
+
: f);
|
|
516
|
+
}
|
package/dist/src/step-exec.d.ts
CHANGED
|
@@ -238,8 +238,8 @@ export declare function checkVerify(agentDir: string, verify: string, ctx: {
|
|
|
238
238
|
* and `matches:` test this, because it is also what the run's headline
|
|
239
239
|
* is read from: a reporter that narrated between tool calls ("Now let
|
|
240
240
|
* me write the report…") failed `matches: ^BAD\b` with a correct
|
|
241
|
-
* headline while the check read every turn joined. `judge:`
|
|
242
|
-
* the whole result. */
|
|
241
|
+
* headline while the check read every turn joined. `judge:` grades this
|
|
242
|
+
* too, as "the reply", with the whole result beside it as the work. */
|
|
243
243
|
conclusion?: string | null;
|
|
244
244
|
data?: unknown;
|
|
245
245
|
/** The step's own model environment, for `judge:` — it grades on the
|
|
@@ -249,3 +249,23 @@ export declare function checkVerify(agentDir: string, verify: string, ctx: {
|
|
|
249
249
|
* person asked the run to stop. */
|
|
250
250
|
signal?: AbortSignal;
|
|
251
251
|
}): Promise<VerifyVerdict>;
|
|
252
|
+
/**
|
|
253
|
+
* A toolless, fast-tier grading call: does the reply satisfy the sentence?
|
|
254
|
+
* Same shape as the eval judge, and answered with one word first so the
|
|
255
|
+
* verdict is a prefix test rather than a reading.
|
|
256
|
+
*/
|
|
257
|
+
export declare function judgePrompt(rubric: string, reply: string, work: string): string;
|
|
258
|
+
/**
|
|
259
|
+
* The environment an Agent SDK session starts with: the step's, minus the
|
|
260
|
+
* connectors of whoever is signed in to claude.ai on this machine. Claude
|
|
261
|
+
* Code loads an account's claude.ai MCP servers (Docs, Drive, …) unless told
|
|
262
|
+
* not to, and `settingSources: []` does not reach them — they come with the
|
|
263
|
+
* login, not from a settings file. A local `foldrun run` handed every agent
|
|
264
|
+
* the person's own connectors: the hello template's agents reported that
|
|
265
|
+
* "the only tools I have are the Claude Docs ones". `strictMcpConfig` keeps
|
|
266
|
+
* the rest of the on-disk MCP configuration out; this keeps the account's.
|
|
267
|
+
*/
|
|
268
|
+
export declare function sdkEnv(base: Record<string, string | undefined> | undefined, extra?: Record<string, string>): Record<string, string | undefined>;
|
|
269
|
+
/** The text of a tool result, whichever shape it came in: a string, or a
|
|
270
|
+
* list of content blocks of which only the text ones say anything. */
|
|
271
|
+
export declare function toolResultText(content: unknown): string;
|
package/dist/src/step-exec.js
CHANGED
|
@@ -188,6 +188,13 @@ async function executeStepInner(opts, runQuery) {
|
|
|
188
188
|
// declares no tools gets none, instead of seeing the full Claude
|
|
189
189
|
// Code toolset and burning turns on denied calls.
|
|
190
190
|
tools: opts.allowed,
|
|
191
|
+
// Stated, never left to the SDK. Unset, it chose its own mode for the
|
|
192
|
+
// login it ran under, and in auto mode its classifier refused a granted
|
|
193
|
+
// script before canUseTool saw it: "denied by the Claude Code auto mode
|
|
194
|
+
// classifier. Reason: [Irreversible Local Destruction]" for backlink-
|
|
195
|
+
// desk's outreach_reset, which only marks old bodies STALE (2026-10-04
|
|
196
|
+
// and 05). The decision is ours: canUseTool and the PreToolUse hook.
|
|
197
|
+
permissionMode: "default",
|
|
191
198
|
// Deliberately NOT allowedTools: listing them there auto-approves and
|
|
192
199
|
// skips canUseTool entirely, which is where confinement happens. The
|
|
193
200
|
// toolset is still restricted by `tools` above.
|
|
@@ -213,8 +220,10 @@ async function executeStepInner(opts, runQuery) {
|
|
|
213
220
|
// With sub-agents, the SDK would also offer its built-in ones
|
|
214
221
|
// (general-purpose has every tool the session has). Only the declared
|
|
215
222
|
// ones may run; the guard in the hook below refuses the rest as well.
|
|
216
|
-
env:
|
|
223
|
+
env: sdkEnv(opts.env, subagents ? { CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS: "1" } : {}),
|
|
217
224
|
mcpServers: opts.mcpServers,
|
|
225
|
+
// Only the servers foldrun passes. See sdkEnv for the claude.ai half.
|
|
226
|
+
strictMcpConfig: true,
|
|
218
227
|
// canUseTool is only asked when the SDK wants permission, and it never
|
|
219
228
|
// asks for a read inside the cwd — so `Read workspace/storage/x` went
|
|
220
229
|
// to `<agentDir>/workspace/storage/x` unchecked and unrewritten. On
|
|
@@ -509,12 +518,25 @@ async function executeStepInner(opts, runQuery) {
|
|
|
509
518
|
...(block.is_error ? { err: true } : {}),
|
|
510
519
|
...(open.subagent ? { subagent: open.subagent } : {}),
|
|
511
520
|
});
|
|
521
|
+
// Why it failed, on the trail. The span above carried only `err`,
|
|
522
|
+
// so a refused call left the agent's own guess as the only account
|
|
523
|
+
// of it: backlink-desk's outreach_reset "denied by the permission
|
|
524
|
+
// classifier" (run-muufboma-2t6l, 2026-10-05) could not be checked.
|
|
525
|
+
if (block.is_error) {
|
|
526
|
+
const why = toolResultText(block.content).replace(/\s+/g, " ").trim().slice(0, 300);
|
|
527
|
+
if (why)
|
|
528
|
+
emit("error", `${open.name}: ${why}`, { call: block.tool_use_id, ...(open.subagent ? { subagent: open.subagent } : {}) });
|
|
529
|
+
}
|
|
512
530
|
readPod(block.tool_use_id);
|
|
513
531
|
}
|
|
514
532
|
}
|
|
515
533
|
}
|
|
516
534
|
else if (message.type === "result") {
|
|
517
535
|
status = message.subtype === "success" ? "completed" : "failed";
|
|
536
|
+
// Calls the SDK itself refused, which canUseTool never saw.
|
|
537
|
+
for (const d of ("permission_denials" in message && Array.isArray(message.permission_denials) ? message.permission_denials : [])) {
|
|
538
|
+
emit("error", `refused before it ran: ${String(d.tool_name ?? "a tool")}`);
|
|
539
|
+
}
|
|
518
540
|
if (message.subtype === "error_max_turns") {
|
|
519
541
|
emit("error", `stopped after ${opts.maxTurns ?? "its"} turns (max_turns: in the flow file) — the step did not finish`);
|
|
520
542
|
}
|
|
@@ -799,7 +821,13 @@ export async function checkVerify(agentDir, verify, ctx) {
|
|
|
799
821
|
return { ok, headline: ok ? "present and non-empty" : `${value} is missing or empty`, detail: "" };
|
|
800
822
|
}
|
|
801
823
|
case "judge": {
|
|
802
|
-
|
|
824
|
+
// The reply is the final turn, as everywhere else; every turn goes
|
|
825
|
+
// beside it as the work, so a claim about what the step did can still
|
|
826
|
+
// be checked. Both as one <reply> failed `judge: the reply opens with
|
|
827
|
+
// GOOD` on a reply that did (fix-desk, run-muwevg72-7mza): the joined
|
|
828
|
+
// text opened with the narration before it.
|
|
829
|
+
const reply = ctx.conclusion?.trim() ? ctx.conclusion : output;
|
|
830
|
+
const verdict = await judgeReply(value, reply, output, ctx.modelEnv ?? {}, ctx.signal);
|
|
803
831
|
const ok = /^\s*PASS\b/i.test(verdict);
|
|
804
832
|
return { ok, headline: ok ? "PASS" : "FAIL", detail: verdict.slice(0, 300) };
|
|
805
833
|
}
|
|
@@ -811,7 +839,18 @@ export async function checkVerify(agentDir, verify, ctx) {
|
|
|
811
839
|
* Same shape as the eval judge, and answered with one word first so the
|
|
812
840
|
* verdict is a prefix test rather than a reading.
|
|
813
841
|
*/
|
|
814
|
-
|
|
842
|
+
export function judgePrompt(rubric, reply, work) {
|
|
843
|
+
const shown = reply.slice(0, 20_000);
|
|
844
|
+
const context = work.trim() && work.trim() !== reply.trim()
|
|
845
|
+
? `Everything the step wrote while working, for context. "The reply" means only the reply block below, ` +
|
|
846
|
+
`never this:\n<work>\n${work.slice(-20_000)}\n</work>\n\n`
|
|
847
|
+
: "";
|
|
848
|
+
return (`You are grading a reply against one requirement. Answer with PASS or FAIL as the ` +
|
|
849
|
+
`first word, then one sentence of reason.\n\nRequirement: ${rubric}\n\n` +
|
|
850
|
+
context +
|
|
851
|
+
`<reply>\n${shown}\n</reply>`);
|
|
852
|
+
}
|
|
853
|
+
async function judgeReply(rubric, reply, work, env, signal) {
|
|
815
854
|
const texts = [];
|
|
816
855
|
const abortController = new AbortController();
|
|
817
856
|
if (signal?.aborted)
|
|
@@ -819,15 +858,14 @@ async function judgeReply(rubric, output, env, signal) {
|
|
|
819
858
|
signal?.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
820
859
|
try {
|
|
821
860
|
const q = query({
|
|
822
|
-
prompt:
|
|
823
|
-
`first word, then one sentence of reason.\n\nRequirement: ${rubric}\n\n` +
|
|
824
|
-
`<reply>\n${output.slice(0, 40_000)}\n</reply>`,
|
|
861
|
+
prompt: judgePrompt(rubric, reply, work),
|
|
825
862
|
options: {
|
|
826
863
|
model: "haiku",
|
|
827
864
|
systemPrompt: "You grade text against a stated requirement. Be strict and literal.",
|
|
828
865
|
tools: [],
|
|
829
866
|
settingSources: [],
|
|
830
|
-
env,
|
|
867
|
+
env: sdkEnv(env),
|
|
868
|
+
strictMcpConfig: true,
|
|
831
869
|
maxTurns: 1,
|
|
832
870
|
abortController,
|
|
833
871
|
},
|
|
@@ -898,3 +936,28 @@ function runVerify(agentDir, command, env, data, signal) {
|
|
|
898
936
|
child.on("close", (code) => resolve({ code, out }));
|
|
899
937
|
});
|
|
900
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* The environment an Agent SDK session starts with: the step's, minus the
|
|
941
|
+
* connectors of whoever is signed in to claude.ai on this machine. Claude
|
|
942
|
+
* Code loads an account's claude.ai MCP servers (Docs, Drive, …) unless told
|
|
943
|
+
* not to, and `settingSources: []` does not reach them — they come with the
|
|
944
|
+
* login, not from a settings file. A local `foldrun run` handed every agent
|
|
945
|
+
* the person's own connectors: the hello template's agents reported that
|
|
946
|
+
* "the only tools I have are the Claude Docs ones". `strictMcpConfig` keeps
|
|
947
|
+
* the rest of the on-disk MCP configuration out; this keeps the account's.
|
|
948
|
+
*/
|
|
949
|
+
export function sdkEnv(base, extra = {}) {
|
|
950
|
+
return { ...(base ?? process.env), ...extra, ENABLE_CLAUDEAI_MCP_SERVERS: "false" };
|
|
951
|
+
}
|
|
952
|
+
/** The text of a tool result, whichever shape it came in: a string, or a
|
|
953
|
+
* list of content blocks of which only the text ones say anything. */
|
|
954
|
+
export function toolResultText(content) {
|
|
955
|
+
if (typeof content === "string")
|
|
956
|
+
return content;
|
|
957
|
+
if (!Array.isArray(content))
|
|
958
|
+
return "";
|
|
959
|
+
return content
|
|
960
|
+
.filter((b) => !!b && b.type === "text" && typeof b.text === "string")
|
|
961
|
+
.map((b) => b.text)
|
|
962
|
+
.join(" ");
|
|
963
|
+
}
|
package/dist/src/storage.d.ts
CHANGED
|
@@ -128,6 +128,10 @@ export declare function uploadUrl(tenant: string, workspace: string, rel: string
|
|
|
128
128
|
* is worth writing when a workspace actually outgrows its quota, not before.
|
|
129
129
|
*/
|
|
130
130
|
export declare function materializeFiles(tenant: string, workspace: string): Promise<string[]>;
|
|
131
|
+
/** path → sha of every stored file: what `storage/` holds once
|
|
132
|
+
* materializeFiles has run. A run keeps it to tell its own writes from the
|
|
133
|
+
* files it was handed. */
|
|
134
|
+
export declare function storageBaseline(tenant: string, workspace: string): Record<string, string>;
|
|
131
135
|
/**
|
|
132
136
|
* Take what the run left in `storage/` back into the store, stamped with the
|
|
133
137
|
* run that wrote it.
|
|
@@ -136,7 +140,9 @@ export declare function materializeFiles(tenant: string, workspace: string): Pro
|
|
|
136
140
|
* derived from the index, so anything on disk that the index does not already
|
|
137
141
|
* describe with the same size and hash is, by construction, new work.
|
|
138
142
|
*/
|
|
139
|
-
export declare function harvestFiles(tenant: string, workspace: string, by: string
|
|
143
|
+
export declare function harvestFiles(tenant: string, workspace: string, by: string, opts?: {
|
|
144
|
+
changedFrom?: Record<string, string>;
|
|
145
|
+
}): Promise<{
|
|
140
146
|
saved: string[];
|
|
141
147
|
errors: string[];
|
|
142
148
|
}>;
|
package/dist/src/storage.js
CHANGED
|
@@ -686,6 +686,14 @@ export async function materializeFiles(tenant, workspace) {
|
|
|
686
686
|
fs.mkdirSync(root, { recursive: true });
|
|
687
687
|
return brought;
|
|
688
688
|
}
|
|
689
|
+
/** path → sha of every stored file: what `storage/` holds once
|
|
690
|
+
* materializeFiles has run. A run keeps it to tell its own writes from the
|
|
691
|
+
* files it was handed. */
|
|
692
|
+
export function storageBaseline(tenant, workspace) {
|
|
693
|
+
if (!fileStoreEnabled())
|
|
694
|
+
return {};
|
|
695
|
+
return Object.fromEntries(readIndex(tenant, workspace).files.map((f) => [f.path, f.sha]));
|
|
696
|
+
}
|
|
689
697
|
/**
|
|
690
698
|
* Take what the run left in `storage/` back into the store, stamped with the
|
|
691
699
|
* run that wrote it.
|
|
@@ -694,7 +702,7 @@ export async function materializeFiles(tenant, workspace) {
|
|
|
694
702
|
* derived from the index, so anything on disk that the index does not already
|
|
695
703
|
* describe with the same size and hash is, by construction, new work.
|
|
696
704
|
*/
|
|
697
|
-
export async function harvestFiles(tenant, workspace, by) {
|
|
705
|
+
export async function harvestFiles(tenant, workspace, by, opts = {}) {
|
|
698
706
|
if (!fileStoreEnabled())
|
|
699
707
|
return { saved: [], errors: [] };
|
|
700
708
|
adoptLegacyFilesDir(workspaceDir(tenant, workspace));
|
|
@@ -729,6 +737,11 @@ export async function harvestFiles(tenant, workspace, by) {
|
|
|
729
737
|
if (sha256(fs.readFileSync(abs)) === previous.sha)
|
|
730
738
|
continue;
|
|
731
739
|
}
|
|
740
|
+
// Only what the run itself changed: a file still holding the bytes it
|
|
741
|
+
// was handed is not this run's work, and the index may have moved on
|
|
742
|
+
// since (an upload while the run was going) — that must win.
|
|
743
|
+
if (opts.changedFrom && opts.changedFrom[norm] === sha256(fs.readFileSync(abs)))
|
|
744
|
+
continue;
|
|
732
745
|
await putFileQuietly(tenant, workspace, norm, fs.readFileSync(abs), by);
|
|
733
746
|
saved.push(norm);
|
|
734
747
|
}
|
package/dist/src/store.d.ts
CHANGED
|
@@ -188,6 +188,15 @@ export declare function fencedCodeBlock(body: string): {
|
|
|
188
188
|
* failure costs something that cannot be taken back. See `lintFlow`.
|
|
189
189
|
*/
|
|
190
190
|
export declare function toolIsOutward(data: Record<string, unknown>): boolean;
|
|
191
|
+
/** How much of a tool.md body rides to the model with the tool. */
|
|
192
|
+
export declare const TOOL_GUIDE_MAX = 6000;
|
|
193
|
+
/**
|
|
194
|
+
* The prose under a tool.md's frontmatter: how to call it, what it refuses,
|
|
195
|
+
* what its output means. Only `description:` used to reach the model, so
|
|
196
|
+
* agents invented what the body documented (5 Oct 2026). The fenced program
|
|
197
|
+
* of a single-file tool is code, not guidance, and stays out.
|
|
198
|
+
*/
|
|
199
|
+
export declare function toolGuide(body: string | undefined, inlineProgram?: boolean): string | undefined;
|
|
191
200
|
export declare function parseToolDef(data: Record<string, unknown>, fallbackName: string, body?: string): ToolDef | null;
|
|
192
201
|
/**
|
|
193
202
|
* Tools in a directory, in both shapes.
|
|
@@ -234,6 +243,8 @@ export interface ApiSpec {
|
|
|
234
243
|
name: string;
|
|
235
244
|
base: string;
|
|
236
245
|
description: string;
|
|
246
|
+
/** The tool.md body, for the model (see toolGuide). */
|
|
247
|
+
guide?: string;
|
|
237
248
|
headers: Record<string, string>;
|
|
238
249
|
query: Record<string, string>;
|
|
239
250
|
methods: string[];
|
|
@@ -846,6 +857,9 @@ export declare function setWorkspaceDescription(tenant: string, workspace: strin
|
|
|
846
857
|
* this copy still said `type: Agent` after the CLI's had moved to `kind:`,
|
|
847
858
|
* so every workspace made from the dashboard was born in the old format.
|
|
848
859
|
*/
|
|
860
|
+
/** What the dashboard's "Create workspace" makes: blank (blankWorkspaceFiles),
|
|
861
|
+
* minus the laptop-only files, for the same reason as templateFiles. */
|
|
862
|
+
export declare function blankTemplateFiles(workspace: string): DeployFile[];
|
|
849
863
|
export declare function templateFiles(workspace: string): DeployFile[];
|
|
850
864
|
export declare const AGENT_TEMPLATE: (name: string) => string;
|
|
851
865
|
export declare const FLOW_TEMPLATE: (name: string, firstAgent: string) => string;
|
|
@@ -992,6 +1006,10 @@ export interface StepRecord {
|
|
|
992
1006
|
why?: string;
|
|
993
1007
|
pod?: boolean;
|
|
994
1008
|
} | null;
|
|
1009
|
+
/** Which model credential the step ran on, by its variable — "API key
|
|
1010
|
+
* (ANTHROPIC_API_KEY)", "Claude login token (CLAUDE_CODE_OAUTH_TOKEN)",
|
|
1011
|
+
* "provider: …" — or "none set" (model-credential.ts). */
|
|
1012
|
+
credential?: string;
|
|
995
1013
|
/** Slim browsing through the account's pod (browser-pod.ts): reconnect
|
|
996
1014
|
* tries and how many got through; when the pod was lost, why and what
|
|
997
1015
|
* the step had written by then; and what happened — `fallback` (the
|
|
@@ -1337,6 +1355,12 @@ export declare function runCost(run: RunRecord): number;
|
|
|
1337
1355
|
* fan-out template is a row in the trace rather than work — billing any of
|
|
1338
1356
|
* them would charge for steps the customer can see never executed, which is
|
|
1339
1357
|
* the fastest way to lose an argument about an invoice.
|
|
1358
|
+
*
|
|
1359
|
+
* Tokens count only for steps that ran on the platform's own model
|
|
1360
|
+
* credential. A step on the account's own key (`credential` "provider: …" —
|
|
1361
|
+
* a provider: block, or Settings → Model) was paid for at the provider, and
|
|
1362
|
+
* billing it again here charged a BYOK customer twice. And a carried step's
|
|
1363
|
+
* tokens were billed where it ran, like its compute.
|
|
1340
1364
|
*/
|
|
1341
1365
|
export declare function runMeter(run: RunRecord): {
|
|
1342
1366
|
tokenCostUsd: number;
|
package/dist/src/store.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// or an agent, and belongs in git.
|
|
20
20
|
//
|
|
21
21
|
// An account is a named directory (per OKF: tenancy is a hosting concern).
|
|
22
|
+
import { expandPlatformVars } from "./host-env.js";
|
|
22
23
|
import fs from "node:fs";
|
|
23
24
|
import path from "node:path";
|
|
24
25
|
import { recordRevision, registerTreeReader } from "./history.js";
|
|
@@ -50,7 +51,7 @@ import { localeProblems } from "./locale.js";
|
|
|
50
51
|
import { readBundle, syncIndex, appendLog, provenanceMarks, syncWorkspaceBundles, } from "./okf.js";
|
|
51
52
|
import { readTransport, KINDS } from "./kinds.js";
|
|
52
53
|
import { providerPreset, looksOpenAiShaped, PROTECTED_PARAMS } from "./providers.js";
|
|
53
|
-
import { starterFiles, accountFiles } from "./starter.js";
|
|
54
|
+
import { starterFiles, accountFiles, blankWorkspaceFiles } from "./starter.js";
|
|
54
55
|
import { trimSlashes, MAX_EDITABLE_FILE } from "./paths.js";
|
|
55
56
|
// Where workspaces live. The hosted app keeps many under data/; the CLI runs
|
|
56
57
|
// against one folder, which is what `foldrun run ./my-desk` has to mean.
|
|
@@ -288,6 +289,18 @@ export function assertCanonicalCase(rel) {
|
|
|
288
289
|
// trigger-log.jsonl: the scheduler's record of why a flow did or did not
|
|
289
290
|
// fire (trigger-log.ts). Before it was listed here every deploy erased it.
|
|
290
291
|
export const PLATFORM_FILES = ["secrets.json", "hooks.json", "hook-deliveries.jsonl", ".repo.json", "trigger-log.jsonl"];
|
|
292
|
+
// The agent engine's own home files. Each step's sandbox runs the Claude
|
|
293
|
+
// Agent SDK — the engine, whatever model answers — with its home in the
|
|
294
|
+
// agent's folder, and the engine writes its settings (.claude.json, its
|
|
295
|
+
// backups), plugin caches and the full transcript of every session
|
|
296
|
+
// (.claude/projects/*.jsonl) there. Nothing in foldrun reads them. Copied
|
|
297
|
+
// back after every step, they had piled up to 5,274 files and 19 MB in one
|
|
298
|
+
// agent's folder on the box by 6 Oct 2026, carried every past conversation
|
|
299
|
+
// into every sandbox, and their clashing rewrites were the "write-back
|
|
300
|
+
// conflict on .claude.json" lines (and the big merges) of that day. A
|
|
301
|
+
// workspace's own .claude/agents/*.md — Claude Code subagents an author
|
|
302
|
+
// writes, imported as agents — sits at the workspace root and is untouched.
|
|
303
|
+
const ENGINE_HOME_RE = /^agents\/[^/]+\/\.claude([./]|$)/;
|
|
291
304
|
/** True for anything the platform, not the author or the agent, writes. */
|
|
292
305
|
export function isPlatformPath(rel) {
|
|
293
306
|
const norm = rel.replaceAll("\\", "/");
|
|
@@ -299,6 +312,8 @@ export function isPlatformPath(rel) {
|
|
|
299
312
|
return true;
|
|
300
313
|
if (norm === "trigger-log.jsonl.tmp")
|
|
301
314
|
return true;
|
|
315
|
+
if (ENGINE_HOME_RE.test(norm))
|
|
316
|
+
return true;
|
|
302
317
|
return norm === "runs" || norm.startsWith("runs/") || norm === ".foldrun" || norm.startsWith(".foldrun/");
|
|
303
318
|
}
|
|
304
319
|
/** What a deploy keeps whatever it ships — the rule above saveWorkspace's
|
|
@@ -483,7 +498,7 @@ export function buildMemoryIndex(dir, prefix = "") {
|
|
|
483
498
|
// Workspace-scoped shared assets: skills/, memory/, tools/ sit beside agents/
|
|
484
499
|
// and are available to every agent in that workspace.
|
|
485
500
|
export function workspaceMemoryIndex(tenant, workspace) {
|
|
486
|
-
return buildMemoryIndex(path.join(workspaceDir(tenant, workspace), "memory"), "
|
|
501
|
+
return buildMemoryIndex(path.join(workspaceDir(tenant, workspace), "memory"), "workspace/memory/");
|
|
487
502
|
}
|
|
488
503
|
// Knowledge is what you *gave* an agent — stable reference material an agent
|
|
489
504
|
// reads and never rewrites. Memory is what it *learned* and authors itself.
|
|
@@ -493,7 +508,7 @@ export function knowledgeIndex(dir, prefix = "") {
|
|
|
493
508
|
return buildMemoryIndex(dir, prefix);
|
|
494
509
|
}
|
|
495
510
|
export function workspaceKnowledgeIndex(tenant, workspace) {
|
|
496
|
-
return knowledgeIndex(path.join(workspaceDir(tenant, workspace), "knowledge"), "
|
|
511
|
+
return knowledgeIndex(path.join(workspaceDir(tenant, workspace), "knowledge"), "workspace/knowledge/");
|
|
497
512
|
}
|
|
498
513
|
// One noun for capability. A tools/<name>.md file declares either an HTTP
|
|
499
514
|
// endpoint or a script — `type:` says which, and it's inferred when omitted
|
|
@@ -547,6 +562,28 @@ export function fencedCodeBlock(body) {
|
|
|
547
562
|
export function toolIsOutward(data) {
|
|
548
563
|
return data.outward === true;
|
|
549
564
|
}
|
|
565
|
+
/** How much of a tool.md body rides to the model with the tool. */
|
|
566
|
+
export const TOOL_GUIDE_MAX = 6000;
|
|
567
|
+
/**
|
|
568
|
+
* The prose under a tool.md's frontmatter: how to call it, what it refuses,
|
|
569
|
+
* what its output means. Only `description:` used to reach the model, so
|
|
570
|
+
* agents invented what the body documented (5 Oct 2026). The fenced program
|
|
571
|
+
* of a single-file tool is code, not guidance, and stays out.
|
|
572
|
+
*/
|
|
573
|
+
export function toolGuide(body, inlineProgram = false) {
|
|
574
|
+
if (!body)
|
|
575
|
+
return undefined;
|
|
576
|
+
let text = body.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
577
|
+
// Only a single-file tool's program is cut; in a `run:` tool every fenced
|
|
578
|
+
// block is an example of a call, which is exactly what the model needs.
|
|
579
|
+
const inline = inlineProgram ? fencedCodeBlock(text) : null;
|
|
580
|
+
if (inline)
|
|
581
|
+
text = text.slice(0, inline.start) + text.slice(inline.end);
|
|
582
|
+
text = text.trim();
|
|
583
|
+
if (!text)
|
|
584
|
+
return undefined;
|
|
585
|
+
return text.length > TOOL_GUIDE_MAX ? `${text.slice(0, TOOL_GUIDE_MAX)}\n\n[the rest of tool.md is cut here]` : text;
|
|
586
|
+
}
|
|
550
587
|
export function parseToolDef(data, fallbackName, body) {
|
|
551
588
|
const name = typeof data.name === "string" ? data.name : fallbackName;
|
|
552
589
|
// `transport:`, or a legacy `type: http|script|mcp` from before `type:` meant
|
|
@@ -580,18 +617,28 @@ export function parseToolDef(data, fallbackName, body) {
|
|
|
580
617
|
};
|
|
581
618
|
}
|
|
582
619
|
if (kind === "script") {
|
|
620
|
+
// `guide` is the body's, never the frontmatter's: an author's `guide:`
|
|
621
|
+
// would otherwise ride to the model in place of the manual.
|
|
622
|
+
if ("guide" in data) {
|
|
623
|
+
data = { ...data };
|
|
624
|
+
delete data.guide;
|
|
625
|
+
}
|
|
583
626
|
// The program is a run: path, or the file's own fenced code block — the
|
|
584
627
|
// single-file form, which is what lets a script tool read and edit like
|
|
585
628
|
// every other markdown document.
|
|
586
|
-
if (typeof data.run === "string")
|
|
587
|
-
|
|
629
|
+
if (typeof data.run === "string") {
|
|
630
|
+
const guide = toolGuide(body);
|
|
631
|
+
return { kind: "script", name, spec: { ...data, name, ...(guide ? { guide } : {}) } };
|
|
632
|
+
}
|
|
588
633
|
const inline = body ? fencedCode(body) : null;
|
|
589
634
|
if (!inline)
|
|
590
635
|
return null;
|
|
591
|
-
|
|
636
|
+
const guide = toolGuide(body, true);
|
|
637
|
+
return { kind: "script", name, spec: { ...data, name, code: inline.code, codeExt: inline.ext, ...(guide ? { guide } : {}) } };
|
|
592
638
|
}
|
|
593
639
|
const [spec] = parseApis([{ ...data, name }]);
|
|
594
|
-
|
|
640
|
+
const guide = toolGuide(body);
|
|
641
|
+
return spec ? { kind: "http", name, spec: guide ? { ...spec, guide } : spec } : null;
|
|
595
642
|
}
|
|
596
643
|
/**
|
|
597
644
|
* Tools in a directory, in both shapes.
|
|
@@ -955,7 +1002,9 @@ export function parseApis(raw) {
|
|
|
955
1002
|
out.push({
|
|
956
1003
|
name: name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
957
1004
|
...(Number(e.timeout) > 0 ? { timeout: Number(e.timeout) } : {}),
|
|
958
|
-
|
|
1005
|
+
// `${FOLDRUN_API_URL}` and `${FOLDRUN_PUBLIC_URL}` are the install's own
|
|
1006
|
+
// addresses, filled here on the host so a desk never names one.
|
|
1007
|
+
base: trimSlashes(expandPlatformVars(base)),
|
|
959
1008
|
description: typeof e.description === "string" ? e.description : "",
|
|
960
1009
|
headers: asRecordLocal(e.headers),
|
|
961
1010
|
query: asRecordLocal(e.query),
|
|
@@ -2430,6 +2479,11 @@ export function setWorkspaceDescription(tenant, workspace, description) {
|
|
|
2430
2479
|
* this copy still said `type: Agent` after the CLI's had moved to `kind:`,
|
|
2431
2480
|
* so every workspace made from the dashboard was born in the old format.
|
|
2432
2481
|
*/
|
|
2482
|
+
/** What the dashboard's "Create workspace" makes: blank (blankWorkspaceFiles),
|
|
2483
|
+
* minus the laptop-only files, for the same reason as templateFiles. */
|
|
2484
|
+
export function blankTemplateFiles(workspace) {
|
|
2485
|
+
return blankWorkspaceFiles(workspace).filter((f) => f.path === "AGENTS.md" || IN_WORKSPACE_DIR.test(f.path));
|
|
2486
|
+
}
|
|
2433
2487
|
export function templateFiles(workspace) {
|
|
2434
2488
|
// The starter minus what only matters on a laptop's disk: .gitignore
|
|
2435
2489
|
// guards a local clone's secrets, but the hosted store never keeps
|
|
@@ -2579,7 +2633,25 @@ export function runOutputPath(tenant, workspace, runId, agent, rel) {
|
|
|
2579
2633
|
}
|
|
2580
2634
|
export function readRun(tenant, workspace, runId) {
|
|
2581
2635
|
const p = runFilePath(tenant, workspace, runId);
|
|
2582
|
-
|
|
2636
|
+
// EFS/NFS: writeRun() replaces this file by rename, so an open()+read()
|
|
2637
|
+
// here can land on the just-unlinked inode and come back ESTALE (errno
|
|
2638
|
+
// -116, surfaced by Node as "Unknown system error -116", syscall read).
|
|
2639
|
+
// That isn't corruption — the next open() by path sees the committed new
|
|
2640
|
+
// inode — so re-read a few times on ESTALE and rethrow anything else.
|
|
2641
|
+
// (On dev-aws this surfaced as a CLI `--wait` call returning HTTP 500
|
|
2642
|
+
// from waitForRun's poll while the run itself completed fine.)
|
|
2643
|
+
const attempts = 5;
|
|
2644
|
+
for (let i = 1;; i++) {
|
|
2645
|
+
try {
|
|
2646
|
+
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : null;
|
|
2647
|
+
}
|
|
2648
|
+
catch (e) {
|
|
2649
|
+
const err = e;
|
|
2650
|
+
if ((err.code === "ESTALE" || err.errno === -116) && i < attempts)
|
|
2651
|
+
continue;
|
|
2652
|
+
throw e;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2583
2655
|
}
|
|
2584
2656
|
export function writeRun(tenant, workspace, run) {
|
|
2585
2657
|
const p = runFilePath(tenant, workspace, run.id);
|
|
@@ -2825,12 +2897,19 @@ export function runCost(run) {
|
|
|
2825
2897
|
* fan-out template is a row in the trace rather than work — billing any of
|
|
2826
2898
|
* them would charge for steps the customer can see never executed, which is
|
|
2827
2899
|
* the fastest way to lose an argument about an invoice.
|
|
2900
|
+
*
|
|
2901
|
+
* Tokens count only for steps that ran on the platform's own model
|
|
2902
|
+
* credential. A step on the account's own key (`credential` "provider: …" —
|
|
2903
|
+
* a provider: block, or Settings → Model) was paid for at the provider, and
|
|
2904
|
+
* billing it again here charged a BYOK customer twice. And a carried step's
|
|
2905
|
+
* tokens were billed where it ran, like its compute.
|
|
2828
2906
|
*/
|
|
2829
2907
|
export function runMeter(run) {
|
|
2830
2908
|
let steps = 0;
|
|
2831
2909
|
let computeSecs = 0;
|
|
2832
2910
|
let smallSecs = 0;
|
|
2833
2911
|
let netBytes = 0;
|
|
2912
|
+
let tokenCostUsd = 0;
|
|
2834
2913
|
for (const s of run.steps) {
|
|
2835
2914
|
if (s.status !== "completed" && s.status !== "failed")
|
|
2836
2915
|
continue;
|
|
@@ -2838,13 +2917,15 @@ export function runMeter(run) {
|
|
|
2838
2917
|
if (s.carriedFrom)
|
|
2839
2918
|
continue;
|
|
2840
2919
|
steps += 1;
|
|
2920
|
+
if (!(s.credential ?? "").startsWith("provider:"))
|
|
2921
|
+
tokenCostUsd += s.costUsd ?? 0;
|
|
2841
2922
|
const secs = s.computeSecs ?? 0;
|
|
2842
2923
|
computeSecs += secs;
|
|
2843
2924
|
if (s.size === "small")
|
|
2844
2925
|
smallSecs += secs;
|
|
2845
2926
|
netBytes += (s.actual?.rxBytes ?? 0) + (s.actual?.txBytes ?? 0);
|
|
2846
2927
|
}
|
|
2847
|
-
return { tokenCostUsd
|
|
2928
|
+
return { tokenCostUsd, steps, computeSecs, smallSecs, netBytes };
|
|
2848
2929
|
}
|
|
2849
2930
|
/**
|
|
2850
2931
|
* The compute meter for hold+work pricing: core-seconds and GiB-seconds
|
package/dist/src/subagents.js
CHANGED
|
@@ -71,7 +71,7 @@ export function subagentPrompt(o) {
|
|
|
71
71
|
`# Where you are\n\n` +
|
|
72
72
|
`You are ${o.name}, a sub-agent started by ${o.parent} to do one job and report back. ` +
|
|
73
73
|
`You work in ${o.parent}'s directory, \`agents/${o.parent}/\` in the \`${o.workspace}\` workspace: ` +
|
|
74
|
-
|
|
74
|
+
`\`workspace/\` is the workspace root, its knowledge is at \`workspace/knowledge/\`, and absolute paths are refused. ` +
|
|
75
75
|
`You have only the tools listed for you. When the job is done, reply with what you found or made — ` +
|
|
76
76
|
`your reply is all ${o.parent} sees of your work, so put the facts and file paths in it.`,
|
|
77
77
|
...(o.shared?.trim() ? [o.shared.trim()] : []),
|
package/dist/src/test-mode.d.ts
CHANGED
|
@@ -103,8 +103,23 @@ export declare function divertedSummary(rel: string, was: Buffer | null, next: B
|
|
|
103
103
|
export declare function snapshotDivertedDirs(wsRoot: string): Map<string, Buffer>;
|
|
104
104
|
/** Undo a step's writes to the diverted directories, keeping each changed
|
|
105
105
|
* or new file under `to` by its workspace-relative path. Deletions are
|
|
106
|
-
* undone too — the original is put back.
|
|
107
|
-
|
|
106
|
+
* undone too — the original is put back. `handed` is what the step was
|
|
107
|
+
* given when that was more than the workspace (the run's earlier test
|
|
108
|
+
* writes, laid over it): only what the step changed from THAT is its
|
|
109
|
+
* write. Returns what was set aside. */
|
|
110
|
+
export declare function restoreDivertedDirs(wsRoot: string, before: Map<string, Buffer>, to: string, note: (rel: string, summary: string) => void, handed?: Map<string, Buffer>): string[];
|
|
111
|
+
/**
|
|
112
|
+
* A test run's later steps see what its earlier steps wrote. Those writes
|
|
113
|
+
* are kept under runs/<id>/test-writes/, never on the workspace, so each
|
|
114
|
+
* step's copy of the workspace gets them laid over it — otherwise a step
|
|
115
|
+
* reads the real files, and a flow that hands work on through storage/
|
|
116
|
+
* tests something no live run does: on 2026-10-03 a strata-desk test run's
|
|
117
|
+
* compiler wrote today's PASS report, and the mailer was handed the
|
|
118
|
+
* morning's FAIL one and refused. Times are kept, so a verify that asks
|
|
119
|
+
* whether a file is fresh answers for the step that wrote it. Returns the
|
|
120
|
+
* workspace-relative paths laid over.
|
|
121
|
+
*/
|
|
122
|
+
export declare function overlayTestWrites(wsCopy: string, from: string): string[];
|
|
108
123
|
/** `[test] ` in front of a headline, once. */
|
|
109
124
|
export declare function testHeadline(summary: string): string;
|
|
110
125
|
/** The env every test-run sandbox gets, beside its secrets. */
|