@archmax-ai/harness 0.3.2 → 0.4.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 (112) hide show
  1. package/README.md +3 -1
  2. package/dist/assembly/compose.d.ts +40 -3
  3. package/dist/assembly/compose.js +81 -7
  4. package/dist/assembly/compose.js.map +1 -1
  5. package/dist/assembly/delegation-registry.js +5 -2
  6. package/dist/assembly/delegation-registry.js.map +1 -1
  7. package/dist/assembly/image-reads.d.ts +53 -0
  8. package/dist/assembly/image-reads.js +163 -0
  9. package/dist/assembly/image-reads.js.map +1 -0
  10. package/dist/assembly/index.d.ts +21 -0
  11. package/dist/assembly/index.js +7 -1
  12. package/dist/assembly/index.js.map +1 -1
  13. package/dist/assembly/plain.js +6 -3
  14. package/dist/assembly/plain.js.map +1 -1
  15. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +48 -1
  16. package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +27 -1
  17. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +67 -22
  18. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +24 -5
  19. package/dist/core/binary-read.d.ts +52 -0
  20. package/dist/core/binary-read.js +129 -0
  21. package/dist/core/binary-read.js.map +1 -0
  22. package/dist/core/file-operations.d.ts +43 -0
  23. package/dist/core/file-operations.js +368 -0
  24. package/dist/core/file-operations.js.map +1 -0
  25. package/dist/core/path-mapping.d.ts +10 -3
  26. package/dist/core/path-mapping.js +27 -0
  27. package/dist/core/path-mapping.js.map +1 -1
  28. package/dist/core/platform-prompt.generated.js +1 -1
  29. package/dist/core/platform-prompt.generated.js.map +1 -1
  30. package/dist/core/prompt.d.ts +27 -0
  31. package/dist/core/prompt.js +65 -5
  32. package/dist/core/prompt.js.map +1 -1
  33. package/dist/core/session-zone.d.ts +3 -0
  34. package/dist/core/session-zone.js +9 -0
  35. package/dist/core/session-zone.js.map +1 -1
  36. package/dist/core/tool-context.d.ts +23 -0
  37. package/dist/core/tool-context.js +14 -0
  38. package/dist/core/tool-context.js.map +1 -0
  39. package/dist/core/tool-telemetry.js +2 -1
  40. package/dist/core/tool-telemetry.js.map +1 -1
  41. package/dist/core/workspace-router.d.ts +11 -1
  42. package/dist/core/workspace-router.js +47 -3
  43. package/dist/core/workspace-router.js.map +1 -1
  44. package/dist/index.d.ts +11 -1
  45. package/dist/index.js +3 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/kernel/kernel.js +167 -119
  48. package/dist/kernel/kernel.js.map +1 -1
  49. package/dist/machine/allow.d.ts +22 -0
  50. package/dist/machine/allow.js +47 -15
  51. package/dist/machine/allow.js.map +1 -1
  52. package/dist/machine/lint-spec.js +12 -3
  53. package/dist/machine/lint-spec.js.map +1 -1
  54. package/dist/machine/machine.d.ts +10 -1
  55. package/dist/machine/machine.js +36 -8
  56. package/dist/machine/machine.js.map +1 -1
  57. package/dist/machine/spec-schema.js +14 -0
  58. package/dist/machine/spec-schema.js.map +1 -1
  59. package/dist/machine/tool-names.d.ts +27 -7
  60. package/dist/machine/tool-names.js +43 -13
  61. package/dist/machine/tool-names.js.map +1 -1
  62. package/dist/machine/tool-paths.d.ts +51 -0
  63. package/dist/machine/tool-paths.js +127 -0
  64. package/dist/machine/tool-paths.js.map +1 -0
  65. package/dist/public/spec.d.ts +6 -1
  66. package/dist/public/spec.js +5 -1
  67. package/dist/public/spec.js.map +1 -1
  68. package/dist/validate/validate.js +10 -5
  69. package/dist/validate/validate.js.map +1 -1
  70. package/dist/workflow/agent-tools.d.ts +20 -1
  71. package/dist/workflow/agent-tools.js +31 -1
  72. package/dist/workflow/agent-tools.js.map +1 -1
  73. package/dist/workflow/control-tools.d.ts +4 -1
  74. package/dist/workflow/control-tools.js +16 -9
  75. package/dist/workflow/control-tools.js.map +1 -1
  76. package/dist/workflow/governance.d.ts +2 -0
  77. package/dist/workflow/governance.js +1 -0
  78. package/dist/workflow/governance.js.map +1 -1
  79. package/dist/workflow/middleware.d.ts +2 -0
  80. package/dist/workflow/middleware.js +27 -12
  81. package/dist/workflow/middleware.js.map +1 -1
  82. package/dist/workflow/on-error.js +3 -2
  83. package/dist/workflow/on-error.js.map +1 -1
  84. package/dist/workflow/parks.js +5 -5
  85. package/dist/workflow/parks.js.map +1 -1
  86. package/dist/workflow/render-prompt.d.ts +2 -0
  87. package/dist/workflow/render-prompt.js +9 -3
  88. package/dist/workflow/render-prompt.js.map +1 -1
  89. package/dist/workflow/returns-check.d.ts +35 -0
  90. package/dist/workflow/returns-check.js +10 -0
  91. package/dist/workflow/returns-check.js.map +1 -0
  92. package/dist/workflow/signature-checks.d.ts +26 -1
  93. package/dist/workflow/signature-checks.js +35 -10
  94. package/dist/workflow/signature-checks.js.map +1 -1
  95. package/dist/workflow/state.d.ts +75 -1
  96. package/dist/workflow/state.js +83 -0
  97. package/dist/workflow/state.js.map +1 -1
  98. package/dist/workflow/sub-workflow.d.ts +16 -5
  99. package/dist/workflow/sub-workflow.js +84 -37
  100. package/dist/workflow/sub-workflow.js.map +1 -1
  101. package/dist/workflow/tool-service.js +57 -10
  102. package/dist/workflow/tool-service.js.map +1 -1
  103. package/dist/workflow/turn-boundary.d.ts +8 -7
  104. package/dist/workflow/turn-boundary.js +10 -9
  105. package/dist/workflow/turn-boundary.js.map +1 -1
  106. package/dist/workflow/workflow-tools.js +5 -1
  107. package/dist/workflow/workflow-tools.js.map +1 -1
  108. package/package.json +1 -1
  109. package/skills/archmax-harness/references/backend-integration.md +48 -1
  110. package/skills/archmax-harness/references/hook-and-test-scripts.md +27 -1
  111. package/skills/archmax-harness/references/workflow-schema.md +67 -22
  112. package/skills/archmax-harness/references/workflow-yaml.md +24 -5
@@ -81,6 +81,7 @@ From `@archmax-ai/harness` (`src/index.ts`):
81
81
  | `SessionStoreRequiredError`, `SessionStoreCapabilityError` | class | Assembly / capability errors for run storage |
82
82
  | `workflowStateSchema`, `workflowPaths`, `sessionPaths`, `runArtifactPaths` | schema/fn | State schema + path helpers |
83
83
  | `runTests`, `RunTestsOptions`, `CaseResult` | fn/type | Run a workflow's cases — [against your own agent](#cases-against-your-agent) |
84
+ | `toolsFromMap`, `AgentToolDescriptor`, `ToolContext`, `ToolPaths`, `PathAccess`, `ToolPathsError` | fn/type/class | Host tools: a `name → descriptor` map, each descriptor's declared `paths` and a handler handed the turn's workspace — [host tools](#host-tools) |
84
85
  | `SIGNATURE_TYPES`, `normalizeSignature`, `signatureForTrigger`, `signatureJsonSchema`, `signatureValueIssues` | const/fn | A trigger's typed signature as JSON Schema and its conformance rule — [exposing a workflow](#exposing-a-workflow-to-outside-callers); also on `@archmax-ai/harness/spec` |
85
86
 
86
87
  ## createAgent options
@@ -98,6 +99,10 @@ All optional (`CreateAgentParams`, `src/assembly/index.ts`):
98
99
  | `mounts` | `Record<string, MountSpec>` | Authored mounts as a `CompositeBackend` route table: key → backend, or `{ backend, readOnly, governed, searchable }`. Trailing slash = directory mount; no slash = exact-path file mount (`"/AGENTS.md"`). Read-only unless declared otherwise. `governed: true` hands the spec's `mounts` block the decision of **which states** may reach it (closed by default, like a skill bundle) and whether each may write there; a mount not so declared is visible in every state exactly as today, so a table that marks nothing behaves unchanged. `readOnly` is a **ceiling** the spec cannot lift: a grant may narrow a writable mount to reads in a state, never open a read-only one. A key may be several segments deep (`"/catalogs/eu/"`); its **first** segment is what is reserved, so `/workflows/x/` fails as `/workflows/` does. Omitted on the default backend: `defaultMounts(rootDir)` (the conventional table — `/skills/`, `/.platform/`, `/AGENTS.md`, none governed — extend with `{ ...defaultMounts(root), "/templates/": … }`). Omitted with a custom `backend`: nothing authored is served; expose subtrees with `mountSubtree(backend, "skills")`. A key colliding with a run area — or naming the authoring-plane prefix (`/workflows/`) — throws `MountCollisionError`. `searchable: false` declares a mount the runtime never searches on its own initiative: a root-wide `grep`/`glob` fans out over the other mounts only, while a search the agent addresses at the mount (or a path inside it) still reaches its backend and returns the backend's own answer — matches or `{ error }` — verbatim. Use it for a backend that must refuse searches (a remote folder served live). Listing and reading are unchanged, the prompt marks it "browse only", names arrive as `MountPrefixes.unsearchable`, and it is ignored on a file mount. |
99
100
  | `sessionStore` | `SessionStore` | Physical storage for run state (checkpoints, artifacts, the per-session `scratchpad/`). Zero-config default: `createFilesystemSessionStore({ dir: "<rootDir>/sessions" })`. Required with a custom `backend` (else `SessionStoreRequiredError`). Build with `createBackendSessionStore({ backend, prefix? })` for S3-style storage or `createMemorySessionStore()` for ephemeral runs. |
100
101
  | `systemPrompt` | `string` | Appended after workspace `AGENTS.md`. |
102
+ | `tools` | `StructuredTool[]` | Host tools, granted per state by `tools.allow`. Build them with `toolsFromMap` — see [host tools](#host-tools). A name starting `archmax_`, or naming a runtime file operation (`copy_file`, `move_file`, `remove_file`), throws `ReservedToolNameError`. |
103
+ | `essentialTools` | `string[]` | Host tools treated as always on: disclosed and permitted in every state; a state's own entry still narrows one. |
104
+ | `images` | `boolean \| ImageReadOptions` | Show a model with vision the images it reads: the model's `read_file` of a PNG/JPEG/GIF/WebP answers with one line and the image is added to each later request as a `user` message after that batch of tool results, read per request — no bytes in history, checkpoints, events or scripts. `{ maxBytes?, keep? }` (default 10 MB, every image). Off by default; every other binary stays refused. |
105
+ | `toolPaths` | `Record<string, ToolPaths>` | Each host tool's path arguments and how a call uses each (`read`, `list`, `search`, `write`, `remove`, `execute`): `{ get_markdown: { path: "read" } }`. Every path rule then governs them like the built-ins'. Wins over a descriptor's `paths`; declaring a built-in's paths throws `ToolPathsError`. |
101
106
  | `checkpointer` | `BaseCheckpointSaver` | Custom-adapter escape hatch; takes precedence over the session store for checkpoint persistence. Defaults to a durable `BackendCheckpointSaver` writing through the session store. Use `MemorySaver` for ephemeral runs. |
102
107
  | `middleware` | `AgentMiddleware[]` | Extra middleware appended after workflow instrumentation. |
103
108
  | `onEvent` | `WorkflowEventHandler` | Subscribe to all lifecycle diagnostics; suppresses console output when set. |
@@ -429,7 +434,8 @@ opening, a human park, run-start seeding.
429
434
  rides its own event so that rule needs no exception, and it is safe there because
430
435
  a title is bounded to one short line by its write check. Indexing a run's title on
431
436
  your own record is a handler for this event and nothing else — no checkpoint read,
432
- which matters when you install your own checkpointer.
437
+ which matters when you install your own checkpointer. A sub-workflow's child is not
438
+ asked to name itself, so it emits none unless its caller passed a `title` down.
433
439
 
434
440
  ### Reading tokens and cost
435
441
 
@@ -595,6 +601,47 @@ A custom `backend` **requires** an explicit `sessionStore` — assembly throws
595
601
  Persisting the session zone off-box (an S3-backed session store) is how you share run
596
602
  state across a worker fleet and an API.
597
603
 
604
+ The file operations (`copy_file`, `move_file`, `remove_file`) want more than
605
+ text. A write to a binary-typed path (an image, a PDF) carries **base64**, which
606
+ the backend's `write` must decode — Deep Agents' convention. Bytes the text
607
+ channel cannot carry (a `.docx`, a `.zip`, Latin-1 text) travel through
608
+ `uploadFiles`, and a move or removal needs `delete`. Every copy is read back
609
+ (`downloadFiles`, else `readRaw`); a store that kept something else is refused,
610
+ never reported as copied. Implement all three methods and the base64 decode on a
611
+ custom backend — mounts and session store alike. A read-only mount refuses an
612
+ upload and a delete itself.
613
+
614
+ ### Host tools
615
+
616
+ A host tool built with `toolsFromMap` reaches the workspace the way the
617
+ built-in file tools do — no second routing table:
618
+
619
+ ```ts
620
+ const tools = toolsFromMap({
621
+ get_markdown: {
622
+ description: "Read a document as Markdown.",
623
+ inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] },
624
+ paths: { path: "read" }, // governed like read_file's file_path
625
+ handler: async (input, { workspace }) => {
626
+ const raw = await workspace.readRaw(String(input.path));
627
+ return raw.error ? `Error: ${raw.error}` : toMarkdown(raw.data!);
628
+ },
629
+ },
630
+ });
631
+ const agent = await createAgent({ tools, essentialTools: ["get_markdown"] });
632
+ ```
633
+
634
+ - `paths` declares the arguments that name workspace paths and how the call
635
+ uses each. Every path rule binds them: the read-only zone, the runtime's own
636
+ areas, the skills and mounts the state was given (`mount.not-allowed`,
637
+ `skill.not-allowed`), a mount's `access: read` (`mount.read-only` for a
638
+ `write` or `remove`), inherited denials and `paths:` guards. A refused call
639
+ never runs the handler. No declaration, no path rules.
640
+ - `context.workspace` is the turn's workspace, bound to the session — the
641
+ instance `read_file` uses — with `downloadFiles`/`uploadFiles` and `delete`
642
+ besides the text methods. The plain agent hands the same context. Outside a
643
+ turn, reading it throws.
644
+
598
645
  ## Cases against your agent
599
646
 
600
647
  `runTests` runs a workflow's cases — declarative YAML documents
@@ -163,7 +163,7 @@ export default async function hook({ state, phase, trigger, variables, messages,
163
163
  | `variables` | The run's variables as a plain `name → value` map, including the built-in `trigger`. Structured values are present whole (`variables.order.items[0].sku`). **Read-only**: assigning changes nothing, and the variable tools are absent from `tools.*` — a hook that could write variables would be editing the governance inputs of the state it judges. |
164
164
  | `messages` | The recent transcript (last 24 messages), newest last, as `{ role, text, toolCalls? }`. `role` is `user`, `assistant`, `tool` (with `tool: <name>`), `system`, or **`runtime`** — a note the runtime wrote (an arrival, a decision, an error route, a completion check; `note` names the kind). A hook that reacts to narration matches `role === "runtime"`, never a `[bracket]` prefix in a user message. |
165
165
  | `from`, `to`, `reason` | Present on `after` hooks: the transition the agent is attempting (`reason` is the agent's `archmax_advance` reason). |
166
- | `tools` | The privileged tool-call bridge (async), camelCased: `await tools.readFile({ file_path: "skills/order-data/assets/orders.json" })` → file text. See [Governance](#governance-of-tools-calls). |
166
+ | `tools` | The privileged tool-call bridge (async), camelCased: `await tools.readFile({ file_path: "skills/order-data/assets/orders.json" })` → file text, or the binary notice for a binary file (see below). See [Governance](#governance-of-tools-calls). |
167
167
 
168
168
  **The verdict** — the three helpers are globals:
169
169
 
@@ -231,6 +231,32 @@ try {
231
231
  }
232
232
  ```
233
233
 
234
+ A **binary file** is not a refusal and does not throw: reads are text only, so
235
+ `tools.readFile` on an image, audio, video, PDF or PowerPoint file, or on any
236
+ file whose content holds a NUL byte (an archive, an Office document), resolves to
237
+ the notice `Error: '<path>' is a binary file (<type>, <size>) and was not read;
238
+ read_file returns text files only.` instead of content — an image too, even when
239
+ the host turned on `images`, which shows images to the model only. A missing file reads as
240
+ an `Error: …` string the same way. A script that parses what it read checks
241
+ first:
242
+
243
+ ```js
244
+ const text = await tools.readFile({ file_path: "scratchpad/upload.json" });
245
+ const upload = text.startsWith("Error:") ? null : JSON.parse(text);
246
+ ```
247
+
248
+ To **duplicate, move or delete** a file — a template, an image, a `.docx` —
249
+ call `tools.copyFile({ source, destination, overwrite? })`,
250
+ `tools.moveFile({ source, destination, overwrite? })` or
251
+ `tools.removeFile({ file_path })` instead of reading and writing it back. The
252
+ bytes move, binary files included, with no `readFile` line limit, and each
253
+ call resolves to one line (`Copied '<source>' to '<destination>' (<size>).`,
254
+ `Moved …`, `Removed '<path>'.`). They are governed by their declared paths like
255
+ any other call, so a governance refusal (a destination in a read-only mount, a
256
+ disabled skill's file) throws. A failure the operation itself meets (a missing
257
+ source, a directory, an existing destination without `overwrite: true`)
258
+ resolves to an `Error: …` string instead.
259
+
234
260
  **Authoring rule for `archmax_run` scripts:** if sandboxed code calls `tools.X`,
235
261
  the state's `allow` list must cover `tool: X` (with any argument constraints the
236
262
  script's call satisfies), or the call is blocked. Grant it per state with
@@ -535,9 +535,15 @@ states:
535
535
  wiring the SDK already reads, and a loose declaration that swallowed one would
536
536
  start the run somewhere you did not say.
537
537
  - `requires:` / `returns:` are the run's **signature**, and both are enforced. A
538
- firing that does not supply every `requires` name does not start; a run that
539
- reaches a terminal state without every `returns` name set is rejected (a run
540
- that *parks* is not checked — it has not finished). Each entry is a bare
538
+ firing that does not supply every `requires` name does not start. `returns`
539
+ is checked once as the run finishes, and **nothing is handed back**: a
540
+ sub-workflow child with a name unset **completes**, and its caller gets the
541
+ returns it set plus `note: "Not all return variables were set by the
542
+ sub-workflow: '<name>' was not set."`; a top-level run with a name unset is
543
+ **rejected**; a typed one mistyped rejects either way. A run that *parks* is
544
+ not checked — it has not finished. `note` is reserved: a `returns` list may
545
+ not declare it. Want the model to try again? Check the returns in a terminal
546
+ `after` hook, which can `correct`. Each entry is a bare
541
547
  run-variable name (**untyped**: any value, `null` included) or a strict object
542
548
  `{ name, type?, description? }` naming one; the spellings mix in one list and
543
549
  names are distinct across both. What the agent must *do* to produce a
@@ -630,8 +636,8 @@ silently granting nothing. A host tool may never claim the prefix. Deep Agents'
630
636
  (`read_file`, `task`, …) keep their own names.
631
637
 
632
638
  - A state with **no `tools` block** gets the **always-on tools only**
633
- (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `write_todos`,
634
- `archmax_eval`, `archmax_run`, `task`, plus the always-allowed `archmax_advance` and `allow_always`
639
+ (`ls`, `read_file`, `write_file`, `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`,
640
+ `grep`, `write_todos`, `archmax_eval`, `archmax_run`, plus the always-allowed `archmax_advance` and `allow_always`
635
641
  grants — minus
636
642
  whatever a `forbid` list denies there, and read-only-zone protection).
637
643
  Governance is closed by default; there is no way to declare a fully open state.
@@ -710,8 +716,8 @@ platform-provided tool resolves through when its collection has several) and
710
716
  runtime matches governance by `tool` name alone and never reads `source`.
711
717
 
712
718
  **The model is closed by default, and a denial beats every grant.** Every state always has the
713
- **always-on tools** (`ls`, `read_file`, `write_file`, `edit_file`, `glob`,
714
- `grep`, `write_todos`, `archmax_eval`, `archmax_run`). One built-in is **never
719
+ **always-on tools** (`ls`, `read_file`, `write_file`, `edit_file`, `copy_file`, `move_file`,
720
+ `remove_file`, `glob`, `grep`, `write_todos`, `archmax_eval`, `archmax_run`). One built-in is **never
715
721
  disclosed** and grantable by nothing: `task`, the runtime's own dispatch for
716
722
  grading rubrics — a rubric grades the agent rather than serving it, so naming
717
723
  `task` in an allow entry is a `validate` error and the kernel refuses the call.
@@ -722,7 +728,20 @@ default because the `tools.*` calls its code makes are decided against the activ
722
728
  state by the same kernel: it adds computation over what the state permits, not
723
729
  access. `archmax_run` — executing an authored script **file** — is always on on
724
730
  the same terms (the script is a file the state could already read); declare it
725
- per state to **narrow** which files may run there. Any other tool
731
+ per state to **narrow** which files may run there. The file operations —
732
+ `copy_file({ source, destination, overwrite? })`, `move_file` (same arguments)
733
+ and `remove_file({ file_path })` — are always on on the same terms: every tool
734
+ **declares its path arguments** with how the call uses each (`read`, `list`,
735
+ `search`, `write`, `remove`, `execute`), and every path rule reads that
736
+ declaration. `copy_file` reads `source` and writes `destination`; `move_file`
737
+ **removes** `source`, so a move out of a read-only mount is refused before
738
+ anything is written; a removal is refused wherever a write is. A call naming
739
+ several paths is refused when any one is, naming the argument. `paths:` in an
740
+ entry guards every declared path argument: a grant needs all of them to match
741
+ (`{ tool: copy_file, paths: [skills/**, reports/**] }`), a denial needs one
742
+ (`{ tool: "*", paths: [secrets/**] }` blocks a copy in or out). A host tool that
743
+ declares its paths (`toolPaths`, or a descriptor's `paths`) is governed the same
744
+ way. Any other tool
726
745
  must be declared in the state's `tools.allow` or workflow-level
727
746
  `tools.allow_always`. A `tools.allow` entry naming an always-on tool **narrows**
728
747
  it for that state.
@@ -984,7 +1003,7 @@ the host at assembly (`variables`, every seed locked), the agent mid-run
984
1003
 
985
1004
  | | `trigger` | `title` |
986
1005
  |---|---|---|
987
- | Set by | the runtime, at every arrival | the **agent**, prompted by the platform prompt |
1006
+ | Set by | the runtime, at every arrival | the **agent**, prompted by the platform prompt (a sub-workflow child is not) |
988
1007
  | Locked | always | **never**, by any route |
989
1008
  | Guaranteed set | yes | no — absent until written |
990
1009
  | In a trigger's `requires:` | accepted | accepted (seeded unlocked) |
@@ -993,7 +1012,10 @@ the host at assembly (`variables`, every seed locked), the agent mid-run
993
1012
  `trigger` holds the current turn's trigger id. `title` holds a short one-line label
994
1013
  naming the task the run is doing; the platform system prompt asks the agent to set
995
1014
  one as its first activity and update it when the task changes significantly, so a
996
- host can name a run instead of showing a session id. A write must be a non-empty
1015
+ host can name a run instead of showing a session id. A sub-workflow's child is not
1016
+ asked — its title names a session nothing lists and is never returned — so its
1017
+ prompt leaves the step out; do not write "set the title" into a child's state
1018
+ `instructions` to bring it back. A write must be a non-empty
997
1019
  single-line string of at most 200 characters (stored trimmed) and must not pass
998
1020
  `lock: true` — both are refused, atomically, like every other `set_variables`
999
1021
  rejection. A host may seed a `title`; it lands **unlocked** (the one exception to
@@ -1286,11 +1308,21 @@ enrich:
1286
1308
  no target may call none; `tools.forbid_always` blocks a delegation tool like any
1287
1309
  other.
1288
1310
  - **A failure is a tool error** the calling agent can handle; the state's
1289
- `on_error` catches the turn if it cannot. Depth, cycle, missing-input
1311
+ `on_error` catches the turn if it cannot. Each failed call is recorded on
1312
+ its own, with its arguments, and stays the calling state's failure until a
1313
+ **later** successful call of the same workflow from the same state recovers
1314
+ it. One success recovers **one** failed call: the one whose arguments it
1315
+ repeats (compared after `${{…}}` resolves, key order ignored), else the
1316
+ oldest — so two parallel calls that failed need two successes. A sibling
1317
+ call in the same batch, another workflow's success, or a rejection with
1318
+ another cause (a terminal kernel block, exhausted parks) recovers nothing,
1319
+ and the route names every outstanding failure with its arguments. An
1320
+ advance, a park or a reset clears them, as it clears any pending rejection. Depth, cycle, missing-input
1290
1321
  (`missing-param`), mistyped-input (`invalid-param`), unresolvable-reference
1291
1322
  (`unresolved-param`) and **disabled-target** refusals are blocked calls —
1292
- nothing ran, so correct the call and retry. A child whose returns are unset
1293
- (`missing-return`) or mistyped (`invalid-return`) is a failure: it ran.
1323
+ nothing ran, so correct the call and retry. A child whose returns are
1324
+ mistyped (`invalid-return`) is a failure: it ran. One that left returns unset
1325
+ is not: the call answers with what it set and a `note`.
1294
1326
  - **A whole-argument reference keeps its type.** `{ quantity: "${{count}}" }`
1295
1327
  seeds the child with the number `count` holds, so it satisfies an `integer`
1296
1328
  parameter; `{ note: "order ${{order_id}}" }` is substituted as text.
@@ -1327,9 +1359,11 @@ enrich:
1327
1359
  reads `manual` either way, so the machine cannot branch on having been called.
1328
1360
 
1329
1361
  A dispatch missing a `requires` name is **refused before the child is
1330
- composed**; a child that completes without every `returns` name set is
1331
- **rejected** rather than handing back half a contract (a child that *parks* is
1332
- not checked — it has not finished). A typed entry is held to its type at both
1362
+ composed**; a child that completes without every `returns` name set
1363
+ **completes anyway**, and the call answers with the returns it set plus a
1364
+ `note` naming the rest — no second attempt is spent, and the caller decides
1365
+ (a child that *parks* is not checked — it has not finished; a child that
1366
+ raises fails the call `raised`). A typed entry is held to its type at both
1333
1367
  ends: a mistyped argument is refused `invalid-param` before the child runs, and
1334
1368
  a mistyped return fails the call `invalid-return`. What the child must *do* to
1335
1369
  produce a variable belongs in the state `instructions` that set it. The names,
@@ -1344,13 +1378,24 @@ enrich:
1344
1378
  agent records what it needs with `archmax_set_variables`, or a script writes it,
1345
1379
  and the calling state's `requires:` is what makes that mandatory.
1346
1380
  - **Mocks are held to the same contract.** An `archmax_workflow_<slug>` mock for a
1347
- signed target supplies `returns:`, and one that omits a declared name
1348
- (`missing-return`) or supplies a mistyped typed one (`invalid-return`) fails
1349
- the mocked dispatch — a mock stands in for the sub-run, never for its agreement.
1381
+ signed target supplies `returns:`. One that supplies a mistyped typed one
1382
+ (`invalid-return`) fails the mocked dispatch, and one that omits a declared
1383
+ name answers like a real child that left it unset: what it supplied, plus a
1384
+ `note` — a mock stands in for the sub-run, never for its agreement.
1350
1385
  - **No prose crosses the boundary.** A sub-run gets no instruction from its
1351
- caller — it works from its own state `instructions` plus the arguments it was
1352
- seeded with as locked variables, both of which reach the model through the
1353
- system prompt. Nothing to author, nothing to keep in sync.
1386
+ caller — it works from its own state `instructions` (system prompt) plus the
1387
+ arguments it was seeded with as locked variables, whose values its opening
1388
+ message lists: strings JSON-quoted, numbers and booleans as written. A list,
1389
+ an object, `null`, a value rendering past 200 characters, or one past the
1390
+ message's 1,000-character total is only named, with `archmax_get_variables`
1391
+ as the way to read it. Nothing to author, nothing to keep in sync, and nothing
1392
+ in the child's state `instructions` should say "read your inputs first".
1393
+ - **A child goes straight to its work.** Its prompt leaves out the platform
1394
+ prompt's "Name the run first" step (a child's `title` is never returned), so a
1395
+ one-state child with short scalar inputs spends its first model call on the
1396
+ state's work. A workspace overriding the platform prompt
1397
+ (`.platform/system/GRAPH_STATE.md`) keeps that by wrapping what children skip
1398
+ in `<!-- top-level-only -->` … `<!-- /top-level-only -->` lines.
1354
1399
  - **Same agent, isolated context, shared session.** Same model, backend, host
1355
1400
  tools; its **own** grading rubrics, from its own spec; a fresh transcript; the child's *own* per-state
1356
1401
  `tools.allow`. It shares the run's session, so it writes `scratchpad/…` and
@@ -48,7 +48,8 @@ states:
48
48
  requires: # variables a firing must supply (a caller's required params):
49
49
  - order_id # a bare name is untyped (any value, null included)
50
50
  - { name: due, type: date, description: The day it is due. } # typed: held at the boundary
51
- returns: [result_file] # variables the run guarantees on completion (a caller's result)
51
+ returns: [result_file] # variables the run guarantees on completion (a caller's result);
52
+ # short of them: a child returns what it set + a `note`; a top-level run is rejected
52
53
  # entry types: string integer number boolean date date-time object array
53
54
  instructions: >-
54
55
  Record the decision in scratchpad/refund.json, then archmax_advance to
@@ -119,7 +120,8 @@ states:
119
120
  Host seeds (`--variables`, `createAgent({ variables })`, a delivery) are
120
121
  **locked**; the agent uses `archmax_get_variables`/`archmax_set_variables`
121
122
  (`lock: true` to freeze). Reserved: `trigger` (current trigger id, always
122
- locked) and `title` (the run's one-line task label, agent-owned). A
123
+ locked) and `title` (the run's one-line task label, agent-owned; a
124
+ sub-workflow child is not asked for one). A
123
125
  `${{name}}` / `${{a.b.-1.c}}` reference works in `tools.allow` globs
124
126
  (guard) and inside any text tool argument the model writes (exact
125
127
  substitution, no retyping) — but not in scripts, which read
@@ -185,7 +187,16 @@ states:
185
187
  `requires` are the parameters (typed where the entries are typed; a
186
188
  mistyped argument is refused `invalid-param`, and `"${{count}}"` alone
187
189
  passes the value with its own type), its `returns` the result
188
- (`{ message, returns }`; a mistyped typed return fails `invalid-return`).
190
+ (`{ message, returns }`; a child finishing with some unset completes, and
191
+ `returns` holds what it set plus a `note` naming the rest; a mistyped typed
192
+ return fails `invalid-return`). Each failed call is the
193
+ calling state's failure, recorded with its arguments and routed through
194
+ `on_error` when the agent finishes unless a later successful call of the
195
+ same workflow from the same state recovers it first. One success recovers
196
+ one failed call: the one whose arguments it repeats (compared after
197
+ `${{…}}` resolves, key order ignored), else the oldest. A sibling in the
198
+ same batch does not count; another workflow's success recovers nothing;
199
+ the route names every outstanding failure with its arguments.
189
200
  The trigger's `description` leads the tool description. Nothing captures the result — the caller records
190
201
  what it needs with `archmax_set_variables`, made mandatory by `requires`.
191
202
  The child is a separate session with a fresh transcript, sees only its
@@ -239,7 +250,8 @@ no way to see more of the graph.
239
250
  Closed by default; **deny beats allow, and no narrower level widens a denial.**
240
251
 
241
252
  - **Always on, never declared**: `ls`, `read_file`, `write_file`,
242
- `edit_file`, `glob`, `grep`, `write_todos`, `archmax_eval`, `archmax_run`,
253
+ `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`,
254
+ `write_todos`, `archmax_eval`, `archmax_run`,
243
255
  and the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
244
256
  `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`. Any other
245
257
  tool needs an
@@ -247,8 +259,15 @@ Closed by default; **deny beats allow, and no narrower level widens a denial.**
247
259
  it. `task` is ungrantable (error).
248
260
  - **Entries**: bare name; `{ tool, args: { <arg>: ["glob", …] } }` (matches
249
261
  if any glob matches; `${{var}}` allowed); `{ tool, paths: [...] }`
250
- (shorthand for `args.file_path`). `"*"` as the tool is legal only in a
262
+ (guards every path argument the tool declares — all must match to grant,
263
+ any matching denies). `"*"` as the tool is legal only in a
251
264
  forbid list. Globs match dot-segments (`secrets/**` covers `secrets/.env`).
265
+ - **Path rules read declared path arguments**: `copy_file`/`move_file` take
266
+ `{ source, destination, overwrite? }` (copy reads `source`, move removes it;
267
+ both write `destination`), `remove_file` takes `{ file_path }`. A removal is
268
+ refused wherever a write is; a call naming several paths is refused when any
269
+ one is. An existing destination is refused unless `overwrite: true`.
270
+ `forbid: [{ tool: "*", paths: [secrets/**] }]` blocks a copy in or out.
252
271
  - **Skills** are governed by **slug**, never path. Enabling a bundle makes
253
272
  `skills/<slug>/**` readable and its scripts runnable with no `tools.allow`
254
273
  entry; a disabled bundle is unreachable **and invisible** (not listed, not