@archmax-ai/harness 1.1.0 → 2.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.
Files changed (70) hide show
  1. package/README.md +167 -170
  2. package/dist/agent.d.ts +2 -2
  3. package/dist/agent.js +2 -2
  4. package/dist/assembly/compose.js +2 -2
  5. package/dist/assembly/compose.js.map +1 -1
  6. package/dist/authoring-skill/archmax-harness/README.md +13 -15
  7. package/dist/authoring-skill/archmax-harness/SKILL.md +174 -264
  8. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +244 -776
  9. package/dist/authoring-skill/archmax-harness/references/cases.md +178 -53
  10. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +211 -100
  11. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +57 -78
  12. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +613 -2006
  13. package/dist/core/deepagents.d.ts +1 -2
  14. package/dist/core/deepagents.js +1 -2
  15. package/dist/core/deepagents.js.map +1 -1
  16. package/dist/kernel/kernel.js +2 -2
  17. package/dist/kernel/kernel.js.map +1 -1
  18. package/dist/machine/lint-spec.js +6 -5
  19. package/dist/machine/lint-spec.js.map +1 -1
  20. package/dist/machine/machine.d.ts +8 -10
  21. package/dist/machine/machine.js +11 -12
  22. package/dist/machine/machine.js.map +1 -1
  23. package/dist/machine/spec-schema.d.ts +1 -4
  24. package/dist/machine/spec-schema.js +66 -47
  25. package/dist/machine/spec-schema.js.map +1 -1
  26. package/dist/machine/tool-names.d.ts +24 -34
  27. package/dist/machine/tool-names.js +43 -63
  28. package/dist/machine/tool-names.js.map +1 -1
  29. package/dist/machine/types.d.ts +11 -10
  30. package/dist/machine/types.js.map +1 -1
  31. package/dist/public/spec.d.ts +5 -5
  32. package/dist/public/spec.js +5 -5
  33. package/dist/public/spec.js.map +1 -1
  34. package/dist/validate/validate.js +3 -3
  35. package/dist/validate/validate.js.map +1 -1
  36. package/dist/workflow/automated.js +6 -1
  37. package/dist/workflow/automated.js.map +1 -1
  38. package/dist/workflow/control-tools.js +0 -2
  39. package/dist/workflow/control-tools.js.map +1 -1
  40. package/dist/workflow/grader-state.d.ts +2 -2
  41. package/dist/workflow/grader-state.js +5 -15
  42. package/dist/workflow/grader-state.js.map +1 -1
  43. package/dist/workflow/middleware.js +2 -26
  44. package/dist/workflow/middleware.js.map +1 -1
  45. package/dist/workflow/on-error.js +0 -1
  46. package/dist/workflow/on-error.js.map +1 -1
  47. package/dist/workflow/parks.js +0 -1
  48. package/dist/workflow/parks.js.map +1 -1
  49. package/dist/workflow/render-prompt.d.ts +4 -5
  50. package/dist/workflow/render-prompt.js +24 -27
  51. package/dist/workflow/render-prompt.js.map +1 -1
  52. package/dist/workflow/state.d.ts +0 -9
  53. package/dist/workflow/state.js +0 -3
  54. package/dist/workflow/state.js.map +1 -1
  55. package/dist/workflow/turn-boundary.js +0 -1
  56. package/dist/workflow/turn-boundary.js.map +1 -1
  57. package/package.json +1 -2
  58. package/skills/archmax-harness/README.md +13 -15
  59. package/skills/archmax-harness/SKILL.md +174 -264
  60. package/skills/archmax-harness/references/backend-integration.md +244 -776
  61. package/skills/archmax-harness/references/cases.md +178 -53
  62. package/skills/archmax-harness/references/code-states-and-scripts.md +211 -100
  63. package/skills/archmax-harness/references/diagnostics.md +57 -78
  64. package/skills/archmax-harness/references/workflow-schema.md +613 -2006
  65. package/dist/authoring-skill/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  66. package/dist/authoring-skill/archmax-harness/references/wiring.md +0 -119
  67. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +0 -402
  68. package/skills/archmax-harness/references/code-state-and-test-scripts.md +0 -1424
  69. package/skills/archmax-harness/references/wiring.md +0 -119
  70. package/skills/archmax-harness/references/workflow-yaml.md +0 -402
@@ -1,2070 +1,677 @@
1
- # workflow.yaml — exhaustive schema reference
1
+ # `workflow.yaml` reference
2
2
 
3
- The complete, field-by-field reference for authoring a `workflow.yaml` (and its
4
- optional `WORKFLOW.md` prose addendum) plus the surrounding workspace. Targets
5
- **runtime contract version `"3"`**.
3
+ Load this when you write or edit a `workflow.yaml`. It holds every key, the rules the
4
+ machine enforces, and governance. How grader and code states run, and how to write
5
+ scripts, is in [`code-states-and-scripts.md`](code-states-and-scripts.md). Cases are in
6
+ [`cases.md`](cases.md).
7
+
8
+ The schema is strict: an unknown key anywhere is a load error that names the key. The
9
+ one free slot is `metadata`. A trigger declaration also keeps a host's own keys, with a
10
+ warning.
6
11
 
7
12
  ## Contents
8
13
 
9
- - [File shape](#file-shape)
10
- - [Root fields](#root-fields)
11
- - [States (`MachineState`)](#states-machinestate)
12
- - [Ending a run as a failure: `archmax_raise`](#ending-a-run-as-a-failure-archmax_raise)
13
- - [Transitions](#transitions)
14
+ - [The skeleton](#the-skeleton)
15
+ - [Files](#files)
16
+ - [Root keys](#root-keys)
17
+ - [State keys](#state-keys)
18
+ - [Transitions and what the agent sees](#transitions-and-what-the-agent-sees)
19
+ - [Triggers and signatures](#triggers-and-signatures)
20
+ - [Variables](#variables)
14
21
  - [Tool governance](#tool-governance)
15
22
  - [Skill governance](#skill-governance)
16
23
  - [Mount governance](#mount-governance)
17
- - [Grader and code states](#grader-and-code-states)
18
- — full script contract + worked examples in
19
- [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md)
20
- - [Budgets & error routing](#budgets--error-routing)
21
- - [Human states](#human-states)
22
- - [Sub-workflows are tools](#sub-workflows-are-tools)
23
- - [Removed keys you may meet in an older workflow](#removed-keys-you-may-meet-in-an-older-workflow)
24
- - [Workspace layout: skills, scripts](#workspace-layout)
25
- - [Cases](#cases)
26
- - [Code description docblocks](#code-description-docblocks)
27
- - [Read-only zones](#read-only-zones)
28
- - [Annotated example: order-lookup](#annotated-example-order-lookup)
29
-
30
- ---
31
-
32
- ## File shape
33
-
34
- **Two-file layout** under `workflows/<name>/`:
35
-
36
- - **`workflow.yaml`** — the canonical, enforced machine spec: a **pure YAML
37
- mapping** (no `---` frontmatter fences, no markdown). Parsed with YAML 1.2
38
- semantics and validated against the schema. Top-level keys: `title`,
39
- `instructions`, `disabled`, `runtime`, `settings`, `tests`, `tools`, `skills`,
40
- `mounts`, `metadata`, `states`.
41
- - **`WORKFLOW.md`** (optional, beside it) — a plain-markdown prose addendum with
42
- **no frontmatter**. A frontmatter block in `WORKFLOW.md` when `workflow.yaml`
43
- exists is a **validation error**. The runtime renders the system prompt's
44
- workflow header deterministically from the spec (`title` + `instructions`, and
45
- NO state of the graph) and appends the `WORKFLOW.md` prose after it — prose is
46
- never load-bearing and cannot disagree with enforcement. The graph is disclosed
47
- per model call, for the ACTIVE STATE ONLY.
48
- There is no "frontmatter wins" rule anymore; `workflow.yaml` is the only
49
- authority.
50
-
51
- **Removed**: the single-file `WORKFLOW.md` layout (YAML frontmatter + markdown
52
- body, without a `workflow.yaml`) no longer loads — it is a load error. Migrate by
53
- moving the frontmatter into `workflow.yaml` and stripping the fences from the
54
- prose.
55
-
56
- ## Root fields
57
-
58
- ```yaml
59
- title?: string # the workflow's HUMAN-READABLE name (metadata only).
60
- # Identity is the WORKFLOW SLUG — the directory name
61
- # (`workflows/<slug>/`, also the CLI argument), authored
62
- # in the SAME kebab-case as state slugs
63
- # (`order-lookup`); validate ERRORS on a missing or
64
- # non-kebab workflow slug, exactly as for a state slug.
65
- # A top-level `name:` is REMOVED — it is
66
- # a load/validate ERROR; rename it to `title:`.
67
- instructions?: string # the workflow's STANDING instructions — free-form
68
- # markdown, direction that holds for the WHOLE run
69
- # (conventions, house rules, what "done" means).
70
- # Author it as a LITERAL block (`|-`), not folded
71
- # (`>-`): it renders as markdown, and folding
72
- # collapses blank lines into one run-on paragraph.
73
- # Rendered verbatim as an `## Instructions` block at
74
- # the top of the workflow prompt section (unlike a
75
- # state's `instructions`, which are per model call) —
76
- # so it lands in the static,
77
- # cacheable prefix. validate WARNS when absent,
78
- # ERRORS when present but not a non-empty string.
79
- # A top-level `description:` is REMOVED and now
80
- # simply ignored; use `instructions:`, rereading the
81
- # text as direction, not narration.
82
- # A test case's `description` is UNAFFECTED. A
83
- # TRANSITION's `description` is what the agent routes
84
- # on (see `transitions` below).
85
-
86
- disabled?: boolean # `true` = this machine STARTS NO NEW TURNS. Bounds
87
- # STARTING, not finishing.
88
- # REFUSES: `archmax run` (one line, non-zero exit, no
89
- # session minted); a host `invoke`; a firing on a fresh
90
- # session; a FOLLOW-UP message to a settled session
91
- # (a new turn is new work) — all rejected AT THE TURN
92
- # BOUNDARY with `status: rejected` and
93
- # "Refusing to start: workflow '<slug>' is disabled.",
94
- # before any state or model call; and a caller's
95
- # `archmax_workflow_<slug>` call (blocked call, refusal
96
- # kind `disabled`, before a child is composed — it also
97
- # beats a declared MOCK, so a case can never assert a
98
- # delegation production would refuse).
99
- # STILL WORKS: assembly; `decide` / `reply` / `deliver`
100
- # (each resumes an unfinished turn, so parked human
101
- # decisions are never trapped); `archmax validate` (full,
102
- # plus a warning); and a CALLER's assembly — the tool is
103
- # still bound, so disabling one leaf never takes its
104
- # callers offline (validate WARNS on each state that
105
- # allows a disabled target).
106
- # `archmax test` SKIPS the suite and exits 0 — not run is
107
- # not failed. There is NO flag to force cases to run.
108
- # Read FAIL-CLOSED: anything but an absent key, `null` or
109
- # boolean `false` disables the workflow, so `disabled:
110
- # "no"` STOPS runs; validate ERRORS on a non-boolean.
111
- # NOT a kill switch for work in flight, and NOT a
112
- # per-state/per-trigger switch (that is a forbid list).
113
-
114
- runtime?:
115
- engine?: string # "archmax-harness"
116
- version?: string | number # target contract — "3", the only one supported (and
117
- # the default when omitted). "1"/"2" are refused as
118
- # an unsupported runtime contract, naming the
119
- # supported set.
120
-
121
- # There is NO root `budget`: declaring one is an unknown-key load error
122
- # (`Unrecognized key: "budget"`). A whole turn — one message, decision, reply or
123
- # delivery, across every state — is bounded by LangGraph's `recursionLimit`:
124
- # 400 unless the host's invoke config sets its own, which wins. A turn that hits
125
- # it ends the session `failed` (exit code `recursion-limit`). A state's own
126
- # `budget` bounds that state (see Budgets & error routing).
127
-
128
- settings?:
129
- model?: string # MODEL ID ONLY — the id every state of this workflow runs
130
- # on, over the endpoint/credentials the assembly already
131
- # has. Overrides ARCHMAX_MODEL; a state's `model` overrides
132
- # it. NOT a block: `{ id, temperature }` is a load error —
133
- # sampling and endpoint are env/host configuration.
134
- # Opaque: nothing validates the id against a list.
135
- # A grader state runs on it too unless it declares its
136
- # own `model`. Does NOT change the case judge
137
- # (tests.judge.model).
138
- timeoutMs?: number # default 15000; per sandbox evaluation — one
139
- # archmax_eval/archmax_run call, or a code state's
140
- # whole script
141
- memoryLimitBytes?: number
142
- maxPtcCalls?: number | null
143
- maxResultChars?: number
144
- prompt_cache?:
145
- enabled?: boolean # default true (provider prompt caching)
146
- ttl?: "5m" | "1h" # default "5m"
147
-
148
- tests?: # cases suite config (declarative data;
149
- # tests.config.js is REMOVED — see Cases)
150
- maxConcurrency?: number # only 1 (or absent) accepted — concurrency is
151
- # not implemented; >1 is a validate error and
152
- # fails `archmax test` loudly
153
- caseTimeoutMs?: number # per-case WALL-CLOCK budget, default 120000 —
154
- # deliberately named differently from
155
- # settings.timeoutMs (per-script sandbox budget)
156
- judge?: # {} = enable the case judge (`grade:`) with the
157
- model?: string # env-configured default model — unrelated to
158
- # grader states
159
- modelOptions?: { temperature?: number, maxTokens?: number }
160
- # applied over the env model config, via the
161
- # factory's `judge` role; other keys are errors
162
-
163
- tools?:
164
- allow_always?: AllowEntry[] # the WORKFLOW-WIDE GRANT, in every state. ABSENT = the
165
- # default tools (ls, read_file, write_file, edit_file,
166
- # glob, grep, copy_file, move_file, remove_file,
167
- # write_todos, archmax_eval, archmax_run). DECLARED
168
- # ([] included) = exactly what it lists: it REPLACES
169
- # the default tools, so list each one you want. A
170
- # new workflow lists all twelve.
171
- forbid_always?: ForbidEntry[]
172
- # DENIED in EVERY state, and absolute: nothing reaches
173
- # past it — no state grant, allow_always entry,
174
- # argument guard or consumer rule. Same entry
175
- # grammar as allow, plus the tool "*" meaning
176
- # EVERY tool: { tool: "*", paths: ["logs/**"] }
177
- # denies that path to everything. "*" in an
178
- # ALLOW list is a load error.
179
- # Guards narrow what is denied; globs match
180
- # dotfiles (secrets/** covers secrets/.env).
181
- # Inherited by every sub-workflow, naming the
182
- # workflow that declared it. There is NO
183
- # `policy:` block — writing one is a load error.
184
-
185
- skills?:
186
- allow_always?: string[] # SKILL SLUGS enabled in EVERY state of this workflow —
187
- # the bundle directory names under skills/, kebab-case.
188
- # NEVER paths, NEVER globs: the unit you govern is the
189
- # capability. This is the skills half of
190
- # tools.allow_always: a GRANT, which a state's own
191
- # `skills.allow` ADDS to (the enabled set is the UNION),
192
- # so no state repeats it and no state can subtract from
193
- # it. ABSENT = none granted workflow-wide, exactly like
194
- # `allow_always: []` — write the empty list when every
195
- # capability here is state-specific.
196
- # validate ERRORS on: a slug no source provides (it
197
- # lists the known ones), a path/glob/non-string entry,
198
- # a bundle whose SKILL.md `name:` ≠ its directory; and
199
- # WARNS on a state entry this list already grants
200
- # everywhere, and on a state's `allow: []` beside it
201
- # (an empty state list subtracts nothing).
202
- forbid_always?: string[] # SKILL SLUGS out of reach of EVERY state, whatever any
203
- # list grants. Deny beats allow. (The retired root
204
- # `allow` ceiling is a load error: one skills model.)
205
-
206
- mounts?:
207
- allow_always?: MountGrant[] # MOUNT NAMES reachable in EVERY state of this workflow —
208
- # the keys of the HOST's mount table with their slashes
209
- # stripped (`reference`, `catalogs/eu`, `AGENTS.md`).
210
- # NEVER globs: scope paths WITHIN a reachable mount with
211
- # a tools.allow entry. Only a mount the host declared
212
- # `governed: true` needs a grant — an UNGOVERNED mount is
213
- # visible in every state already, and a grant naming one
214
- # is inert (validate warns). The skills model exactly: a
215
- # GRANT a state's `mounts.allow` ADDS to (the reachable
216
- # set is the UNION), never a ceiling. ABSENT = this
217
- # workflow reaches no governed mount; validate says so
218
- # ONCE, naming them — write `allow_always: []` to mean it.
219
- # An entry is a NAME, or `{ mount: <name>, access: read |
220
- # read_write }` to say what this level gives. A bare NAME
221
- # asks for NO write: a governed mount is written only
222
- # where some grant of THIS workflow says `read_write`
223
- # (a child's or caller's grant never counts). The host's
224
- # readOnly is a CEILING: `read_write` on a read-only mount
225
- # opens nothing (validate warns); `read` NARROWS a
226
- # writable one, and no narrower level widens it back.
227
- # validate ERRORS on a name the table does not mount.
228
- forbid_always?: string[] # MOUNT NAMES out of reach of EVERY state, governed or
229
- # not, whatever any list grants. Deny beats allow.
230
- # Binds this workflow's grader and code states too,
231
- # and is inherited by every sub-workflow, naming the workflow
232
- # that declared it. A NAME only — a denial is total, so
233
- # there is no `access` to qualify. (A root `mounts.allow`
234
- # is a load error: one mounts model.)
235
-
236
- metadata?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
237
- # UI's own state (canvas zoom, pan). Also accepted on
238
- # a state (its position). The one key the runtime
239
- # never reads: not shape-checked, not prompted,
240
- # not governed, and excluded from the spec hash.
241
- # Do NOT author it by hand and do NOT invent a
242
- # use for it — but PRESERVE it verbatim when you
243
- # rewrite a spec that already carries one, or the
244
- # builder that wrote it loses every state position.
245
- # The root is otherwise STRICT: any other
246
- # unrecognized key is a load error — `extensions:`
247
- # included.
248
-
249
- states: Record<slug, MachineState> # REQUIRED; ≥1 state, ≥1 with a trigger
250
- # the KEY is the state's SLUG (see below)
251
- ```
252
-
253
- ## States (`MachineState`)
254
-
255
- Each key under `states` is the state's **slug**: its identity, and the only thing
256
- that resolves it — `transitions[].to`, `on_error`, the `to` the agent passes to
257
- `archmax_advance`, emitted events, session artifacts, and cases
258
- `reachedState` assertions are all slugs. Author slugs as **hyphen-separated kebab-case** — lowercase alphanumeric
259
- segments joined by single hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`, e.g.
260
- `identify-case`, `report-requested`); no underscores, capitals, doubled hyphens,
261
- or leading/trailing hyphens —
262
- `validate` reports a nonconforming slug — or a state declared under an empty key,
263
- which has no identity at all — as an **error**. A slug is a reference target, so
264
- rename one only deliberately, and update every reference to it.
24
+ - [Moving, parking and failing](#moving-parking-and-failing)
25
+ - [Budgets and `on_error`](#budgets-and-on_error)
26
+ - [Sub-workflows](#sub-workflows)
27
+ - [Models](#models)
28
+ - [Prompt economy](#prompt-economy)
29
+ - [Removed keys](#removed-keys)
30
+ - [Paths and zones](#paths-and-zones)
265
31
 
266
- ```yaml
267
- states:
268
- <state-slug>:
269
- requires?: [string] # RUN VARIABLES this state must have set before it can
270
- # be left: archmax_advance out of it is refused,
271
- # recoverably, while any is unset. Variables are not
272
- # declared anywhere, so this list — together with a
273
- # TRIGGER's `requires` (see below) — is
274
- # the only spec-visible statement that a variable will
275
- # exist, which is what makes a ${{name}} guard
276
- # reference GUARANTEED rather than dependent on agent
277
- # behavior. Name here anything a downstream guard
278
- # reads.
279
- locks?: [string] # RUN VARIABLES this state OWNS: only it may write them,
280
- # whenever it is active, before or after they are set; a write
281
- # from another state is refused naming the owner. To
282
- # change one (the person meant another order), the
283
- # session goes BACK to the owner, whose rules apply
284
- # again, and later guards re-read it — so give the
285
- # owner an edge back from the states that use it. Any
286
- # variable, guarded or not. One owner per variable;
287
- # never `trigger`/`title`; not on a human state.
288
- # `lock: true` is refused for an owned variable, and a
289
- # host-supplied value with the same name stays final.
290
- # Pair with `requires` to make the owner set it.
291
- title?: string # HUMAN-READABLE label. Presentational only: carried
292
- # in a human state's pending-decision record and shown
293
- # in host surfaces. REACHES NO MODEL CALL. Never a
294
- # routing target — `to` always takes a slug — so
295
- # rewording it changes no behavior. Not unique.
296
- # NOT a substitute for `instructions` (behavior
297
- # guidance) or a transition `description` (routing).
298
-
299
- type?: "agent" | "human" | "grader" | "code"
300
- # default "agent" = the session's agent works here.
301
- # "human" = a person routes.
302
- # "grader" = an isolated grader judges the session
303
- # and decides the edge (see Grader and code states).
304
- # "code" = the runtime runs `script` and the
305
- # script decides the edge.
306
- # grader + code = AUTOMATED states: run on entry
307
- # with no call of the agent's model.
308
- # (`workflow` was removed: a state CALLS another
309
- # workflow by allowing `archmax_workflow_<slug>`.)
310
-
311
- script?: string # CODE STATES ONLY, and REQUIRED there: the file the
312
- # state runs, workflow-relative inside `scripts/`,
313
- # ending .js or .mjs, no `..` segment —
314
- # script: scripts/check-refund.js
315
- # resolves to workflows/<slug>/scripts/check-refund.js
316
- # on the authoring plane. A code state without it,
317
- # or `script` on any other type, is a LOAD ERROR.
318
-
319
-
320
- triggers?: Record<triggerId, TriggerDeclaration | null>
321
- # marks a start state. The KEY is the trigger id
322
- # and THIS state is that trigger's entry state —
323
- # the one place a trigger is declared.
324
- # manual: = CLI/Deep-Agent entry
325
- # (prompt = input)
326
- # <other id>: = tool/event trigger
327
- # a null/empty value = the id and nothing more
328
- # SEVERAL keys = the state is the entry for EVERY
329
- # id, in declaration order (one state, one
330
- # behavior, several ways in).
331
- # The value is the declaration (see below):
332
- # session/message/connection/description/
333
- # requires/returns
334
- # plus any key the HOST adds — preserved,
335
- # lint-warned, never interpreted.
336
- # NO `entry:` and NO `name:` inside a declaration:
337
- # the state it sits on IS the entry and the key
338
- # it sits under IS the id. Either one fails load
339
- # naming the key. There is also no root
340
- # `triggers:` block.
341
- # An id is AUTHORED, so deleting or reordering a
342
- # sibling never moves it — hosts key deployments
343
- # on that id.
344
- # two states declaring the same trigger id is a
345
- # validation ERROR (start states must be
346
- # unambiguous). A trigger has ONE entry state; a
347
- # mapping with several keys is several triggers
348
- # entering one state. A repeated id on one state
349
- # is impossible — YAML mappings cannot repeat a key.
350
- # A shared entry state has ONE behavior: branch on
351
- # `${{trigger}}` / `args.trigger` where the paths
352
- # differ, or use separate states.
353
-
354
- summary?: string # ONE-LINE label for AUTHORS AND HOSTS (an editor's
355
- # node caption, a generated diagram). RENDERED INTO
356
- # NO PART OF THE PROMPT — the model never reads it,
357
- # so it is free, and it can NOT carry routing logic.
358
- # The routing text the agent reads is the SOURCE
359
- # state's transition `description`s, or a target's
360
- # `instructions` where an edge is undescribed; a
361
- # target's summary is never disclosed.
362
-
363
- instructions?: string # behavior guidance.
364
- # agent state: surfaced to the LLM while active.
365
- # Write the state's behavior INLINE here; a
366
- # skill bundle (skills/<slug>/) is a capability
367
- # with data or scripts, enabled by slug — not a
368
- # per-state instruction sheet.
369
- # human state: REQUIRED — the decision for the packet
370
- # grader state: REQUIRED — its criteria, the
371
- # grader's system prompt after a compiled-in
372
- # preamble; never shown to the agent
373
- # code state: a validate ERROR (nothing reads it)
374
- # There is NO `before:`/`after:` on a state: hooks
375
- # are removed, and a leftover key is a load error
376
- # naming it (rewrite it as a grader or code state).
377
-
378
- tools?:
379
- allow?: AllowEntry[] # grants beyond the workflow-wide grant (plus the
380
- # always-allowed archmax_advance); an entry naming
381
- # a tool that grant includes NARROWS it, and one
382
- # naming a default tool a declared allow_always
383
- # leaves out GRANTS it here.
384
- forbid?: ForbidEntry[] # what this state DENIES while active — beating the
385
- # workflow-wide grant, this state's own allow,
386
- # and a host's essentialTools alike. Deny beats allow.
387
- # Not inherited by a sub-workflow. Binds a grader's
388
- # and a code state's calls exactly as the agent's.
389
-
390
- skills?:
391
- allow?: string[] # which CAPABILITIES this state ADDS, by SLUG — on top
392
- # of the root's `skills.allow_always`, which is enabled
393
- # here whatever this says. ABSENT = adds none, the same
394
- # as `allow: []`. An empty list is NOT a deny (validate
395
- # warns about one that reads like it) — `forbid` is.
396
- forbid?: string[] # SLUGS this state does NOT get, subtracting from the
397
- # workflow's allow_always and from its own allow.
398
- # Refused as `skill.forbidden` (vs `skill.not-allowed`
399
- # where nothing granted it).
400
- # Enabling a slug is a GRANT: skills/<slug>/** becomes
401
- # readable and its scripts runnable with NO tools.allow
402
- # path entry. Not enabling one makes the bundle
403
- # unreachable (read/ls/glob/grep/archmax_run all
404
- # blocked, rule `skill.not-allowed`, above every
405
- # declaration) AND undisclosed — its slug and
406
- # description never enter this state's prompt, and a
407
- # listing of skills/ does not show it.
408
- # A tools.allow entry still NARROWS within the enabled
409
- # set: the two compose as enabled AND allowed.
410
- # A grader or code state is bound like the agent: a
411
- # code state reading skills/order-data/assets/orders.json
412
- # enables `order-data` in ITS OWN list. To gate the
413
- # agent's use of a capability while a check still
414
- # reads it, enable it on the check's state only.
415
-
416
- mounts?:
417
- allow?: MountGrant[] # which MOUNTS this state ADDS, by NAME — on top of the
418
- # root's `mounts.allow_always`, which is reachable here
419
- # whatever this says. ABSENT = adds none, the same as
420
- # `allow: []`. An empty list is NOT a deny (validate
421
- # warns about one that reads like it) — `forbid` is.
422
- # An entry may name its access:
423
- # allow: [{ mount: shared, access: read }]
424
- # reads a writable mount without writing it here, and
425
- # allow: [{ mount: shared, access: read_write }]
426
- # asks to write it — which a bare name never does. The
427
- # host's posture is the ceiling — `read_write` cannot
428
- # open a read-only mount — and a `read` at either level
429
- # holds, so a state never widens what the workflow
430
- # narrowed. A write refused by THIS key is
431
- # `mount.read-only`; one refused by the WIRING is
432
- # `zone.read-only`.
433
- forbid?: string[] # MOUNT NAMES this state does NOT get, subtracting from
434
- # the workflow's allow_always, from its own allow, and
435
- # from an UNGOVERNED mount's standing visibility alike.
436
- # Refused as `mount.forbidden` (vs `mount.not-allowed`
437
- # where nothing enabled it).
438
- # A governed mount this state does not have is
439
- # unreachable (read/ls/glob/grep all blocked, above
440
- # every declaration) AND undisclosed — absent from the
441
- # volatile prompt block, and filtered out of every
442
- # ls/glob/grep result, the WORKSPACE ROOT LISTING
443
- # included. Binds a grader's and a code state's
444
- # calls exactly as the agent's: grant the mount on
445
- # the automated state that reads it.
446
-
447
- model?: string # MODEL ID ONLY — this state's turns run on it (and the
448
- # handoff message of a park it holds). Most specific of
449
- # state.model > settings.model > the assembly's default
450
- # (createAgent `model`/`modelFactory`, else ARCHMAX_MODEL).
451
- # Same id as settings.model ⇒ validate WARNS (inert).
452
- # On a GRADER state: the id the grader runs on, resolved
453
- # under the `grader` model role
454
- # (modelFactory("grader", env, id)). On a CODE state: a
455
- # validate ERROR. A sub-workflow resolves its OWN
456
- # spec's ids — nothing is inherited across it.
457
- # A host passing an explicit `model` INSTANCE to
458
- # createAgent outranks every declared id (one assembly
459
- # warning names them); `modelFactory` is what honours one.
460
-
461
- budget?: # per-state execution budget
462
- maxTurns?: number # bounds this turn's agent loop (on a grader: the
463
- # grader's own model calls)
464
- timeoutMs?: number # aborts the turn when exceeded (on a grader: its run)
465
- maxParks?: number # bounds archmax_wait parks here (see Parking); an
466
- # ERROR on a grader, which cannot park
467
- # Any `budget` on a CODE state is a validate ERROR:
468
- # settings.timeoutMs bounds its script.
469
- on_error?: string # state to route to on a TERMINAL failure
470
- # (see Budgets & error routing)
471
-
472
- transitions?: Transition[] # outgoing edges. EMPTY/absent ⇒ terminal state.
473
- # `parallel:`/`join:` are REMOVED — a run traverses
474
- # one state at a time; sequence what were branches.
475
- #
476
- # Transition = {
477
- # to: string # a declared state's SLUG
478
- # description?: string # what the agent routes on
479
- # type?: "approve" | "reject" | "refine" | "none"
480
- # }
481
- #
482
- # THE AGENT SEES THIS STATE'S EDGES AND NOTHING
483
- # ELSE OF THE GRAPH — no other state's slug, title,
484
- # summary, script, criteria or transitions, and no
485
- # count of the states. So `description` is what it
486
- # knows about where an edge leads. An UNDESCRIBED
487
- # edge (absent/empty/blank) routes on the TARGET's
488
- # `instructions` instead: the edge's line carries
489
- # them, quoted, and nothing else of the target.
490
- # LOAD ERRORS (states.<slug>.transitions.<i>.description):
491
- # an undescribed edge whose target has no
492
- # instructions; an undescribed transition of a
493
- # HUMAN state (it is the button label); an
494
- # undescribed edge INTO a GRADER (its instructions
495
- # are criteria the work it judges is never shown)
496
- # or a CODE state (it has none); a non-string. A
497
- # code state's OWN edges may go undescribed: its
498
- # script routes, and nothing reads them. An
499
- # undescribed edge INTO a human state discloses
500
- # that state's reviewer-facing instructions to the
501
- # agent — describe it to keep them private.
502
- #
503
- # Write it for a reader standing in THIS state,
504
- # which knows nothing about the target:
505
- # GOOD: "The refund is over $50." (when)
506
- # BAD: "Go to refund-review." (where)
507
- # A routing condition CANNOT live in the target's
508
- # summary, which is never disclosed; it lives in the
509
- # target's instructions only when the edge is left
510
- # undescribed.
511
- #
512
- # The rendered line marks a target that is a human
513
- # decision node, a grader, a code state or terminal
514
- # (each changes the call or what follows it);
515
- # nothing else of the target.
516
- # A GRADER needs at least one transition (validate
517
- # ERROR otherwise): with nowhere to send the session
518
- # it could only approve by saying nothing.
519
-
520
- # human states only (ignored on agent states; a validate ERROR on grader and
521
- # code states):
522
- evidence?: string[] # workspace-relative paths always shown in the packet —
523
- # a baseline the advancing agent adds to (see Human states)
524
- approvers?: string[] # informational; not enforced
525
- ```
32
+ ## The skeleton
526
33
 
527
- ## Parking and sessions
528
-
529
- A state parks the run **itself**: once it has asked for what it needs, its agent
530
- calls `archmax_wait({ reason })` and the run suspends right there — no transition,
531
- no model call spent parking. A delivered event resumes that same state, which
532
- reads what arrived and then picks its own outgoing transition:
34
+ Every key, in a workflow that validates clean. Start a new workflow from it and delete
35
+ what you do not need.
533
36
 
534
37
  ```yaml
535
- clarify:
536
- title: Ask a clarifying question
537
- instructions: >-
538
- Ask exactly one clarifying question and, in that same message, call
539
- archmax_wait with a reason naming what you are waiting for. When the reply
540
- arrives you continue here: read it, and if it identifies the order, advance
541
- to answer.
542
- transitions:
543
- - { to: answer, description: The reply identifies the order. }
544
- - { to: closed, description: The conversation closed before a usable reply. }
545
- ```
38
+ title: Refund desk # a label; the directory name is the workflow's slug
39
+ instructions: |- # rules for the whole session (a warning when absent)
40
+ Decide only from records you have read; never guess an order.
41
+
42
+ Write every file under scratchpad/.
43
+ runtime: { engine: archmax-harness, version: "3" } # "3" is the only contract; omit the block for it
44
+ disabled: false # true: no new turns start; parked sessions still finish
45
+ settings:
46
+ model: gpt-5-mini # the model id every state runs on; overrides ARCHMAX_MODEL
47
+ timeoutMs: 15000 # per sandbox evaluation: one archmax_run call, one code-state script
48
+ memoryLimitBytes: 67108864 # sandbox memory
49
+ maxPtcCalls: 200 # tools.* calls per evaluation
50
+ maxResultChars: 20000 # characters of an evaluation's result
51
+ prompt_cache: { enabled: true, ttl: 5m } # provider prompt caching; ttl 5m or 1h
52
+ tests: # the case suite
53
+ maxConcurrency: 1 # only 1 is accepted
54
+ caseTimeoutMs: 120000 # wall clock per case
55
+ judge: {} # required by grade: ({} = the env model, or { model, modelOptions })
56
+ metadata: { canvas: { zoom: 1 } } # host data the runtime never reads; keep it as you find it
546
57
 
547
- What to know:
548
-
549
- - `archmax_wait` is available in **every** state, terminal ones included — a run
550
- that has answered and now awaits a reply parks where it answered instead of
551
- finishing. `tools: { forbid_always: [archmax_wait] }` removes it workflow-wide.
552
- - The park is durable: status `awaiting_input` (classified **open**, never
553
- finished), a checkpointed record of the state and the reason, and the process
554
- may exit.
555
- - **Any** delivered trigger id resumes a park — the id is recorded as the run's
556
- current trigger (`args.trigger`, `${{trigger}}`), never matched against an edge.
557
- The delivered variables arrive **locked**, and the arrival is written into the
558
- transcript as an `[event]` **runtime note** — a synthetic `archmax_note` tool
559
- call and its result, which is how the runtime says something to a run in a
560
- channel a model cannot read as a person. Nothing may call `archmax_note`, and
561
- derived views (`calledTool`, `usedNoTools`, the CLI's flow) skip the pair.
562
- - The removed `type: wait` state kind is now an unknown state kind (`validate`
563
- reports it as one), and a per-transition `on:` key is ignored. Call
564
- `archmax_wait` instead.
565
- - When what the state waits for is a **time** rather than a reply, the call says so:
566
- `archmax_wait({ reason, until: "1d" })` — a duration (`30m`, `2h`, `1d`) or an
567
- ISO-8601 instant, normalized to an absolute `resumeAt` on the park record, the
568
- session summary, and the park event. The runtime **schedules nothing**: `resumeAt`
569
- tells the host's cron when to `deliver`, and an earlier event resumes the park
570
- regardless. An unparseable `until` is a tool error and the run does not park.
571
- - A state that polls ("look for the transaction, else wait a day and retry") bounds
572
- its own loop with `budget.maxParks` and sends the overrun somewhere with
573
- `on_error`; the runtime counts parks per state in the checkpoint and refuses the
574
- park that would exceed the bound as a turn failure:
575
-
576
- ```yaml
577
- check-bank:
578
- summary: Look for the transaction; wait a day and retry if it has not posted.
579
- budget: { maxParks: 5 }
580
- on_error: escalate
581
- transitions:
582
- - { to: reconcile, description: The transaction was found. }
583
- ```
584
- - A finished session re-invoked is a **new turn** — it continues in the state the
585
- previous turn ended in, *not* at the trigger's entry state, which applies only to
586
- a session's first turn. Give a state re-entry transitions if a conversation must
587
- move on from it; otherwise the agent's way out is `archmax_reset`. To continue
588
- mid-state instead, park rather than finish.
589
-
590
- A trigger's **declaration** sits under the `triggers:` of the state it enters,
591
- keyed by its id. It says which conversation a firing belongs to and what the run
592
- owes its caller:
58
+ tools:
59
+ allow_always: # the workflow-wide grant. Declared, it REPLACES the twelve
60
+ - ls # default tools, so list them all and drop only what this
61
+ - read_file # workflow must not have. A host tool every state needs
62
+ - write_file # goes here too.
63
+ - edit_file
64
+ - glob
65
+ - grep
66
+ - copy_file
67
+ - move_file
68
+ - remove_file
69
+ - write_todos
70
+ - archmax_eval
71
+ - archmax_run
72
+ forbid_always: [archmax_reset] # denied in every state, whatever grants it; "*" = every tool
73
+ skills:
74
+ allow_always: [order-data] # skill bundles enabled in every state, by slug
75
+ forbid_always: [] # skill bundles no state may reach
593
76
 
594
- ```yaml
595
77
  states:
596
- intake:
78
+ decide: # the key is the state's slug
79
+ title: Decide the refund # a label for people; no model reads it
80
+ summary: Record the decision and draft the email. # a label for editors; no model reads it
597
81
  triggers:
598
- email_received:
599
- session: conversation_id
600
- email_reply:
601
- session: triggers.-1.conversationId # newest arrival's conversation id
602
- # unknown/finished conversation → fresh run
603
- enrich:
604
- triggers:
605
- manual: # the one entry: CLI, host firing, or a caller
606
- description: Enrich one order and say whether it is late. # for callers only
607
- requires: # a firing must supply these to start
608
- - order_id # bare name = untyped
609
- - { name: due, type: date, minimum: today, description: The day the order is due. }
610
- - { name: priority, type: string, enum: [low, normal, high] } # a rule
611
- returns: # the run guarantees these when it completes
612
- - enrichment_file
613
- - { name: delayed, type: boolean }
614
- ```
615
-
616
- - There is no `entry:` key and no root block: the state a declaration sits on is
617
- the trigger's entry state, written once. `entry:` or `name:` inside a
618
- declaration fails load naming the key — each would be a second spelling of
619
- wiring the SDK already reads, and a loose declaration that swallowed one would
620
- start the run somewhere you did not say.
621
- - `requires:` / `returns:` are the run's **signature**, and both are enforced. A
622
- firing that does not supply every `requires` name does not start. `returns`
623
- is checked once as the run finishes, and **nothing is handed back**: a
624
- sub-workflow child with a name unset **completes**, and its caller gets
625
- every declared return, the unset ones `null`, plus `note: "Not all return
626
- variables were set by the sub-workflow: '<name>' was not set."`; a top-level
627
- run with a name unset is **rejected**; a typed one mistyped rejects either
628
- way. A run that *parks* is not checked — it has not finished. A return is
629
- owed **only at the finish**: `archmax_advance` checks a state's own
630
- `requires:`, never the returns, a later write replaces an unlocked earlier
631
- one, and a seed of one (a name in both lists) lands **unlocked** and does not
632
- overwrite a value the session already holds. So do not list a return in a
633
- state's `requires:` unless that state must settle it, and do not tell the
634
- agent to lock one before its value is final. `note` is reserved: a `returns`
635
- list may not declare it. Want the model to try again? Route the finishing
636
- state through a code state that checks the returns and sends the work back
637
- with what is missing. Each entry is a bare
638
- run-variable name (**untyped**: any value, `null` included) or a strict object
639
- `{ name, type?, description?, …rules }` naming one; the spellings mix in one list and
640
- names are distinct across both. What the agent must *do* to produce a
641
- variable still belongs in the `instructions` that set it. The names — with
642
- type, rules and description when declared, as
643
- `name (type; rule; rule) — description` — are
644
- rendered into the prompt under the trigger's entry state, so the agent is told
645
- what to produce before it can be failed for omitting it. A trigger's
646
- `requires:` also **guarantees** the variable for `${{…}}` guards, so `validate`
647
- stops warning about a guard bound to it. This is what makes delegation
648
- checkable — see [Sub-workflows are tools](#sub-workflows-are-tools).
649
- - **`type`** is one of `string`, `integer` (no fractional part), `number`
650
- (finite), `boolean`, `date` (RFC 3339 `YYYY-MM-DD`, a real calendar day),
651
- `date-time` (RFC 3339 with a mandatory `Z`/`±hh:mm` offset), `object` (not an
652
- array), `array` or `file` (a path from the workspace root, such as
653
- `scratchpad/attachments/invoice.pdf` or `skills/billing/assets/template.pdf`; see
654
- [Passing files](#passing-files)). Nothing is coerced (`"4"` is not an `integer`) and a typed
655
- entry never takes `null`. It is held **at the start** (a mistyped seed is
656
- refused before any model call), **at the write** (`archmax_set_variables` of a
657
- mistyped typed return of the current trigger is a correctable refusal,
658
- nothing written) and **at completion** (a mistyped typed return rejects the
659
- run — in practice a code state's write). There are no item or property
660
- schemas and no optional inputs. An unknown `type`, an unknown entry key
661
- (`default:`) or an empty `description` fails load.
662
- - **Rules** say what a typed value must look like beyond its type, spelled as
663
- JSON Schema keywords, and are held wherever the type is (start, write,
664
- completion, a delegation call and its settle):
82
+ manual: # the entry the CLI, a host and a calling workflow use
83
+ description: Decide one refund request. # for callers; never shown to this session
84
+ requires: # what a start must supply
85
+ - email # a bare name takes any value
86
+ - { name: order_id, type: string, description: The order to refund. }
87
+ - { name: contact, type: string, format: email } # a rule beside the type
88
+ returns: [decision] # what the session guarantees when it completes
89
+ email_reply: # a host trigger; its payload arrives as locked variables
90
+ session: conversation_id # a dotted path over the variables: the session it joins
91
+ message: false # host-resolved: where the message is, or false for none
92
+ connection: mail # host-resolved: an access connection
93
+ instructions: >-
94
+ Look the customer up with lookup_customer. Record the decision with
95
+ archmax_set_variables as `decision` (approved or denied), draft the email
96
+ in scratchpad/email.md, then advance to check-decision. If a note sends
97
+ you back, fix what it names and advance again.
98
+ tools:
99
+ allow: # adds to allow_always; naming a granted tool narrows it here
100
+ - lookup_customer
101
+ - { tool: write_file, paths: ["reports/${{order_id}}/*.md"] }
102
+ - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] }
103
+ - archmax_workflow_enrich-order # calls workflows/enrich-order/ as a sub-workflow
104
+ forbid: # denied while this state is active; beats every grant
105
+ - { tool: read_file, paths: ["skills/order-data/assets/archive/**"] }
106
+ skills:
107
+ allow: [refund-policy] # adds to skills.allow_always
108
+ requires: [decision] # must be set before the session may leave this state
109
+ locks: [decision] # only this state may write it
110
+ model: gpt-5 # this state's model id; beats settings.model
111
+ budget: { timeoutMs: 60000, maxParks: 3 } # per model call; archmax_wait parks here
112
+ on_error: escalate # where a failure of this state routes
113
+ transitions: # none: a terminal state
114
+ - to: check-decision
115
+ description: The decision is recorded and the email is drafted. # required; say WHEN
116
+
117
+ check-decision: # a code state: runs its script, no model call
118
+ type: code
119
+ script: scripts/check-decision.js # workflows/<slug>/scripts/check-decision.js
120
+ tools: { allow: [lookup_customer] } # what the script calls is granted HERE
121
+ on_error: escalate
122
+ transitions:
123
+ - { to: review, description: The decision matches the refund policy. }
124
+ - { to: decide, description: The decision contradicts the refund policy. }
125
+
126
+ review: # a human state: a person picks the transition
127
+ type: human
128
+ instructions: Confirm the decision before the email goes out. # required: the decision
129
+ evidence: [scratchpad/email.md] # always shown to the person
130
+ approvers: [finance@example.com] # informational; not enforced
131
+ transitions: # one per labelled type; never to itself
132
+ - { to: review-email, type: approve, description: Approve the decision. }
133
+ - { to: decide, type: refine, description: Send it back with a comment. }
134
+
135
+ review-email: # a grader state: an isolated subagent decides
136
+ type: grader
137
+ instructions: |- # required: its criteria
138
+ Read scratchpad/email.md and the `decision` variable.
139
+ - Polite, states the decision plainly and names the order: advance to `send`.
140
+ - Otherwise advance back to `decide`, saying exactly what to change. If the
141
+ trail already holds two steps with kind "grader" and to "decide", raise
142
+ `email-rejected` instead.
143
+ model: gpt-5 # resolved under the grader model role
144
+ budget: { timeoutMs: 60000 } # bounds its whole run
145
+ on_error: escalate
146
+ transitions:
147
+ - { to: send, description: The email is ready to send. }
148
+ - { to: decide, description: The email needs changes. }
665
149
 
666
- | Rule | On | Value |
667
- | --- | --- | --- |
668
- | `format` | `string` | `email` (WHATWG grammar, a domain with a TLD), `url` (absolute `http(s)://`, no credentials), `phone` (E.164, `+4930123456`) |
669
- | `pattern` | `string` | a regex found anywhere in the value (anchor with `^…$`), ≤ 1,000 characters |
670
- | `minLength`, `maxLength` | `string` | non-negative integers, counted in code points |
671
- | `enum` | `string` | a non-empty list of distinct strings, matched exactly |
672
- | `content` | `string` | `text`, `markdown` or `html`: a **declaration** for the host, never checked or sanitised |
673
- | `minimum`, `maximum` | `integer`, `number` | numbers, inclusive |
674
- | `minimum`, `maximum` | `date` | `YYYY-MM-DD` or `today` (UTC), inclusive |
675
- | `minimum`, `maximum` | `date-time` | an RFC 3339 instant or `now`, inclusive |
676
- | `minItems`, `maxItems` | `array` | non-negative integers |
677
- | `accept` | `file` | extensions like `.pdf`, matched on the last extension, case-insensitively |
678
- | `maxSize` | `file` | bytes, or `<n>KB` / `<n>MB` (1,000-based); checked where the file crosses a boundary |
679
-
680
- Nothing is coerced or trimmed; a mistyped value reports its type, not a rule;
681
- each entry reports only the first rule it breaks (`'quantity' must be at most
682
- 100`). Load fails, naming the entry and key, for a rule on a type that does not
683
- take it or on an untyped entry, `format` with `pattern`, a pattern that does not
684
- compile or uses a lookaround or backreference, and inconsistent bounds (`min…`
685
- above `max…`, an empty or repeated `enum`). A `pattern` runs in RE2 (linear
686
- time, so no input can make it slow) and must also compile as an ECMA-262 `u`
687
- regex, so RE2-only syntax like `(?i)` or `\pL` is refused. Add rules where a
688
- caller outside the session supplies the value (a form, an MCP client), or where
689
- the agent must produce a value of a set shape; the agent sees them in its prompt.
690
- - **A `file` value must name a readable file.** The pure rule checks only the
691
- path's shape (a host runs it in a browser). At the start, at the write, at
692
- completion and at a delegation call the runtime also requires a **file** (not a
693
- folder) at the path that the session may read **on runtime authority** — the
694
- runtime's own `file`-value read check, the one call that runs outside a state's
695
- surface: zone rules (`checkpoints/`, `artifacts/`, `_specs/`
696
- never), the workflow's mount grants taken across all its states (a governed
697
- mount no state is granted is out of reach), and every workflow-wide
698
- `forbid_always`, inherited ones included; the state's allow list and its own
699
- mount/skill grants do not narrow it. So tell the agent to
700
- write the file *before* it sets a `file` return: `archmax_set_variables` refuses
701
- a path with nothing there. A host hands a file in with `TurnInput.files` (CLI:
702
- `archmax run … --file invoice=./invoice.pdf`), which places it under
703
- `scratchpad/attachments/` and seeds the path.
704
- - **When to type.** Type an entry when something outside the session reads the
705
- contract: a delegating model that must build the argument, a host publishing an
706
- MCP tool or a start form. Leave it bare when any value will do. Typing a
707
- `returns` entry also tells the agent the shape to write.
708
- - **`description` on the declaration** says, for a **caller**, what calling this
709
- entry does. It leads the delegation tool's description and a host's MCP tool
710
- description; it never reaches the model of the session it starts (that
711
- session's brief is its `instructions`). An entry's own `description` is
712
- different: it *is* shown to the agent, beside the name.
713
- - `session:` is a **dotted path over the run's variables** — the same syntax
714
- `${{…}}` guards use, including array indices and negative indices
715
- (`-1` = last). No script, no callback: name where the host already puts the
716
- conversation id. A `${{…}}`-wrapped value is rejected — write the bare path.
717
- - Related triggers that must land in the same conversation declare the **same
718
- path**. Each declares it itself: there is no shared lookup to change from
719
- elsewhere in the document, and the duplication is the deliberate cost of a
720
- state being legible on its own.
721
- - A trigger with no `session:` path — or a firing whose path does not resolve —
722
- gives that run a session of its own (its session id). Every run has exactly one
723
- session, so there is no absent case.
724
- - `message:` and `connection:` are **host-resolved** keys: shape-checked,
725
- preserved and readable back (`messagePathForTrigger`, `connectionForTrigger`),
726
- but nothing in the runtime acts on them. `message:` takes `session:`'s dotted
727
- path — where a firing's words are — or the literal `false` for a trigger whose
728
- firings carry no message; `connection:` is an opaque non-empty slug naming an
729
- access connection in the host's own configuration. Author them only if the host
730
- you are targeting reads them.
731
- - A declaration key the SDK does not define is a **warning**, not a load failure:
732
- it is ignored, and the workflow still runs. Structural problems — a
733
- non-mapping `triggers:`, a declaration that is neither a mapping nor empty, one
734
- id declared by two states, a malformed `session`/`message` path, an empty
735
- `connection`, an `entry:` or `name:` key — still fail load.
736
- - A host's own keys ride **inside the declaration**, keyed by the trigger id:
737
-
738
- ```yaml
739
- states:
740
- intake:
741
- triggers: # one state, two event sources
742
- outlook-mail: { type: ap, piece: microsoft-outlook, event: newEmail }
743
- slack-message:
744
- type: ap
745
- piece: slack
746
- event: newMessage
747
- session: triggers.-1.threadId
748
- connection: acme-slack
749
- ```
750
-
751
- One state, one behavior, one governance block; `${{trigger}}` and a code
752
- state's `trigger` input still read which one actually fired.
753
- - Every declared trigger **starts** a session. An event that must only ever reach
754
- a live one — a ticket closing, a payment settling — is declared nowhere: the
755
- host names the session it delivers into (`archmax deliver <session> --trigger
756
- <id>`, `agent.workflow.send(sessionId, …)`), or passes a `sessionPath` with the
757
- firing. A firing naming a trigger no state declares, with no session to reach,
758
- is refused.
759
-
760
- Call `archmax_wait` when an **external event** is what lets the work continue (an
761
- email reply, a webhook, a scheduled reminder the host fires); author a `human`
762
- state when a **person** picks the edge. Both park the session durably; they differ
763
- in the resolution channel and in where the run continues — a decision advances
764
- along the chosen edge, a delivery resumes the parked state.
765
-
766
- **Runtime control tools** carry the `archmax_` prefix — `archmax_advance`,
767
- `archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_run`,
768
- `archmax_get_variables`, `archmax_set_variables`, and `archmax_get_history`
769
- (grader states only) — and are the names to write in `tools.allow`,
770
- `tools.allow_always`, and the `forbid`/`forbid_always` lists. These are the only spellings:
771
- an entry naming an unprefixed variant governs a tool that does not exist,
772
- silently granting nothing. A host tool may never claim the prefix. Deep Agents' built-ins
773
- (`read_file`, `task`, …) keep their own names.
774
-
775
- - A state with **no `tools` block** gets the **workflow-wide grant only**: what
776
- `tools.allow_always` lists, or the default tools (`ls`, `read_file`, `write_file`,
777
- `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
778
- `archmax_eval`, `archmax_run`) when the key is absent, plus the always-allowed
779
- `archmax_advance` and a host's `essentialTools`, minus whatever a `forbid` list
780
- denies there, and read-only-zone protection. A grader or code state never has
781
- `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or `archmax_reset`,
782
- even when `allow_always` lists one; a grader also has `archmax_get_history`.
783
- Governance is closed by default; there is no way to declare a fully open state.
784
- - `tools.forbid` on a state **denies** while that state is active, beating every
785
- grant that would otherwise reach it (see [Tool governance](#tool-governance)).
786
- - A state with **no transitions** is **terminal**: the run ends when the agent
787
- finishes its turn there; it cannot call `archmax_advance`.
788
-
789
- ## Ending a run as a failure: `archmax_raise`
790
-
791
- When the work **cannot be completed** — a system it depends on keeps failing, the
792
- thing asked about does not exist — the agent calls
793
- `archmax_raise({ code, reason })`. It is in every state (terminal ones included),
794
- never declared, and only for failure: a session that ends without it is a
795
- success.
796
-
797
- - The call must be the **only** tool call in its message; one sent beside other
798
- calls is refused, and the model may raise again on its own.
799
- - The session ends **at once** with status `failed` (classified **finished**): no
800
- further model call, no `on_error` route, no further state (an automated one
801
- included), no check of the trigger's `returns`. A rejection still
802
- pending from earlier in the turn is replaced. The position stays where the
803
- agent raised; the trail records no step.
804
- - The host reads `Outcome.kind === "failed"` and `Outcome.exit`
805
- (`{ success: false, code, reason }`); a completed turn carries
806
- `exit: { success: true }`. `code` is the agent's own free-form token (≤ 64
807
- characters, one line), so if hosts branch on particular codes, name them in
808
- the state's `instructions` ("if the order does not exist, raise
809
- `order-not-found`").
810
- - A **child** session that raises fails its caller's delegation call with kind
811
- `raised`, naming the code and reason; the caller may recover, route through
812
- its own `on_error`, or raise in turn.
813
- - Ban it with `tools: { forbid_always: [archmax_raise] }` (or one state's
814
- `tools.forbid`) where the agent must not end the run itself — say, a state that
815
- must always reach a person instead. An agent's script cannot call it; a code
816
- state's script (`tools.archmax_raise`) and a grader can, and their raise ends the
817
- session the same way (the note and `exit` carry their code).
818
- - Test it with the structural `raised` assertion; `succeeded: true` fails on a
819
- raise (it passes only on a `completed` outcome).
820
-
821
- ## Transitions
150
+ send: # terminal: no transitions
151
+ tools: { allow: [send_email] }
152
+ instructions: Send scratchpad/email.md to `email` with send_email.
822
153
 
823
- ```yaml
824
- transitions:
825
- - to: string # REQUIRED target state SLUG (must exist); never a title
826
- description?: string # shown to the agent (or human) to pick the edge; absent =
827
- # the agent is shown the target's instructions instead.
828
- # REQUIRED on a human state's edges and on an edge INTO
829
- # a grader or code state; a code state's own may omit it
830
- type?: "approve" | "reject" | "refine" | "none" # default "none"
154
+ escalate:
155
+ instructions: Tell the customer that a person will follow up.
831
156
  ```
832
157
 
833
- - On **agent** states the agent chooses one edge via
834
- `archmax_advance({ to, reason })`; `type` is metadata only.
835
- - On **grader** and **code** states the grader or the script chooses one the same
836
- way (`archmax_advance` / `tools.archmax_advance`), and its `reason` is what the
837
- next state reads.
838
- - On **human** states each transition is a selectable outcome (a UI renders one
839
- button per edge, labeled by `type`). `refine` conventionally loops back to a
840
- prior state with the reviewer's comment as correction. Max one transition per
841
- `type` on a human state.
842
-
843
- ## Tool governance
158
+ ## Files
844
159
 
845
- `AllowEntry` forms:
160
+ - `workflows/<slug>/workflow.yaml` is the machine: a plain YAML mapping with no
161
+ frontmatter. The directory name is the workflow's slug, kebab-case
162
+ (`^[a-z0-9]+(-[a-z0-9]+)*$`). `title` is only a label.
163
+ - `WORKFLOW.md` beside it is optional Markdown, appended to the prompt after the
164
+ graph section. Frontmatter in it is a load error. Put a diagram for human readers
165
+ inside an HTML comment: comments are stripped from the prompt.
166
+ - `scripts/` holds the code-state scripts and `tests/` the cases.
846
167
 
847
- ```yaml
848
- # 1. bare tool name (any args)
849
- - read_file
168
+ ## Root keys
850
169
 
851
- # 2. object with glob arg matchers (matches if the arg satisfies ANY glob)
852
- - tool: write_file
853
- args:
854
- file_path: ["reports/*.json"]
170
+ | Key | What it does |
171
+ | --- | --- |
172
+ | `title` | A label for people. |
173
+ | `instructions` | Rules for the whole session, rendered once at the top of the cacheable prompt prefix. Write a literal block (`\|-`): a folded block (`>-`) joins paragraphs. A warning when absent. |
174
+ | `runtime` | `{ engine: archmax-harness, version: "3" }`. `3` is the only contract and the default. `1` and `2` are refused. |
175
+ | `disabled` | `true` starts no new turns: `archmax run`, a host's turn, a follow-up message and a calling workflow's `archmax_workflow_<slug>` call are refused. `decide`, `reply` and `deliver` still work, so parked sessions finish. `archmax test` skips the suite and exits 0. Any value but absent, `null` or `false` disables, and a non-boolean is a validate error. |
176
+ | `settings.model` | The model id every state runs on, over the endpoint the host configured. It overrides `ARCHMAX_MODEL`. An id only: a mapping with `temperature` is a load error. |
177
+ | `settings.timeoutMs` | Bounds one sandbox evaluation (default 15000): an `archmax_eval` or `archmax_run` call, or a code state's whole script. `memoryLimitBytes`, `maxPtcCalls` and `maxResultChars` bound it too. |
178
+ | `settings.prompt_cache` | `{ enabled, ttl: 5m \| 1h }`: provider prompt caching, on by default. |
179
+ | `tests` | The case suite: `maxConcurrency` (only `1`), `caseTimeoutMs` (default 120000, per case) and `judge`, which `grade:` requires (`{}` is the env model; or `{ model, modelOptions: { temperature, maxTokens } }`). |
180
+ | `tools` | `allow_always` and `forbid_always`: [Tool governance](#tool-governance). |
181
+ | `skills` | `allow_always` and `forbid_always`, by slug: [Skill governance](#skill-governance). |
182
+ | `mounts` | `allow_always` and `forbid_always`, by mount name: [Mount governance](#mount-governance). |
183
+ | `metadata` | Host data, such as an editor's canvas. Never read or checked. Keep it verbatim when you rewrite a spec. |
184
+ | `states` | Required: at least one state, and at least one with a trigger. |
185
+
186
+ ## State keys
187
+
188
+ The key under `states` is the state's slug, kebab-case. It is the only token that `to`,
189
+ `on_error`, `archmax_advance`, events and `reachedState` resolve, so rename a state only
190
+ together with every reference to it.
191
+
192
+ | Key | On | What it does |
193
+ | --- | --- | --- |
194
+ | `type` | — | `agent` (the default), `human`, `grader` or `code`. |
195
+ | `title`, `summary` | all | Labels for people and editors. No model reads them, so they cannot carry routing. |
196
+ | `triggers` | all | Marks an entry state: [Triggers and signatures](#triggers-and-signatures). |
197
+ | `instructions` | agent, human, grader | An agent state's behaviour, shown while it is active. Required on a human state (the decision to present) and on a grader (its criteria). |
198
+ | `script` | code | Required on a code state and refused elsewhere: `scripts/<file>.js` or `.mjs`, no `..`, resolving to `workflows/<slug>/scripts/<file>`. |
199
+ | `tools`, `skills`, `mounts` | all | This state's `allow` and `forbid` lists. A grader's and a code state's calls are held to their own state's lists. |
200
+ | `requires` | all | Variables that must be set before the session may leave the state. |
201
+ | `locks` | agent, grader, code | Variables only this state may write. One owner per variable, and never `trigger` or `title`. |
202
+ | `model` | agent, human, grader | This state's model id; it beats `settings.model`. Naming the workflow's own id is a warning. |
203
+ | `budget` | agent, grader | `timeoutMs` and `maxParks`: [Budgets and `on_error`](#budgets-and-on_error). |
204
+ | `on_error` | all | The state a failure of this state routes to. |
205
+ | `transitions` | all | `[{ to, description?, type? }]`. A state with none is terminal. `description` may be left out only on an agent state's lone edge into another agent state. |
206
+ | `evidence`, `approvers` | human | Paths always shown in the decision record; who decides (informational). |
207
+ | `metadata` | all | Host data, never read. |
208
+
209
+ What each type needs and refuses:
210
+
211
+ | Type | Who runs it | Requires | A validate error |
212
+ | --- | --- | --- | --- |
213
+ | `agent` | the session's agent | — | — |
214
+ | `human` | a person picks the transition | `instructions`, at least one transition | a transition to itself, two transitions of one labelled `type` |
215
+ | `grader` | an isolated subagent | `instructions`, at least one transition | `evidence`, `approvers`, `budget.maxParks` |
216
+ | `code` | its script, in the sandbox | `script` | `instructions`, `model`, `budget`, `evidence`, `approvers` |
217
+
218
+ Grader and code states are **automated states**: the runtime runs them on entry, with no
219
+ call of the agent's model. [`code-states-and-scripts.md`](code-states-and-scripts.md)
220
+ says how they run, what they are offered and how they settle.
221
+
222
+ ## Transitions and what the agent sees
223
+
224
+ The agent is shown the active state and nothing else of the graph. Each model call carries
225
+ that state's transitions:
855
226
 
856
- # 3. `paths` shorthand for args.file_path (string or array)
857
- - tool: write_file
858
- paths: ["reports/*.json"]
227
+ ```
228
+ Transitions — choose one with `archmax_advance` when this state's work is done:
229
+ - to `check-decision` (code) — The decision is recorded and the email is drafted.
230
+ - to `review` (human decision node) — The decision needs a person's approval.
231
+ - to `review-email` (grader) — The email is drafted.
232
+ - to `escalate` (terminal) — The order does not exist.
859
233
  ```
860
234
 
861
- An object entry may also carry `connection` (the connection slug a
862
- platform-provided tool resolves through when its collection has several) and
863
- `source` (any string: a host's own label for where the tool came from) — the
864
- runtime matches governance by `tool` name alone and never reads `source`.
865
-
866
- **The model is closed by default, and a denial beats every grant.** Every state has the
867
- **workflow-wide grant**: what `tools.allow_always` lists, or, when the workflow does not
868
- declare that key, the **default tools** (`ls`, `read_file`, `write_file`, `edit_file`,
869
- `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`, `archmax_eval`,
870
- `archmax_run`). A declared list **replaces** the default tools, never adds to them:
871
- `allow_always: []` grants no tool workflow-wide, and a list without `write_file` leaves
872
- every state that does not name it unable to write. So a new workflow lists all twelve and
873
- removes only what it must not have. Every state also has its type's control tools and
874
- any tool the host passes as `essentialTools`, whatever the workflow declares. A grader or
875
- code state is never offered `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or
876
- `archmax_reset`, even when `allow_always` lists them, and naming one in its own `tools.allow`
877
- is a `validate` error.
878
- One built-in is **never disclosed** and grantable by nothing: `task`, Deep Agents'
879
- subagent dispatch — grading is a grader state and delegating is a sub-workflow, so naming
880
- `task` in an allow entry is a `validate` error and the kernel refuses the call.
881
- The planning scratchpad `write_todos` and the sandbox
882
- `archmax_eval` are default tools: granted wherever the workflow-wide grant includes
883
- them (everywhere, in a workflow without `allow_always`). `write_todos`'s prompt
884
- guidance appears only when some state is granted it, and code states run in the
885
- sandbox whether or not any state is granted `archmax_eval`. The interpreter is safe as a
886
- default because the `tools.*` calls its code makes are decided against the active
887
- state by the same kernel: it adds computation over what the state permits, not
888
- access. `archmax_run` — executing an authored script **file** — is a default tool on
889
- the same terms (the script is a file the state could already read); declare it
890
- per state to **narrow** which files may run there. The file operations —
891
- `copy_file({ source, destination, overwrite? })`, `move_file` (same arguments)
892
- and `remove_file({ file_path })` — are default tools on the same terms: every tool
893
- **declares its path arguments** with how the call uses each (`read`, `list`,
894
- `search`, `write`, `remove`, `execute`), and every path rule reads that
895
- declaration. `copy_file` reads `source` and writes `destination`; `move_file`
896
- **removes** `source`, so a move out of a read-only mount is refused before
897
- anything is written; a removal is refused wherever a write is. A call naming
898
- several paths is refused when any one is, naming the argument. `paths:` in an
899
- entry guards every declared path argument: a grant needs all of them to match
900
- (`{ tool: copy_file, paths: [skills/**, reports/**] }`), a denial needs one
901
- (`{ tool: "*", paths: [secrets/**] }` blocks a copy in or out). A host tool that
902
- declares its paths (`toolPaths`, or a descriptor's `paths`) is governed the same
903
- way; an argument may hold a list of paths, each one governed (and each one
904
- tested by a `paths:` guard), and any other shape is refused
905
- (`tool.path-argument`). `scratchpad/` is open in every state to each default file tool the state
906
- is granted, whatever the granting entry narrows, and to a host's `essentialTools`.
907
- A file tool the state is not granted gets nothing from it, and a host tool a state
908
- must grant stays refused where it is not granted, whatever its paths. Any other tool
909
- must be declared in the state's `tools.allow` or workflow-level
910
- `tools.allow_always`. A `tools.allow` entry naming a tool the workflow-wide grant
911
- includes **narrows** it for that state. A guard on an `allow_always` entry is
912
- enforced like any other (`{ tool: write_file, paths: [reports/**] }` binds in every
913
- state that does not name `write_file`).
914
- Denial is declared beside the grant, in the same two positions and with the same
915
- entries: `tools.forbid_always` at the root denies in every state,
916
- `tools.forbid` on a state denies while it is active — subtracting from the
917
- workflow-wide grant, the state's own `allow` and a host's `essentialTools` alike.
918
- **Deny beats allow, and no narrower level widens a denial.** A forbid entry may
919
- name the tool `*` (every tool); `*` in an allow list is a load error. A grader's
920
- calls and a code state's script are governed against **their own state** exactly
921
- as the agent is — its `allow`, its `forbid`, `forbid_always` and every ancestor's.
922
- The only call on runtime authority is the runtime's own `file`-value read check,
923
- held to the workflow's grants across all its states and to every `forbid_always`.
924
-
925
- Precedence (highest first):
926
-
927
- 0. **Skill and mount governance** — a path inside a skill bundle the active state
928
- does not enable is refused before anything below is consulted (rule
929
- `skill.not-allowed`), and so is a path under a mount the state does not have
930
- (`mount.not-allowed` / `mount.forbidden`, or `mount.read-only` for a write into
931
- one a grant narrowed to reads). Nothing widens either: not a wildcard
932
- `tools.allow` entry, not `allow_always`, not a consumer rule.
933
- 1. **Declared denials** — `tools.forbid_always` denies in every state,
934
- `tools.forbid` denies in the active state, both regardless of any allow list
935
- (and regardless of always-allowed): deny beats allow, and no narrower level
936
- widens a denial.
937
- 2. **The state type's control tools** (baked in): an agent or human state has
938
- `archmax_advance`, `archmax_reset`, `archmax_wait`, `archmax_raise` and the
939
- variable tools; a code state `archmax_advance`, `archmax_raise` and the
940
- variable tools; a grader state those plus `archmax_get_history`.
941
- 3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for a
942
- tool the workflow-wide grant includes takes precedence (**narrower wins** —
943
- the state's arg globs apply, not the broader grant); an entry for a default
944
- tool a declared `allow_always` omits grants it in this state.
945
- 4. **The workflow-wide grant** — the declared `tools.allow_always`, guards
946
- included, or the default tools, unconstrained, when the key is absent; in a
947
- grader or code state, minus `write_todos`, `archmax_eval` and `archmax_run`.
948
- 5. **The host's `essentialTools`** — always permitted when no state entry
949
- narrows them, whatever the workflow declares.
950
- 6. **Default**: everything else is blocked.
951
-
952
- **Progressive disclosure.** The model is only shown the active state's tool
953
- surface: the workflow-wide grant + a host's `essentialTools` + the state's entries +
954
- its control tools (`archmax_advance` hidden in terminal states), minus every tool
955
- denied there by name. A grader is offered, and a code state's script reaches,
956
- exactly the same derivation for its own state — anything else is absent
957
- (`undefined` on `tools`). Disclosure is
958
- name-level (arg-constrained tools stay visible; the kernel enforces args at
959
- call time) and never replaces enforcement. **Skills are disclosed the same way**:
960
- each model call names only the skills the active state enables, with their
961
- descriptions — a disabled bundle's slug never reaches the prompt, and `ls skills/`
962
- does not list it.
235
+ The marker says what taking the transition leads to: a person decides next, a grader or
236
+ code state runs at once and may send the session straight back, or the session ends. A
237
+ transition's `type`, other than `none`, is shown too. Nothing else of the target is
238
+ disclosed: not its title, summary, instructions, script or transitions.
963
239
 
964
- ## Skill governance
240
+ So a transition needs a `description`, a non-empty string. It is all the agent learns
241
+ about the transition. Write it for a reader standing in the source state:
965
242
 
966
- Govern a **capability** by its slug, not by path globs. The two levels compose
967
- exactly as tools do: `skills.allow_always` at the spec root is enabled in **every**
968
- state, and a state's `skills.allow` **adds** to it. **A skill is enabled only where
969
- a list names it** — an absent state block adds nothing, exactly as an absent
970
- `tools.allow` grants no tool — and a capability is declared **once**, at the level
971
- where the decision was made:
243
+ - Say **when** to take it: `The refund is over $50, or the customer has asked twice.`
244
+ - Never say **where** it goes: `Go to refund-review.` tells this reader nothing.
245
+ - Keep routing conditions out of the target. A target's `summary` and `instructions`
246
+ never reach the state that routes into it.
972
247
 
973
- ```yaml
974
- skills:
975
- allow_always: [order-data] # enabled in every state, named once
248
+ **A lone edge between agent states may go undescribed.** When an agent state with
249
+ `instructions` has a single transition, into another agent state, write `- to: send` and
250
+ no description, never a filler like `Proceed`. The agent is told to take it as soon as
251
+ the state's instructions are done, without asking anyone whether to move on:
976
252
 
977
- states:
978
- orders-question: {} # no block, and it still has order-data
979
- refund-review:
980
- skills:
981
- allow: [refund-policy] # this state adds one, and has both
982
- identify-case:
983
- skills:
984
- forbid: [order-data] # routes only: opts out of the grant
253
+ ```
254
+ Transition — the only one out of this state, with no condition of its own: take it with `archmax_advance` as soon as this state's instructions are done, without asking anyone whether to move on.
255
+ - to `send` (terminal)
985
256
  ```
986
257
 
987
- A state's `allow: []` is not a deny — a state's `allow` only ever adds
988
- (`validate` warns about an entry that reads like a denial). **`skills.forbid` is
989
- what subtracts**: it removes a slug from that state's enabled set, the workflow's
990
- `allow_always` included, so one state opts out of a capability every other state
991
- keeps; `skills.forbid_always` at the root puts a bundle out of reach everywhere.
992
- There is no root `skills.allow` ceiling — writing one is a load error.
993
-
994
- Why by slug, and not with `tools.allow` paths:
995
-
996
- - **One declaration covers the whole bundle.** Enabling `order-data` makes
997
- `skills/order-data/**` readable and its scripts runnable. The path-glob
998
- alternative needs an entry per tool (`read_file`, `ls`, `glob`, `grep`,
999
- `archmax_run`) and gets subtly wrong the moment a bundle grows a directory.
1000
- - **Path entries do not un-advertise.** A blocked read still leaves the skill's
1001
- name and description in the prompt of every state, so the model keeps reaching
1002
- for a capability it cannot have. A skill that is not enabled is not disclosed.
1003
- - **The two still compose** as *enabled AND allowed*: a `tools.allow` entry
1004
- narrows **within** the enabled set. `{ tool: archmax_run, paths:
1005
- ["skills/<slug>/scripts/**"] }` remains worth writing beside the list that
1006
- enables `<slug>` — the skills list puts every other capability out
1007
- of reach, while the entry confines execution to *this* capability's scripts.
1008
- Capability governance and execution governance are different questions.
1009
- - **`archmax_run` can only ever be narrowed.** A non-overridable kernel rule
1010
- (`script.skill-only`) confines it to skill bundles, so a script the agent writes
1011
- into `scratchpad/` is unrunnable, a code-state script is unrunnable, and
1012
- `{ tool: archmax_run, paths: ["**"] }` grants nothing — `validate` reports such
1013
- an entry as inert.
1014
-
1015
- - **No skill enabled means no script at all.** Since `script.skill-only` confines
1016
- `archmax_run` to bundles, a state that enables no skill — nothing on
1017
- `allow_always`, nothing on its own list — can execute nothing: the tool stays on
1018
- the surface and refuses every path.
1019
-
1020
- `archmax validate` warns when a state entry names a slug `allow_always` already
1021
- grants everywhere, when a state's `allow: []` beside such a grant subtracts
1022
- nothing, and once when a spec declares no root `skills` block at all in a workspace
1023
- that serves bundles, naming what the workflow cannot reach. Any root block silences
1024
- that last one — write `skills: { allow_always: [] }` when a workflow grants no
1025
- capability workflow-wide.
1026
-
1027
- Grader and code states are **not exempt**: their calls are governed against their
1028
- own state, so a code state that reads a bundle enables it in its own
1029
- `skills.allow`. That is the shape to reach for when a capability should *enforce*
1030
- something without being *usable* by the agent: the agent's state enables nothing,
1031
- and a code state in front of or behind it enables the bundle and runs the check. A
1032
- bundle the workflow's `skills.forbid_always` denies is refused to every state.
1033
-
1034
- **`tools.forbid` on a state denies there**, and `tools.forbid_always` at the
1035
- root denies everywhere. Both beat every grant. Migrating a workflow off the
1036
- retired `policy` block:
1037
-
1038
- - `policy.forbid_tools: [x]` → `tools.forbid_always: [x]`.
1039
- - `policy.forbid_paths: [g]` → `tools.forbid_always: [{ tool: write_file, paths:
1040
- [g] }, { tool: edit_file, paths: [g] }]` for the identical meaning (it only
1041
- ever blocked those two), or `[{ tool: "*", paths: [g] }]` to deny reads too.
1042
-
1043
- ## Mount governance
1044
-
1045
- A **mount** is a folder (or a single file) the *host* routes into the agent's
1046
- workspace: `skills/`, `AGENTS.md`, and whatever else the wiring composes
1047
- (`reference/`, `catalogs/eu/`). The host owns which backend serves a mount, its
1048
- write posture, and whether the spec governs it; the spec owns which states reach
1049
- it, and what they may do there. **Neither is inferred from the other**, so you
1050
- cannot make a folder appear by naming it — ask the host to mount it.
258
+ So the instructions are the whole condition. When moving on waits for something they do
259
+ not say (a reply, an approval), describe the edge. The description stays required on one
260
+ of several edges, on every edge of a human, grader or code state, on an edge into one of
261
+ those (say what must be ready), on an edge back into its own state, and out of a state
262
+ with no `instructions`.
263
+
264
+ At a human state the description is also the button's label, and `type` (`approve`,
265
+ `reject`, `refine`, `none`) labels the choice. Each labelled type maps to one target. A
266
+ `refine` transition carries the person's comment back as a correction. On other states
267
+ `type` is metadata the marker shows.
268
+
269
+ A session is in one state at a time: there is no fan-out. Sequence what would be
270
+ branches; concurrency belongs to sub-workflow calls.
271
+
272
+ ## Triggers and signatures
273
+
274
+ A trigger is declared under the `triggers:` of the state it enters, keyed by its id. That
275
+ state is its entry. There is no root `triggers:` block, and `entry:` or `name:` inside a
276
+ declaration is a load error. Several keys on one state make it the entry for each; where
277
+ their paths differ, branch on `${{trigger}}` (a script reads `trigger`). Each trigger id
278
+ belongs to one state.
279
+
280
+ - **`manual`** is the entry the CLI, a host's plain turn and a calling workflow use.
281
+ - **Any other id** is a host trigger. Its payload arrives as locked variables.
282
+ - **`session:`** is a dotted path over the variables that names the session the trigger
283
+ joins (`conversation_id`, or `triggers.-1.conversationId`, where `-1` is the last
284
+ element). Write the bare path, not `${{…}}`. Without one, or when it does not resolve,
285
+ the trigger starts a session of its own.
286
+ - **`message:`** (a dotted path, or `false`) and **`connection:`** (a slug) are read by
287
+ the host, not by the runtime.
288
+ - **A key the harness does not define** is kept, with a warning. A host's own keys go
289
+ there.
290
+ - **Every declared trigger starts a session.** An event that must only reach a live
291
+ session is declared nowhere: the host delivers it to that session by id.
292
+
293
+ **The signature.** `requires` and `returns` are the session's contract with its caller,
294
+ and both are enforced.
295
+
296
+ - An entry is a bare variable name, which takes any value (`null` included), or
297
+ `{ name, type?, description?, …rules }`. The two forms mix in one list.
298
+ - `type` is one of `string`, `integer`, `number`, `boolean`, `date` (`YYYY-MM-DD`),
299
+ `date-time` (RFC 3339 with an offset), `object`, `array` or `file`. Nothing is coerced
300
+ (`"4"` is not an `integer`), and a typed entry never takes `null`. A typed entry is
301
+ held at the start, at each `archmax_set_variables` of a typed return, and at
302
+ completion.
303
+ - **Rules** say what a typed value must look like beyond its type, as JSON Schema
304
+ keywords, and are held wherever the type is. Each entry reports the first rule it
305
+ breaks (`'quantity' must be at most 100`).
1051
306
 
1052
- A mount the host declares **governed** (`{ backend, governed: true }` in the
1053
- wiring) is closed by default, exactly like a skill bundle: reachable only from the
1054
- states a `mounts` list enables. An **ungoverned** mount — every mount of a
1055
- zero-config workspace, and every mount of a host that marks nothing — is visible
1056
- in every state, and only a `forbid` takes it away. So a spec with no `mounts`
1057
- block runs unchanged over any table.
307
+ | Rule | On | Value |
308
+ | --- | --- | --- |
309
+ | `format` | `string` | `email`, `url` (absolute `http(s)://`) or `phone` (E.164) |
310
+ | `pattern` | `string` | a regex found anywhere in the value (anchor it with `^…$`) |
311
+ | `minLength`, `maxLength` | `string` | counts of code points |
312
+ | `enum` | `string` | a list of distinct strings, matched exactly |
313
+ | `content` | `string` | `text`, `markdown` or `html`: for the host, never checked |
314
+ | `minimum`, `maximum` | `integer`, `number`, `date`, `date-time` | inclusive; `today` and `now` for the dates |
315
+ | `minItems`, `maxItems` | `array` | counts of items |
316
+ | `accept`, `maxSize` | `file` | extensions like `.pdf`; bytes, or `<n>KB` / `<n>MB` |
317
+
318
+ A rule on a type that does not take it or on an untyped entry, `format` with
319
+ `pattern`, a pattern with a lookaround or a backreference, and inconsistent bounds are
320
+ load errors. A `pattern` runs in RE2, so no input can make it slow.
321
+ - A start that omits a `requires` name, or supplies a mistyped one, is refused before
322
+ any model call.
323
+ - `returns` is checked when the session completes. A top-level session with a return
324
+ unset is rejected. A sub-workflow child completes anyway, and its caller gets the unset
325
+ ones as `null` plus a `note`. A mistyped return is rejected either way. A parked session
326
+ is not checked.
327
+ - A return is owed only at the finish: no state needs it to leave, and a later write
328
+ replaces an earlier unlocked one. Lock a return only once it is final. `note` is
329
+ reserved and may not be declared.
330
+ - A `file` value is a workspace path to a file the session may read. Tell the agent to
331
+ write the file before it sets the variable: `archmax_set_variables` refuses a path with
332
+ nothing there.
333
+ - The trigger's `description` is for callers: a calling model, an MCP client. The session
334
+ it starts never sees it. An entry's own `description` is shown to the agent beside the
335
+ name.
336
+ - Type an entry when something outside the session builds or reads the value. Leave it
337
+ bare when any value will do.
338
+
339
+ ## Variables
340
+
341
+ A session has one flat store of variables, `name → value`, never declared. A variable
342
+ exists once something sets it:
343
+
344
+ - **The host** seeds it: `--variables`, `createAgent({ variables })`, a turn's
345
+ `variables`, a delivery. Every seed is **locked**, so `archmax_set_variables` refuses
346
+ to change it.
347
+ - **The agent** sets it with `archmax_set_variables`; `lock: true` freezes it until
348
+ `archmax_reset`. A misspelled name creates a new variable, and a state's `requires` is
349
+ what surfaces the mistake.
350
+ - **A state owns it** with `locks: [name]`: only that state writes it, so changing it
351
+ means going back there. Give the owner a transition back from the states that use it,
352
+ and pair `locks` with `requires`.
353
+
354
+ Two names are reserved. `trigger` holds the current trigger id and is always locked.
355
+ `title` is the session's one-line task label: the agent sets it (a sub-workflow child is
356
+ not asked to), it is never locked, and a host's seed of it lands unlocked.
357
+
358
+ **References.** `${{name}}` or `${{a.b.-1.c}}` resolves against the variables in two
359
+ places:
360
+
361
+ - **In a `tools.allow` glob**, as a guard:
362
+ `{ tool: gmail__send, args: { to: ["${{from_email}}"] } }`. Lock what a guard reads,
363
+ and guarantee it: name it in an earlier state's or the trigger's `requires`, or seed
364
+ it. A guard that cannot resolve fails the session.
365
+ - **Inside any text argument the model writes**, mid-string and as often as needed. The
366
+ tool receives the value, while the transcript keeps the reference, so a stored value
367
+ costs its tokens once. Tell a state that must emit a stored value to write the
368
+ reference (`Quote the customer's message by writing ${{inbound.text}}`): a reference is
369
+ exact where a retyped copy drifts. A reference that cannot resolve is refused, and the
370
+ agent retries.
371
+
372
+ A value is matched literally, so a variable never widens a guard. A path reads own
373
+ properties and must end at a scalar. Write `$${{name}}` for the literal characters.
374
+ Scripts get no references: they read `variables` (a code state) or `args.variables`
375
+ (`archmax_run`).
1058
376
 
1059
- The two levels compose exactly as `skills` does:
377
+ ## Tool governance
1060
378
 
1061
- ```yaml
1062
- mounts:
1063
- allow_always: [reference] # reachable in every state, named once
1064
- forbid_always: [catalogs/uk] # out of reach everywhere, and in every
1065
- # child session this workflow delegates to
379
+ Governance is closed by default. **A denial beats every grant, and no narrower level
380
+ widens a denial.**
1066
381
 
1067
- states:
1068
- intake: {} # no block, and it still has reference/
1069
- triage:
1070
- mounts:
1071
- allow: [catalogs/eu] # this state adds one, and has both
1072
- route:
1073
- mounts:
1074
- forbid: [reference] # opts out of the workflow's grant
1075
- ```
382
+ **The workflow-wide grant** is `tools.allow_always`, granted in every state. When the
383
+ key is absent, it is the twelve **default tools**: `ls`, `read_file`, `write_file`,
384
+ `edit_file`, `glob`, `grep`, `copy_file`, `move_file`, `remove_file`, `write_todos`,
385
+ `archmax_eval` and `archmax_run`. When it is declared, an empty list included, it is
386
+ exactly what it lists: it *replaces* the default tools. So a new workflow lists all
387
+ twelve and drops only what it must not have.
1076
388
 
1077
- A state's `allow: []` is not a deny — a state's `allow` only ever adds
1078
- (`validate` warns about an entry that reads like a denial). **`mounts.forbid` is
1079
- what subtracts**, the workflow's `allow_always` and an ungoverned mount's standing
1080
- visibility alike. There is no root `mounts.allow` ceiling — writing one is a load
1081
- error.
389
+ **A state's `tools.allow`** adds to that grant. An entry naming a tool the workflow-wide
390
+ grant includes **narrows** it in that state; an entry naming a default tool that a
391
+ declared `allow_always` leaves out grants it there. Every other tool, a host tool or a
392
+ delegation tool, needs an entry.
1082
393
 
1083
- ### Read or read/write
394
+ **Denials** sit beside the grants. `tools.forbid_always` at the root denies in every
395
+ state, and in every sub-workflow the session calls. A state's `tools.forbid` denies
396
+ while it is active. Both beat the workflow-wide grant, the state's `allow` and a host's
397
+ `essentialTools`.
1084
398
 
1085
- A grant entry is either a bare mount name, which asks for no write, or a mapping
1086
- naming the access this level gives:
399
+ **Each state type's own tools** come from one table, `STATE_TYPE_TOOLS` on
400
+ `@archmax-ai/harness/spec`. A type's controls need no grant, though a `forbid` can still
401
+ block one. What a type withholds, no grant can give.
1087
402
 
1088
- ```yaml
1089
- mounts:
1090
- allow_always:
1091
- - reference # read; written only if a grant below asks
1092
- - { mount: shared, access: read_write } # writable where the host allows it
403
+ | State type | Controls it always has | Never offered |
404
+ | --- | --- | --- |
405
+ | `agent`, `human` | `archmax_advance`, `archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_get_variables`, `archmax_set_variables` | — |
406
+ | `grader`, `code` | `archmax_advance`, `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`, `archmax_get_history` | `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait`, `archmax_reset` |
1093
407
 
1094
- states:
1095
- review:
1096
- mounts:
1097
- allow: [{ mount: shared, access: read }] # reads it, cannot write it here
1098
- ```
408
+ - `archmax_advance` is absent in a terminal state.
409
+ - A withheld tool listed in `allow_always` is left out of a grader or code state. One
410
+ named in that state's own `tools.allow` is a validate error.
411
+ - Deep Agents' `task` is callable in no state, and naming it in any allow list is a
412
+ validate error: grading is a grader state, and delegating is a sub-workflow.
413
+ - A host's `essentialTools` are on in every state.
1099
414
 
1100
- **A workflow writes a governed mount only where its own grants ask to.** It is
1101
- writable in a state when the host serves it writable, some grant of *this*
1102
- workflow says `access: read_write` (in `allow_always` or any state's `allow`), and
1103
- no grant applicable in the state says `access: read`. So a bare grant in one state
1104
- and a `read_write` grant in another both write. One mount table serves a session
1105
- and every child it dispatches, and the host serves a route writable when any
1106
- workflow in that tree asks; a mount still belongs to the workflow that grants it,
1107
- so a caller's `read_write` never lets its child write through a bare grant, nor the
1108
- other way round. An ungoverned mount takes the host's posture: it has no grant to
1109
- ask with.
1110
-
1111
- The host's `readOnly` is a **ceiling**, not a default to argue with:
1112
-
1113
- - `access: read_write` on a mount the workspace serves read-only opens nothing,
1114
- and `validate` warns that the entry is inert. Ask the host to mount it
1115
- `{ readOnly: false }` instead.
1116
- - `access: read` is a restriction, so it behaves like every other restriction
1117
- here: whichever level asks for it gets it, and **no narrower level widens it
1118
- back**. A state cannot re-open what the workflow narrowed.
1119
- - A `forbid` entry is a name only. A denial is total, so there is no access to
1120
- qualify — `{ mount: x, access: read }` in a `forbid` list is a load error.
1121
-
1122
- Two refusals, because they are two different edits: a write into a mount the
1123
- **host** serves read-only is `zone.read-only` (fix the wiring), and a write into
1124
- one a **grant** narrowed, or one no grant of the workflow asks to write, is
1125
- `mount.read-only` (fix the spec; the refusal says which).
1126
-
1127
- ### What the agent sees
1128
-
1129
- A governed mount the state does not have is not merely refused — it is not shown:
1130
-
1131
- - `ls`, `glob` and `grep` results lose every entry under it, the **workspace root
1132
- listing included**, so the agent is never shown a directory it then cannot read.
1133
- - The static "Workspace zones" prompt section names ungoverned mounts only, one
1134
- line each with a `read-only` or `read/write` marker. The
1135
- governed mounts of *this* state are listed in the volatile "Current state"
1136
- block, read-only or read/write per the state's own grant — which is why the
1137
- cacheable prompt prefix stays byte-identical across states.
1138
- - Every call is still decided by the kernel (`mount.not-allowed`,
1139
- `mount.forbidden`, `mount.read-only`), whether or not a listing showed the path.
1140
- Redaction is the courtesy; the kernel is the enforcement.
1141
-
1142
- ### Nested and file mounts
1143
-
1144
- A mount key may be several segments deep (`catalogs/eu`), and a mount name is that
1145
- key with its slashes stripped — that is the token a `mounts` list takes, never a
1146
- glob (`reference/**` is a load error; scope paths *within* an enabled mount with a
1147
- `tools.allow` entry). A path is matched to a mount by **longest prefix**, so
1148
- `catalogs/eu` wins over a `catalogs` mounted beside it, and a sibling
1149
- `catalogs/uk` matches neither. A file mount (`AGENTS.md`) matches its exact path,
1150
- so `AGENTS.md.bak` is not captured.
1151
-
1152
- ### Diagnostics
1153
-
1154
- `archmax validate` reports, given the host's mount table:
1155
-
1156
- - **error** — a name the table does not mount at all, naming the mounts it does;
1157
- - a grant naming an **ungoverned** mount (it grants nothing: the mount is visible
1158
- everywhere already — use `forbid` to take one away);
1159
- - an `access: read_write` on a mount the host serves **read-only**;
1160
- - a name in both `allow_always` and a state's `allow` (unless the state entry
1161
- narrows the access to `read`, or asks `read_write` of a mount the root grants
1162
- bare: both do real work);
1163
- - a state's `allow: []` beside a non-empty `allow_always`;
1164
- - a `forbid` naming a governed mount nothing would have enabled there;
1165
- - a `forbid_always` name some `allow` list also grants;
1166
- - a `tools.allow` path entry that can only match inside a mount the state does not
1167
- enable (the grant reaches nothing);
1168
- - **once**, when a spec declares no root `mounts` block at all in a workspace that
1169
- governs mounts, naming what the workflow cannot reach. Any root block silences
1170
- it — write `mounts: { allow_always: [] }` when a workflow reaches none.
1171
-
1172
- Grader and code states are bound by **their own state's** mount grants, exactly
1173
- like the agent: a code state reads a governed mount only where `allow_always` or
1174
- its own `mounts.allow` grants it, and writes one only where that grant asks
1175
- `access: read_write`. So when a check needs a mount no agent state should see,
1176
- grant it on the check's code state alone. The one read on runtime authority is
1177
- the runtime's own `file`-value check, held to the **workflow's** grants taken
1178
- across every state and to every `forbid_always`.
1179
-
1180
- ## Session variables in guards
1181
-
1182
- A `tools.allow` argument glob may reference a **run variable** — `${{name}}` or
1183
- `${{name.dotted.path}}` — resolved against the run's variables at call time:
415
+ **Entries.**
1184
416
 
1185
417
  ```yaml
1186
- tools:
1187
- allow:
1188
- - { tool: gmail__send, args: { to: ["${{from_email}}"] } }
1189
- - { tool: crm__lookup, args: { region: ["${{account.region}}"] } }
1190
- - { tool: write_file, paths: ["reports/${{case_id}}/**"] }
1191
- ```
1192
-
1193
- There is **no `variables:` block** — a variable exists once something sets it:
1194
- the host at assembly (`variables`, every seed locked), the agent mid-run
1195
- (`archmax_set_variables`), or one of the two built-ins.
1196
-
1197
- **Two reserved names.** Do not claim either for something else:
1198
-
1199
- | | `trigger` | `title` |
1200
- |---|---|---|
1201
- | Set by | the runtime, at every arrival | the **agent**, prompted by the platform prompt (a sub-workflow child is not) |
1202
- | Locked | always | **never**, by any route |
1203
- | Guaranteed set | yes | no — absent until written |
1204
- | In a trigger's `requires:` | accepted | accepted (seeded unlocked) |
1205
- | In a trigger's `returns:` | refused | refused |
1206
-
1207
- `trigger` holds the current turn's trigger id. `title` holds a short one-line label
1208
- naming the task the run is doing; the platform system prompt asks the agent to set
1209
- one as its first activity, and to rename it only when the whole conversation has
1210
- moved to a different subject (a follow-up on the same subject keeps it), so a
1211
- host can name a run instead of showing a session id. A sub-workflow's child is not
1212
- asked — its title names a session nothing lists and is never returned — so its
1213
- prompt leaves the step out; do not write "set the title" into a child's state
1214
- `instructions` to bring it back. A write must be a non-empty
1215
- single-line string of at most 200 characters (stored trimmed) and must not pass
1216
- `lock: true` — both are refused, atomically, like every other `set_variables`
1217
- rejection. A host may seed a `title`; it lands **unlocked** (the one exception to
1218
- "every seed is locked") as an opening label, and it will not overwrite one an
1219
- earlier turn established.
1220
-
1221
- Do not confuse it with the spec-root and per-state `title:` keys, which are static
1222
- human labels for the *workflow* and the *state*. The run variable names the **run**.
1223
-
1224
- Because nothing guarantees `title` is set, a guard on `${{title}}` still draws the
1225
- assembly-time warning — correctly.
1226
-
1227
- Two declarations *guarantee* one: a state's `requires:` (checked when leaving that
1228
- state) and a **trigger's** `requires:` (checked before the run starts, so a firing
1229
- that omits it never runs). A trigger's `returns:` is the mirror — the variables a
1230
- run guarantees it has set by the time it completes. See a trigger's declaration.
1231
-
1232
- A value belongs to the **session**: what one state records is readable by every
1233
- state after it and by every later turn, so a downstream guard can bind to an
1234
- upstream fact. Design for that — `requires` in an early state plus `lock: true` is
1235
- how a later `${{name}}` becomes guaranteed rather than hopeful.
1236
-
1237
- **Paths.** Segments descend one level; a numeric segment indexes an array and a
1238
- negative one counts from the end (`${{order.items.-1.sku}}` is the last item).
1239
- Traversal reads own properties only, so `${{tags.length}}` and
1240
- `${{v.constructor}}` do **not** resolve. A reference must resolve to a single
1241
- scalar.
1242
-
1243
- **Authoring rules that matter:**
1244
-
1245
- - **Lock anything a guard reads.** Host seeds are locked automatically (`title`
1246
- and a seed of a trigger's `returns` excepted, which land unlocked); for an
1247
- agent-established fact pass `lock: true` — on a return, only once it is final. An unlocked variable can be
1248
- overwritten by any later state — that rewrite is permitted, being one writer
1249
- twice, and the later value simply wins — so a guard bound to an unlocked name is
1250
- a weak guard. A lock is the only thing that makes a fact settled.
1251
- - **Guarantee the reference.** Name it in the `requires` of a state that runs
1252
- first, or seed it. Otherwise assembly only *warns*, and the run **fails** at
1253
- the guarded call if the agent never set it — an unresolvable reference is
1254
- terminal, not a retryable block, because a guard the runtime cannot evaluate
1255
- is not a guard.
1256
- - **A positional index makes the guard move.** `${{order.items.-1.sku}}` permits
1257
- a different value as the array grows. Sometimes exactly right ("the newest
1258
- attachment"); when you want a *stable* guard, store the value you mean as its
1259
- own named variable.
1260
- - **An `archmax_set_variables` typo creates a new variable** rather than erroring — there
1261
- are no declarations to check against. `requires` is the safety net that turns
1262
- that into a visible failure.
1263
-
1264
- Substituted values are matched **literally** (a value holding `*` matches `*`,
1265
- not everything), so a variable can never widen the guard it lands in.
1266
-
1267
- ### The same references work in the agent's own tool arguments
1268
-
1269
- `${{…}}` is not only an authoring syntax. Anywhere a tool argument is text, the
1270
- runtime substitutes references from the run's variables **before** the call is
1271
- governed or executed — and a reference may sit *inside* a longer string, and
1272
- repeat:
1273
-
418
+ - read_file # a bare name: any arguments
419
+ - { tool: write_file, args: { file_path: ["reports/*.json"] } } # matches when any glob matches
420
+ - { tool: copy_file, paths: ["skills/**", "reports/**"] } # every path argument must match
421
+ - { tool: "*", paths: ["secrets/**"] } # forbid lists only: every tool
1274
422
  ```
1275
- "Thanks — your refund is on its way.
1276
423
 
1277
- --- Original message ---
424
+ - A path argument (a tool's declared paths, else `file_path`) is canonicalized before it
425
+ is matched. Any other argument, such as a URL or an address, is matched as written.
426
+ - `*` never crosses `/`; `**` does. Globs match dot-files, so `secrets/**` covers
427
+ `secrets/.env`. `""` matches only an empty value.
428
+ - In an allow entry, `paths:` needs every path argument to match. In a forbid entry, one
429
+ match denies.
430
+ - `"*"` as the tool is legal only in a forbid list.
431
+ - An entry may also carry `connection` (the connection a platform tool resolves through)
432
+ and `source` (a host's label). The runtime matches by `tool` alone.
433
+
434
+ **Path arguments.** Each tool declares how it uses each path argument: `read`, `list`,
435
+ `search`, `write`, `remove` or `execute`. `copy_file` and `move_file` take
436
+ `{ source, destination, overwrite? }`, and `move_file` removes its source. `remove_file`
437
+ takes `{ file_path }`. A removal is refused wherever a write is, a call naming several
438
+ paths is refused when any one is, and an existing destination is refused without
439
+ `overwrite: true`.
440
+
441
+ **The scratchpad.** `scratchpad/` is open in every state to each default file tool the
442
+ state is granted, whatever the granting entry narrows. So an allow entry inside
443
+ `scratchpad/` narrows nothing, and validate warns. To govern *where* a result lands,
444
+ allow a path outside the reserved zones, such as `reports/*.json`.
445
+
446
+ **The order of decision.** The first rule that applies wins:
447
+
448
+ 1. Skill and mount governance. A path in a skill bundle or a governed mount the state
449
+ does not have is refused first (`skill.not-allowed`, `mount.not-allowed`,
450
+ `mount.forbidden`, `mount.read-only`).
451
+ 2. Declared denials: `forbid_always`, then the state's `forbid`.
452
+ 3. The state type's controls.
453
+ 4. The state's `tools.allow`. A narrowing entry wins over the broader grant.
454
+ 5. The workflow-wide grant, its guards included.
455
+ 6. The host's `essentialTools`, where no state entry narrows them.
456
+ 7. Everything else is refused.
457
+
458
+ The model is shown only the active state's tools, by name. The kernel still checks every
459
+ argument at call time.
1278
460
 
1279
- ${{inbound.text}}"
1280
- ```
461
+ ## Skill governance
1281
462
 
1282
- So the agent quotes a stored value by naming it rather than retyping it. Three
1283
- consequences worth authoring around:
1284
-
1285
- - **A reference is exact.** A copy the model retypes can truncate, paraphrase, or
1286
- drift; a reference cannot. When a state's job is to emit a large stored value,
1287
- its `instructions` should **name the variable to interpolate** rather than
1288
- telling the agent to include the original text.
1289
- - **The guard and the argument meet on the value.** Substitution runs ahead of the
1290
- kernel, so `args: { to: ["${{from_email}}"] }` and an argument of
1291
- `"${{from_email}}"` resolve independently to the same string — they agree by
1292
- construction, not by the model having retyped an address correctly.
1293
- - **A bad reference here is recoverable**, unlike a bad guard. The call is refused
1294
- with the reference named and the agent retries; only an unresolvable *guard* is
1295
- terminal, because that is a defect the agent cannot fix.
1296
-
1297
- The transcript keeps the reference — the substituted value goes to the tool and to
1298
- the `tool-called` event a run trail renders, and no further, so the value's tokens
1299
- are not re-sent on every later model call. An cases `calledTool` assertion
1300
- therefore states the **reference** the agent wrote; pin the delivered text through
1301
- the tool's effect instead (the file it wrote, the variable it set).
1302
-
1303
- To write the characters `${{…}}` literally — authoring a guard through
1304
- `write_file`, or writing about the syntax — double the dollar: `$${{name}}`.
1305
-
1306
- This is **model-facing**: the agent's calls and a grader's get it. An
1307
- `archmax_run` script (`args.variables`) or a code state's script (`variables`)
1308
- already holds the run's variables and composes strings in JavaScript, so `${{…}}`
1309
- in a script's `tools.*` call is ordinary text.
1310
-
1311
- ## Grader and code states
1312
-
1313
- Two state types are run by the runtime itself, together called **automated
1314
- states**. Both are ordinary states on the graph: entered by a transition, leaving
1315
- by `archmax_advance` along one of their own transitions, ending the session with
1316
- `archmax_raise`, governed by their own tool surface.
463
+ A skill bundle is governed by its slug, never by path. The lists compose like the tool
464
+ lists:
1317
465
 
1318
466
  ```yaml
467
+ skills:
468
+ allow_always: [order-data] # enabled in every state; name it on no state
1319
469
  states:
1320
- check-refund: # CODE state: runs its script, makes no model call
1321
- type: code
1322
- script: scripts/check-refund.js
1323
- skills: { allow: [order-data] } # what the SCRIPT reads is granted HERE
1324
- on_error: escalation
1325
- transitions:
1326
- - to: refund-review
1327
- description: The recorded decision matches the refund policy.
1328
- - to: refund-request
1329
- description: The recorded decision contradicts the policy; send it back with the fix.
1330
-
1331
- review-enrichment: # GRADER state: an isolated grader decides the edge
1332
- type: grader
1333
- budget: { maxTurns: 8 } # the grader's own model calls
1334
- on_error: escalation
1335
- skills: { allow: [] } # the GRADER is governed HERE too: no bundle, and
1336
- # read_file (workflow-wide) reads the returned files
1337
- instructions: |- # REQUIRED — its criteria
1338
- Read `orders_to_enrich` and `enrichments` with archmax_get_variables, then
1339
- each entry's `enrichment_file` with read_file.
1340
- - One entry per requested `order_id`, none missing or extra, each `delayed`
1341
- matching its file: advance to `enrichment-reported`.
1342
- - Otherwise advance back to `enrich-order`, naming what to fix. Before
1343
- that, count the steps in archmax_get_history's trail with kind "grader"
1344
- and to "enrich-order"; if there are already two, raise
1345
- `enrichment-incorrect` instead.
1346
- transitions: # REQUIRED — at least one
1347
- - to: enrichment-reported
1348
- description: Every requested order has exactly one enrichment, and each matches its file.
1349
- - to: enrich-order
1350
- description: The enrichments are incomplete or do not match their files; send them back.
470
+ answer: {} # has order-data
471
+ refund:
472
+ skills: { allow: [refund-policy] } # adds one, and has both
473
+ route:
474
+ skills: { forbid: [order-data] } # opts out of the workflow-wide grant
1351
475
  ```
1352
476
 
1353
- - **When it runs.** Whenever the session's position reaches it — the agent's
1354
- `archmax_advance`, another automated state's advance, a human decision, an
1355
- `on_error` route, a turn that opens there, an `archmax_reset` back to it — the
1356
- runtime runs it **in place of the next model call** of the session's agent. The
1357
- agent's model is not called while the session stands there (and an automated
1358
- state spends none of the agent's `maxTurns`). Consecutive automated states run
1359
- one after another, each committed as its own checkpointed step, so **a chain of
1360
- code states makes no model call at all**. A chain that never reaches an agent
1361
- state, a human state or a completion is stopped by the turn's recursion limit
1362
- (`GraphRecursionError`: the session ends `failed` with `recursion-limit`, not an
1363
- `on_error` route).
1364
- - **What it is offered** — exactly its own state's surface: its `tools.allow`, the
1365
- workflow-wide grant (`allow_always`, or the default tools when it is absent), the
1366
- host's essential tools, and its controls — one set for both types:
1367
- `archmax_advance` (absent when terminal), `archmax_raise`,
1368
- `archmax_get_variables`, `archmax_set_variables`, `archmax_get_history` (which
1369
- no agent or human state is offered). Never `write_todos`, `archmax_eval`,
1370
- `archmax_run`, `archmax_wait` or `archmax_reset` — not even when `allow_always`
1371
- lists one (it is left out), and naming one in its own `tools.allow` is a
1372
- `validate` error. An automated state **cannot park**. Every call is decided by
1373
- the kernel against that state, like the agent's: its `forbid`, `forbid_always`,
1374
- its skill and mount grants and every safety rule bind it, and a tool another
1375
- state grants is not reachable.
1376
- - **How it moves.** `archmax_advance({ to, reason })` is checked at the call (the
1377
- edge exists, the state's `requires` hold against the variables at that moment;
1378
- `evidence` only into a human state); a refusal can be retried. The **first
1379
- accepted** advance or raise is the decision and every later one is refused. The
1380
- decision takes effect when the run finishes **cleanly**.
1381
- - **How it settles** — exactly one of:
1382
- - **advanced** → the session moves; the trail records `{ to, kind: grader | code,
1383
- reason }`; a runtime note carries the reason to the next state
1384
- (`[grader] The '<state>' grader moved the run to '<to>': <reason>. …` /
1385
- `[code] '<state>' moved the run to '<to>': <reason>. …`); a human target parks
1386
- after its one reply-only handoff message.
1387
- - **raised** → the session ends `failed`, its `exit` carrying the code and reason.
1388
- - **completed** → a terminal state finished without either call; the session
1389
- completes, held to the trigger's `returns`.
1390
- - **failed** → it threw, timed out, exhausted its budget, or a **non-terminal**
1391
- state finished without either call (`<type> state '<slug>' finished without
1392
- calling archmax_advance or archmax_raise`) — routed through `on_error`, else the
1393
- session ends rejected. A raise wins over an error; an error discards a recorded
1394
- advance. Finishing undecided is never read as approval.
1395
- - **The history.** Both types read the session with `archmax_get_history`, which
1396
- answers `{ messages, trail }` (`SessionHistory`) as JSON: the session as it
1397
- stood when the state was entered, built from the checkpointed session, whole
1398
- and oldest first, nothing windowed. A message is `{ role, text, toolCalls?,
1399
- tool?, id?, note? }`: an `assistant` entry lists its calls (`{ id?, name, args
1400
- }`), the `tool` entry with the matching `id` carries the result inline in
1401
- `text`, and a runtime note is `role: "runtime"` with its `note` kind.
1402
- - **Code state.** `script` (required, `scripts/<file>.js|.mjs`) runs in the QuickJS
1403
- sandbox in a fresh REPL per run. Its default export receives `{ state, trigger?,
1404
- variables, tools }` and reads the history with
1405
- `JSON.parse(await tools.archmax_get_history())`. Its return value is never
1406
- read. `instructions`, `model`, `budget`, `evidence` and `approvers` on a code
1407
- state are `validate` errors; `settings.timeoutMs` bounds the script. A
1408
- **terminal** code state completes the session when its script returns: approve
1409
- into one to end without another model call. Script contract and worked
1410
- examples: [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).
1411
- - **Grader state.** A subagent with its own isolated context, built per run: its
1412
- system prompt is a compiled-in grader preamble followed by the state's
1413
- `instructions`; its task names the state and lists its transitions; it reads the
1414
- session only through `archmax_get_history`, and the work with its own state's
1415
- tools. Its messages never enter the session's transcript, its tokens never
1416
- stream, it writes no checkpoint; the session reads its decision as the trail
1417
- step and the note. `instructions` (non-empty) and at least one transition are
1418
- required; `evidence`, `approvers` and `budget.maxParks` are errors. Every edge
1419
- into a grader needs a `description` (its instructions are never shown to the
1420
- work it judges). `budget.maxTurns` bounds its own model calls and
1421
- `budget.timeoutMs` its run. `model` (else `settings.model`) is resolved under the
1422
- **`grader`** model role.
1423
- - **No iteration budget.** Nothing caps how often a state is entered. A
1424
- state that gives up after N rounds counts them in the trail — each earlier
1425
- send-back is `{ kind: grader | code, to: <graded state> }` — and raises (or routes
1426
- to an escalation) on the round after.
1427
- - **Events.** `code-start { state, script }`, `code-output { state, line }`,
1428
- `code-result { state, status, durationMs, reason? }`, `grader-start { state }`,
1429
- `grader-result { state, status, durationMs, reason? }`; a grader's model calls are
1430
- `model-usage` events under its state, added to the session's usage.
1431
-
1432
- **Patterns.**
1433
-
1434
- | Want | Shape |
1435
- | --- | --- |
1436
- | A deterministic check **in front of** a state | Make the trigger's entry (or the edge's target) a code state that advances to the work, or to a state that declines it (its agent reads the reason from the note) — or raises |
1437
- | A check **behind** a state | Point the state's transition at a code state with an edge onward and an edge **back**; a send-back's `reason` is the correction the agent reads |
1438
- | **Approve, then end** | Approve into a terminal code state (records the result, completes the session, no model call) |
1439
- | Judgment (tone, completeness) | A grader behind the graded state, with an edge back; its `instructions` say when to raise, counting send-backs in the trail. Grade work the agent hands on (a report, a draft), not a live answer: a turn's `reply` is every text the agent said in it, a draft sent back included |
1440
-
1441
- **Migrating hooks.** `before:`/`after:` are removed, with every hook kind
1442
- (`{ script }`, `{ rubric }`, custom kinds), `max_iterations` and the root
1443
- `extensions:` block; each is a load error naming the key. A `before` check becomes
1444
- a code state in front of the state; an `after` check a code state behind it; a
1445
- rubric a grader state behind it. Move `workflows/<slug>/hooks/*.js` to
1446
- `scripts/` and rewrite each to the code-state contract (it advances or raises
1447
- instead of returning `ok`/`veto`/`correct`), and grant on the code state whatever
1448
- the script reads — there is no runtime authority any more.
1449
-
1450
- ## Budgets & error routing
477
+ - Enabling a slug makes `skills/<slug>/**` readable and its scripts runnable, with no
478
+ `tools.allow` entry. A bundle a state does not enable is unreachable and undisclosed:
479
+ its slug is not in the prompt, and a listing of `skills/` does not show it.
480
+ - `allow` only adds, so `allow: []` denies nothing (a warning). `forbid` subtracts, the
481
+ workflow-wide grant included. `forbid_always` puts a bundle out of reach everywhere.
482
+ - A workspace that serves bundles warns once when a spec has no root `skills` block.
483
+ Write `skills: { allow_always: [] }` when every bundle is state-specific.
484
+ - A `tools.allow` entry still narrows inside an enabled bundle:
485
+ `{ tool: archmax_run, paths: ["skills/<slug>/scripts/**"] }` confines execution to that
486
+ bundle's scripts.
487
+ - `archmax_run` runs only skill-bundle scripts (rule `script.skill-only`). A script in
488
+ `scratchpad/` or `workflows/` cannot run, so a state that enables no bundle runs no
489
+ script.
490
+ - A grader or code state enables what it reads in its own `skills.allow`. To let a check
491
+ read a bundle the agent must not use, enable it on the check's state only.
1451
492
 
1452
- ```yaml
1453
- states:
1454
- refund-request:
1455
- budget:
1456
- maxTurns: 24 # bounds this turn's agent loop
1457
- # timeoutMs: 60000 # aborts the turn when exceeded
1458
- on_error: escalation # terminal-failure target
1459
- escalation: { instructions: "Explain the failure and hand off." } # terminal
1460
- ```
493
+ ## Mount governance
1461
494
 
1462
- A state's `budget` bounds that state (a grader's own calls are bounded by its
1463
- `budget.maxTurns`). There is no workflow-wide budget: a whole turn is bounded by
1464
- LangGraph's `recursionLimit`, 400 unless the host's invoke config sets its own (a
1465
- sub-workflow's child session runs under the same bound). A turn that throws (model
1466
- failure, cancellation, the recursion limit) ends the session `failed` with exit
1467
- code `turn-error`, `turn-cancelled` or `recursion-limit`; the next message starts
1468
- a fresh turn.
1469
-
1470
- Two failure classes, routed differently:
1471
-
1472
- - **Recoverable rejections keep the mover in place**: an invalid edge in
1473
- `archmax_advance` or an unmet `requires` — the agent (or a grader, or a code
1474
- state's script, which may catch it) sees the reason and retries within the same
1475
- state. A **tool that throws** is answered the same way: its error message is the
1476
- call's error-status answer, and it never routes to `on_error`. A grader or code
1477
- state *sending the work back* is a transition, not a failure: the agent reads
1478
- the reason as a note.
1479
- - **Terminal failures route to `on_error`**: an exhausted `budget`
1480
- (`maxTurns`/`timeoutMs`/`maxParks`), a failed sub-workflow call nothing
1481
- recovered, and a grader or code state that **fails** — threw, timed out, spent
1482
- its budget, or finished without advancing or raising. With `on_error: <state>`
1483
- declared, the run routes to that state (the failure reason arrives as an
1484
- `[error]` runtime note) instead of ending; without it, the failure ends the run.
1485
-
1486
- **Recommendation**: declare `on_error` on every grader and code state, so a script
1487
- error or an undecided grader routes somewhere instead of ending the session
1488
- rejected. The bundled example demonstrates this: `refund-request` pairs
1489
- `budget.maxTurns: 12` with `on_error: escalation`, and its `screen-request`,
1490
- `check-refund` and `review-enrichment` route to the same terminal escalation state.
1491
-
1492
- ## Human states
1493
-
1494
- `type: human` state:
1495
- - Parks the session with a checkpointed **pending-decision record** built from
1496
- `instructions`, evidence file paths, and the transition descriptions/`type`s.
1497
- Nothing is rendered to a file; `archmax run`/`archmax sessions` present that
1498
- context on demand.
1499
- - **Evidence is declared plus attached.** The state's `evidence:` is the baseline
1500
- every reviewer sees; the agent advancing in may attach what this run produced —
1501
- `archmax_advance({ to: "refund-review", reason: "…", evidence: ["scratchpad/carrier-report.md"] })`
1502
- — and the record lists declared paths first, attached after, de-duplicated.
1503
- Attachments are refused (transition untaken, agent may retry) when `to` is not a
1504
- human state, when a path is not one the agent can read (`scratchpad/…`,
1505
- `scratchpad/…`, an authored mount), or when more than 20 are attached. Each
1506
- attachment belongs to the one decision it was made for. Author `evidence:` for
1507
- what must always be shown; leave run-specific artifacts to the attachment.
1508
- - A **human** selects the outgoing edge (optionally with a comment). The LLM does
1509
- not choose. On resume the graph routes deterministically to the chosen target.
1510
- - The target turn is told what happened by a `[decision]` **runtime note**
1511
- naming the edge, the comment, and the state the run is now in. It is a note,
1512
- not a human message: a model reads authorship from a message's channel, and a
1513
- routing event that looks like a person's request gets treated as one — routed
1514
- onward, answered in place, or waited on — instead of being the cue to do the
1515
- target state's work. Do not write instructions that tell a state to "answer
1516
- the user's latest message" when its only inbound edge is a human decision.
1517
- - Resume programmatically with `agent.workflow.decide(sessionId, { target, comment })` or via
1518
- the CLI `archmax decide <sessionId> <target>`.
1519
-
1520
- ## Sub-workflows are tools
1521
-
1522
- A state that may run another workflow names it in `tools.allow`. That entry is
1523
- the whole declaration — there is no state kind and no second key.
495
+ A mount is a folder or a file the host routes into the agent's workspace: `skills/`,
496
+ `AGENTS.md`, a `reference/` folder. The host decides its backend, whether it is writable,
497
+ and whether the spec governs it. Naming a folder in a spec does not create it: ask the
498
+ host to mount it.
499
+
500
+ - An **ungoverned** mount, the default, is visible in every state. Only a `forbid` takes
501
+ it away.
502
+ - A **governed** mount (`governed: true` in the host's table) is reachable only where a
503
+ `mounts` list grants it, exactly like a skill bundle: `allow_always` at the root,
504
+ `allow` on a state, `forbid` and `forbid_always` to subtract. A name is the mount's key
505
+ without its slashes (`reference`, `catalogs/eu`), never a glob.
506
+ - An entry is a name, which reads, or `{ mount: <name>, access: read | read_write }`. A
507
+ governed mount is writable in a state only when the host serves it writable, some grant
508
+ of this workflow asks `read_write`, and no grant that applies in the state says `read`.
509
+ The host's read-only is a ceiling no grant lifts.
510
+ - A mount the state does not have is hidden from listings and searches, the workspace
511
+ root's listing included.
512
+ - A write the host's wiring refuses is `zone.read-only`. One the spec refuses is
513
+ `mount.read-only`.
514
+ - Validate errors on a name the host's table does not mount. It warns on a grant to an
515
+ ungoverned mount, on `read_write` for a read-only mount, and, once, on a missing root
516
+ `mounts` block in a workspace that governs mounts.
517
+
518
+ ## Moving, parking and failing
519
+
520
+ - **Advance.** The agent moves only with `archmax_advance({ to, reason, evidence? })`, one
521
+ per message. The transition and the state's `requires` are checked, and a refusal leaves
522
+ the agent in the state to try again. Then the target's tools unlock, or a grader or code
523
+ target runs at once. Human decisions, `on_error` routes and automated states' own
524
+ advances are the other ways a session moves.
525
+ - **Reset.** `archmax_reset({ reason })` returns the session to the entry state it began
526
+ in and releases the agent's locks. Forbid it with `forbid_always` when a session must
527
+ never restart.
528
+ - **Wait.** A state that needs an outside event calls `archmax_wait({ reason, until? })`
529
+ in the same message as its question. Any agent state may, terminal ones included.
530
+ `until` (`30m`, `2h`, `1d` or an ISO instant) becomes a `resumeAt` for the host's
531
+ scheduler; the runtime keeps no timer. Any delivered trigger continues the **same
532
+ state**, with its payload as locked variables and an `[event]` runtime note, and the
533
+ state then picks its own transition. Bound a polling loop with `budget.maxParks` and an
534
+ `on_error`.
535
+ - **Human states.** Entering one parks the session with a decision record: the state's
536
+ `instructions`, its `evidence` plus any paths the advancing agent attached with
537
+ `archmax_advance({ evidence })`, and its transitions. A person picks the transition,
538
+ and the target state reads the choice and the comment as a `[decision]` runtime note.
539
+ Entering the state spends one reply-only model call, so the park carries a handoff
540
+ message. Do not tell the agent to announce the handoff, or it says it twice. While the
541
+ session is parked, a `reply` is answered on a reply-only turn (every tool call is
542
+ refused) and the session stays parked. Use a human state when a person picks the
543
+ transition, and `archmax_wait` when an event or a time continues the work.
544
+ - **Raise.** When the work cannot be completed, `archmax_raise({ code, reason })`, alone
545
+ in its message, ends the session `failed` at once: no `on_error` route, no `returns`
546
+ check. `code` is a free-form token of up to 64 characters, so name the codes a host
547
+ branches on in the state's `instructions`. Raise is for failure only: a session that
548
+ never raises succeeds. A child that raises fails its caller's call with kind `raised`.
549
+ - **The next turn.** A finished session given a new message takes its next turn in the
550
+ state it ended in, not at the entry. A session that completed in a terminal automated
551
+ state opens at its trigger's entry. Give a state transitions onward when a conversation
552
+ must move on from it, or park instead of finishing.
553
+
554
+ ## Budgets and `on_error`
555
+
556
+ - `budget.timeoutMs` bounds each model call of an agent state, and a grader's whole run.
557
+ `budget.maxParks` bounds how often an agent state parks with `archmax_wait`. A code
558
+ state takes no `budget`: `settings.timeoutMs` bounds its script.
559
+ - A **turn** is bounded by LangGraph's `recursionLimit`: 400 graph steps unless the
560
+ host's invoke config sets one. Every model call, tool batch and automated state is a
561
+ step. A turn that hits the limit, a cycle of automated states included, ends the
562
+ session `failed` with `recursion-limit`. It does not route through `on_error`.
563
+ - **Failures route to `on_error`**: an exceeded `timeoutMs` or `maxParks`, a failed
564
+ sub-workflow call nothing recovered, and a grader or code state that fails (it threw,
565
+ timed out, or finished without advancing or raising). The target state reads the
566
+ reason as an `[error]` runtime note. Without an `on_error`, the failure ends the
567
+ session rejected.
568
+ - **Refusals leave the agent in place**: an undeclared transition, unmet `requires`, an
569
+ ordinary governance refusal. A tool that throws is answered with its error, and the
570
+ agent may retry or change course; `on_error` never sees it.
571
+ - Declare `on_error` on every grader and code state, and on every bounded loop.
572
+
573
+ ## Sub-workflows
574
+
575
+ A state calls another workflow of the workspace by allowing `archmax_workflow_<slug>`,
576
+ with the slug verbatim. That entry is the whole declaration.
1524
577
 
1525
578
  ```yaml
1526
579
  enrich:
1527
580
  instructions: >-
1528
- Enrich the account by calling archmax_workflow_enrich-account with its id,
1529
- then record what it returned with archmax_set_variables.
1530
- requires:
1531
- - enrichment # nothing captures the result — this makes it mandatory
581
+ Call archmax_workflow_enrich-order with the order id, then record what it
582
+ returns as `enrichment` with archmax_set_variables.
583
+ requires: [enrichment] # nothing records the result for you
1532
584
  tools:
1533
- allow:
1534
- - archmax_workflow_enrich-account # the slug, verbatim
1535
- budget: { timeoutMs: 120000 } # bounds the whole turn, calls included
585
+ allow: [archmax_workflow_enrich-order]
1536
586
  on_error: enrichment-failed
1537
587
  transitions:
1538
- - to: draft-reply
1539
- description: The account has been enriched.
1540
- ```
1541
-
1542
- - **The target's signature is the call signature.** Its `manual` trigger's
1543
- `requires:` are the tool's required parameters (typed in the tool schema
1544
- where the entries are typed, by the same mapping `signatureJsonSchema`
1545
- publishes); its `returns:` are what the result carries; its `description`
1546
- leads the tool description, and its `instructions` never reach the caller:
1547
-
1548
- ```
1549
- archmax_workflow_enrich-account({ account_id: "acct-42" })
1550
- → { message: "Enriched acct-42.",
1551
- returns: { enrichment_file: "scratchpad/e/acct-42.json", risk_level: "low" } }
1552
- ```
1553
-
1554
- A target declaring no `returns:` answers with the closing message alone.
1555
- - **Nothing captures the result.** The agent records what it needs with
1556
- `archmax_set_variables`, or a script writes it. Declare the state's `requires:`
1557
- so it cannot advance without doing so — that is what replaces the old automatic
1558
- capture.
1559
- - **One call runs one sub-run.** There is no fan-out argument: an agent fans out
1560
- with parallel tool calls, a script with `Promise.all`. Both are bounded by
1561
- the dispatcher's `maxConcurrent`, and each call succeeds or fails on its
1562
- own.
1563
- - **A script is the deterministic fan-out.** Delegation tools are on the PTC
1564
- surface, so a script decides how many calls to make:
1565
-
1566
- ```js
1567
- // the bridge camelCases tool names; a tool result arrives as text
1568
- const results = await Promise.all(
1569
- orders.map((o) => tools.archmaxWorkflowEnrichAccount({ account_id: o.id })),
1570
- );
1571
- const returns = results.map((r) => JSON.parse(r).returns);
1572
- ```
1573
-
1574
- A script's delegation **cannot park**: a child that reaches a human state fails
1575
- that call closed with `parked`.
1576
- - **Governance is ordinary.** The surface is closed by default, so a state naming
1577
- no target may call none; `tools.forbid_always` blocks a delegation tool like any
1578
- other.
1579
- - **A failure is a tool error** the calling agent can handle; the state's
1580
- `on_error` catches the turn if it cannot. Each failed call is recorded on
1581
- its own, with its arguments, and stays the calling state's failure until a
1582
- **later** successful call of the same workflow from the same state recovers
1583
- it. One success recovers **one** failed call: the one whose arguments it
1584
- repeats (compared after `${{…}}` resolves, key order ignored), else the
1585
- oldest — so two parallel calls that failed need two successes. A sibling
1586
- call in the same batch, another workflow's success, or a rejection with
1587
- another cause (a terminal kernel block, exhausted parks) recovers nothing,
1588
- and the route names every outstanding failure with its arguments. An
1589
- advance, a park or a reset clears them, as it clears any pending rejection. Depth, cycle, missing-input
1590
- (`missing-param`), mistyped-input (`invalid-param`), unresolvable-reference
1591
- (`unresolved-param`) and **disabled-target** refusals are blocked calls —
1592
- nothing ran, so correct the call and retry. A child whose returns are
1593
- mistyped (`invalid-return`) is a failure: it ran. One that left returns unset
1594
- is not: the call answers with them `null` and a `note`.
1595
- - **A whole-argument reference keeps its type.** `{ quantity: "${{count}}" }`
1596
- seeds the child with the number `count` holds, so it satisfies an `integer`
1597
- parameter; `{ note: "order ${{order_id}}" }` is substituted as text.
1598
- - **A `disabled: true` target is refused at dispatch, not at assembly.** The tool is
1599
- still bound and the caller still assembles (disabling one leaf must not take every
1600
- caller — or its parked sessions — offline); the call is a blocked call of kind
1601
- `disabled`, and it beats a declared mock. `validate` **warns** on each state that
1602
- allows a disabled target: the caller's spec is not wrong.
1603
- - **A caller enters the child's `manual` entry** — the one entry the CLI, a host
1604
- firing and a call all use. There is no opt-in on the target: whether a
1605
- delegation may happen is decided by the caller's `tools.allow` entry. A target
1606
- with no `manual` entry has no state to start in and fails closed. `session:` and
1607
- `message:` on that trigger are host-facing keys a delegation ignores: a sub-run
1608
- is its own session, derived from its caller's, and carries no message. `sub-workflow` is no longer
1609
- a trigger id — load and `validate` report it with the rename.
1610
- - **Declare the child's signature** on that trigger — the contract, enforced both
1611
- ways and at both ingresses:
1612
-
1613
- ```yaml
1614
- states:
1615
- enrich:
1616
- triggers:
1617
- manual:
1618
- description: Enrich one order. # leads the caller's tool description
1619
- requires: # a caller must supply these
1620
- - { name: order_id, type: string, description: The order to enrich. }
1621
- returns: # the child guarantees it sets these
1622
- - enrichment_file
1623
- - { name: delayed, type: boolean }
1624
- ```
1625
-
1626
- Develop it standalone on the same contract:
1627
- `archmax run enrich-order --variables '{"order_id":"ORD-1003"}'`. `${{trigger}}`
1628
- reads `manual` either way, so the machine cannot branch on having been called.
1629
-
1630
- A dispatch missing a `requires` name is **refused before the child is
1631
- composed**; a child that completes without every `returns` name set
1632
- **completes anyway**, and the call answers with every declared return, the
1633
- unset ones `null`, plus a `note` naming them — no second attempt is spent, and
1634
- the caller decides
1635
- (a child that *parks* is not checked — it has not finished; a child that
1636
- raises fails the call `raised`). A typed entry is held to its type at both
1637
- ends: a mistyped argument is refused `invalid-param` before the child runs, and
1638
- a mistyped return fails the call `invalid-return`. What the child must *do* to
1639
- produce a variable belongs in the state `instructions` that set it. The names,
1640
- with type and description where declared, are rendered into the child's prompt
1641
- in every state, so it is told what to produce before it can be failed for
1642
- omitting it — and to set each once its final value is known, locking one only
1643
- when it is final.
1644
- - **The call answers with the returns, by name.** A signed target answers
1645
- `{ message, returns: { enrichment_file, delayed } }`; one declaring no
1646
- `returns:` answers with the closing message alone. The message keeps its own
1647
- key rather than being folded into the data — prose the model reads, values a
1648
- guard or a script reads by name. **Nothing captures it into a variable**: the
1649
- agent records what it needs with `archmax_set_variables`, or a script writes it,
1650
- and the calling state's `requires:` is what makes that mandatory.
1651
- - **Mocks are held to the same contract.** An `archmax_workflow_<slug>` mock for a
1652
- signed target supplies `returns:`. One that supplies a mistyped typed one
1653
- (`invalid-return`) fails the mocked dispatch, and one that omits a declared
1654
- name answers like a real child that left it unset: that name `null`, plus a
1655
- `note` — a mock stands in for the sub-run, never for its agreement.
1656
- - **No prose crosses the boundary.** A sub-run gets no instruction from its
1657
- caller — it works from its own state `instructions` (system prompt) plus the
1658
- arguments it was seeded with as locked variables, whose values its opening
1659
- message lists: strings JSON-quoted, numbers and booleans as written. A list,
1660
- an object, `null`, a value rendering past 200 characters, or one past the
1661
- message's 1,000-character total is only named, with `archmax_get_variables`
1662
- as the way to read it. Nothing to author, nothing to keep in sync, and nothing
1663
- in the child's state `instructions` should say "read your inputs first".
1664
- - **A child goes straight to its work.** Its prompt leaves out the platform
1665
- prompt's "Name the run first" step (a child's `title` is never returned), so a
1666
- one-state child with short scalar inputs spends its first model call on the
1667
- state's work. A workspace overriding the platform prompt
1668
- (`.platform/system/GRAPH_STATE.md`) keeps that by wrapping what children skip
1669
- in `<!-- top-level-only -->` … `<!-- /top-level-only -->` lines.
1670
- - **Same agent, isolated context, own session.** Same model, backend, host
1671
- tools; its **own** grader and code states, from its own spec; a fresh transcript; the child's *own* per-state
1672
- `tools.allow`. It is a separate session (`<parent>~<n>`, `n` the dispatch's
1673
- number in the caller, counted across its turns) with its own checkpoints and its own `scratchpad/`: a path it reads or writes
1674
- resolves inside its own session, so it can neither see nor overwrite a file of
1675
- its caller's, and concurrent sub-runs cannot collide on a path.
1676
- - **Nothing crosses implicitly.** Down: only the arguments the call passed —
1677
- the calling run's own variables are not copied. Up: only the result message
1678
- and the declared returns. Everything else the child sets or writes stays in its
1679
- session. The only files that cross are runtime-made copies of `file` arguments
1680
- and `file` returns (see "Passing files" below).
1681
- - **Bounds** — the dispatcher's `bounds: { maxDepth: 3, maxConcurrent: 4 }`. No longer a spec key: the strict schema rejects `settings.sub_workflows` by name.
1682
- Excess concurrency queues; depth and cycles are refused.
1683
- - **Params are the sub-run's initial variables** — same construction, same name
1684
- rule, same lock as a host's `variables`. So a delegated machine also runs
1685
- **standalone** with those inputs supplied directly:
1686
- `archmax run <child> --variables '{"order_id":"ORD-1003"}'` — no `--trigger`,
1687
- because a caller and the CLI enter the same `manual` entry. That is how you
1688
- develop one in isolation, on exactly the contract a caller holds it to.
1689
- - **A child may stop for a person.** A `type: human` state in a delegated machine
1690
- suspends the sub-run *and* the delegating run; `archmax decide <sessionId>
1691
- <target>` resolves the **child's** decision, the sub-run finishes, and the
1692
- parent carries on with its result. This holds for a **fan-out** too: each
1693
- dispatch is its own checkpointed task, so resolving one decision re-runs only
1694
- that sub-run — siblings that finished keep their results.
1695
- - **Fails closed** — rejection, budget, refusal or an unresolvable param routes
1696
- through `on_error` (or ends the run rejected); a suspension for a person is not
1697
- a failure. Ancestor
1698
- `forbid_always` rules bind inside the child, naming the workflow that
1699
- declared them.
1700
-
1701
- `archmax validate` checks all of this statically: target exists, opted in, no
1702
- cycle, no over-deep chain, correct state shape — and the signature both ways. A
1703
- parent reading a field the child does not return is an **error**; a state whose
1704
- `params:` do not cover the target's `requires:` is a **warning** (the advance may
1705
- still carry it), and a fan-out is exempt from that warning because its
1706
- entries' keys are not knowable statically.
1707
-
1708
- ### Passing files
1709
-
1710
- A sub-run's `scratchpad/` is not its caller's, so a bare path string returned by a
1711
- child (`returns: [result_file]`, untyped) names a file the caller **cannot
1712
- read**. Type the entry `file` instead, and the runtime copies the file across:
1713
-
1714
- ```yaml
1715
- # the child
1716
- triggers:
1717
- manual:
1718
- requires: [{ name: invoice, type: file }]
1719
- returns: [{ name: report, type: file }]
1720
- ```
1721
-
1722
- - **Down.** The caller passes any path in its workspace (`scratchpad/inbox/a.pdf`,
1723
- `skills/billing/assets/template.pdf`). Before the child starts, the runtime
1724
- copies the exact bytes into the **child's** `scratchpad/attachments/` (same name;
1725
- two arguments with one name take `-1`, `-2`) and seeds the child with that path,
1726
- so its opening message shows it. The child edits its own copy and never the
1727
- caller's file. The argument is a `read` path argument of the delegation tool, so
1728
- a `paths:` guard on the allow entry and the zone and mount rules bind it. An
1729
- argument naming nothing is refused `invalid-param` before the child runs. A
1730
- failed call's record names the caller's path.
1731
- - **Up.** On completion, each `file` return the child set is copied into the
1732
- **caller's** `scratchpad/attachments/` and the result names that path. A taken
1733
- name gets the lowest free `-1`, `-2`, … before the extension (`report-1.pdf`), so
1734
- repeated or parallel calls never overwrite each other or the caller's files. A
1735
- return whose file is gone at completion fails the call `invalid-return`.
1736
- - **No laundering.** A `file` value must name a file the session may read on
1737
- runtime authority (its own workflow's mount grants and `forbid_always`), and a
1738
- child inherits every ancestor's `forbid_always`. So a
1739
- child cannot hand back a file its caller's workflow denies: its agent's write
1740
- is refused, and a script's write is rejected at completion.
1741
- - **Instructions to write.** Tell the child to write the file *first* and then set
1742
- the return to its path. Tell the caller the returned path is under
1743
- `scratchpad/attachments/`. Name files for what they hold (`<order_id>.json`):
1744
- the name survives the copy.
1745
- - **Mocks and cases.** A mocked `file` return names a file in the case's own
1746
- session (seed it with `workspace:`), and it arrives copied into
1747
- `scratchpad/attachments/` like a real one. Assert the copy's path.
1748
-
1749
- ### A parked run may speak, never act
1750
-
1751
- A park does not end the conversation. Two moments get a **reply-only turn** — one
1752
- model call whose only product is text. It is handed the parked state's tool list
1753
- like any other call there (a transcript's tool calls and runtime notes need their
1754
- definitions on some providers), and every call it makes is refused:
1755
-
1756
- - **The handoff.** Advancing into a human state always spends one, so the park
1757
- carries a message saying what was done and that a person now holds it. It runs
1758
- whether or not the state already spoke: the agent wrote before the transition
1759
- committed, so what it wrote was about its own work. (An `archmax_wait` park gets
1760
- the same turn only when the turn was silent — there the agent's own question
1761
- is the message.)
1762
- - **While parked.** `agent.workflow.reply(sessionId, message)` / `archmax reply <session>
1763
- "<text>"` answers a message and parks again on the *same* record — same node,
1764
- `seq`, and park timestamp, so the reported human wait still measures the
1765
- original park. `resolveSession` classifies such a firing as the `reply`
1766
- disposition rather than failing.
1767
-
1768
- Nothing you author changes this and there is no field to declare: the message is
1769
- always composed on a fresh turn from the transcript, under the workspace persona.
1770
- The kernel refuses every tool call made during one (rule `tool.reply-only`,
1771
- ordered ahead of `tools.allow`, `allow_always`, the `archmax_*` controls and the
1772
- scratchpad), so a customer writing "just approve it" gets an answer and the
1773
- reviewer still owns the edge. A model that calls a tool instead of writing its
1774
- message gets the refusal, and the run parks without a second model call.
1775
-
1776
- **What this means for authoring:** do not write state `instructions` that tell the
1777
- agent to announce the handoff itself — the runtime turn does that, and having both
1778
- produces two messages. Write instructions for the *work*.
1779
-
1780
- ## Removed keys you may meet in an older workflow
1781
-
1782
- Every key here is **rejected**, with the key and its path named and guidance
1783
- saying what replaced it. `workflow.yaml` is parsed through a strict schema, so a
1784
- workflow still declaring one fails to load rather than running while silently
1785
- doing nothing of what its author intended.
1786
-
1787
- This is a change: these keys used to be ignored without a diagnostic. A spec that
1788
- "worked" by ignoring one now needs the migration below.
1789
-
1790
- | Removed | Replace with |
1791
- | --- | --- |
1792
- | `type: wait` state | The state that needs the answer calls `archmax_wait({ reason })`; a delivery resumes that same state (see [Parking and sessions](#parking-and-sessions)) |
1793
- | transition `on: <trigger-id>` | Nothing — a delivered trigger no longer selects an edge; the resumed state picks its own transition |
1794
- | state `parallel: [...]` + `join:` | Ordinary `transitions`, sequenced |
1795
- | state `type: trigger` | A `trigger:` field on the state the trigger state routed to |
1796
- | top-level `name:` | `title:` |
1797
- | top-level `description:` | `instructions:` |
1798
- | top-level `policy:` | `tools.forbid_always` (and `skills.forbid_always`) — a load error now |
1799
- | root `skills.allow:` (the ceiling) | `skills.allow_always: []` plus the state lists — a load error now |
1800
- | state `type: workflow` | An ordinary agent state allowing `archmax_workflow_<target>` and calling it (see [Sub-workflows are tools](#sub-workflows-are-tools)) |
1801
- | state `workflow: <slug>` | The `archmax_workflow_<slug>` entry in that state's `tools.allow` — the allow entry *is* the declaration |
1802
- | state `params:` | The call's arguments |
1803
- | state `for_each:` | One call per entry — parallel tool calls from the agent, or `Promise.all` in a script |
1804
- | state `result:` | The tool result; add `requires: [<name>]` to the state so it must record what it needs |
1805
- | `archmax_advance({ params })` | The call's arguments |
1806
- | state `before:` (any hook kind) | A **code state in front** of the state — the trigger's entry, or the target of the edge into it — advancing to the work or to a declining state (see [Grader and code states](#grader-and-code-states)) |
1807
- | state `after: { script: hooks/<x>.js }` | A **code state behind** the state: its transition points at the code state, which advances onward or back with the fix as its `reason` |
1808
- | `{ rubric: … }` hook | A **grader state** behind the graded state with an edge back; the criteria become its `instructions` |
1809
- | `max_iterations` / `max_corrections` | Nothing: the grader's `instructions` (or the script) count send-backs in the trail and raise |
1810
- | root `extensions:` (custom hook kinds) and `createAgent({ hookExecutors })` | Nothing: a custom check is a code state |
1811
- | `workflows/<slug>/hooks/*.js` returning `ok`/`veto`/`correct` | `workflows/<slug>/scripts/*.js` that call `tools.archmax_advance` / `tools.archmax_raise` |
1812
- | `runtime.version: "1"` / `"2"` | `"3"` (or omit `runtime`) — 1 and 2 are refused as unsupported |
1813
- | root `budget: { maxModelCalls }` | Nothing — delete it. A turn is bounded by LangGraph's `recursionLimit` (400 unless the host's invoke config sets one); bound a state with its own `budget.maxTurns` |
1814
-
1815
- **No fan-out.** There is no concurrent branching: a run traverses **one state at a
1816
- time**, moved only by `archmax_advance`, a human decision, or `on_error` routing.
1817
- Sequence the states that were branches:
1818
-
1819
- ```yaml
1820
- # REMOVED — do not author this
1821
- states:
1822
- gather:
1823
- parallel: [check-inventory, check-credit]
1824
- join: summarize
1825
-
1826
- # author this instead
1827
- states:
1828
- check-inventory:
1829
- transitions: [{ to: check-credit }]
1830
- check-credit:
1831
- transitions: [{ to: summarize }]
1832
- summarize: { instructions: "..." }
1833
- ```
1834
-
1835
- Concurrent composition **is** sub-workflows: a state calling a delegation tool
1836
- once per unit of work — the calls run concurrently (see
1837
- [Sub-workflows are tools](#sub-workflows-are-tools)). Sequence anything that is not that —
1838
- it costs latency, not correctness.
1839
-
1840
- ## Workspace layout
1841
-
1842
- ```
1843
- workflows/<name>/workflow.yaml # the machine (canonical spec)
1844
- workflows/<name>/WORKFLOW.md # OPTIONAL prose addendum (no frontmatter)
1845
- workflows/<name>/scripts/*.js # code-state scripts — run BY THE RUNTIME when a
1846
- # `type: code` state is entered.
1847
- # Authoring plane: the agent cannot read or run
1848
- # anything under workflows/
1849
- workflows/<name>/tests/ # cases (*.test.yaml, one case per
1850
- # file; fixture files referenced with `from:`)
1851
- skills/<capability>/SKILL.md # a capability, bundled (Agent Skills format).
1852
- # Shared guidance for content that generalizes
1853
- # across MULTIPLE states — single-state
1854
- # behavior goes inline in that state's
1855
- # `instructions` (never one SKILL.md per state)
1856
- skills/<capability>/scripts/*.js # its archmax_run sources — run BY THE AGENT
1857
- # (code-state scripts are NOT here: they live in
1858
- # workflows/<name>/scripts/ — same directory
1859
- # name, other plane — are run by the runtime,
1860
- # and the agent cannot read them)
1861
- skills/<capability>/assets/* # its inputs (read-only)
1862
- scratchpad/ # THE working area: intermediate files and the
1863
- # artifacts a state produces. The run IS the
1864
- # workspace root — resolves to
1865
- # sessions/<sessionId>/scratchpad/. Always
1866
- # writable, per-session
1867
- large_tool_results/ # runtime-offloaded tool results (read-only)
1868
- conversation_history/ # runtime-offloaded history (read-only)
1869
- AGENTS.md # workspace persona/system prompt
1870
- ```
1871
-
1872
- This is the **complete** directory contract — there are no other magic
1873
- locations, so design the full file set up front instead of exploring.
1874
- Reference these paths **workspace-relative, without a leading slash**
1875
- (`skills/order-data/assets/orders.json`, `scratchpad/refund.json`) — in `instructions`, `evidence`,
1876
- allow-entry globs, and paths referenced inside code-state scripts alike.
1877
-
1878
- The authoring-plane prefix (`workflows/`) is **reserved**: a
1879
- host cannot mount it (`MountCollisionError` at assembly), an allow entry naming
1880
- it is a `validate` **error**, and a script that reads it is blocked with a
1881
- diagnostic naming the prefix. Never write an allow entry, an `evidence` path, or a
1882
- case `workspace:` key under it.
1883
-
1884
- ## Cases
1885
-
1886
- Declarative YAML case documents under `workflows/<name>/tests/*.test.yaml`
1887
- (or `.test.yml`) — **one case per file**; the case id is the tests/-relative
1888
- path minus the extension. `archmax test <workflow>` interprets them
1889
- **host-side**: no QuickJS sandbox runs a case (the sandbox serves only
1890
- code-state scripts and `archmax_run` inside the driven agent). These are distinct
1891
- from the runtime's own colocated unit tests. Only `*.test.yaml`/`*.test.yml`
1892
- files are discovered — a leftover `*.test.js` is ignored without a diagnostic.
1893
-
1894
- A workflow declaring `disabled: true` runs **no** cases: the suite is reported as
1895
- **skipped** and the exit code is **0** (not run is not failed, so retiring a
1896
- workflow never reddens CI). There is no flag to force them; `archmax validate` is
1897
- the static feedback while a workflow is out of service.
1898
-
1899
- **Suite config lives in the `tests:` block of `workflow.yaml`**
1900
- (`maxConcurrency`, `caseTimeoutMs`, `judge` — see [Root fields](#root-fields)).
1901
- `tests.config.js` is **REMOVED**: its presence is a hard error in
1902
- `archmax test` and `archmax validate`. There is no `reporters` option.
1903
- `judge: {}` enables the case judge (`grade:`) with the env-configured default model.
1904
-
1905
- **The complete case schema (top-level keys, the flat step-entry list, the
1906
- assertion grammar, start conditions, mocks, `from:` fixture references) plus
1907
- worked examples — routing,
1908
- tool mocking, human-in-the-loop decide flows, triggered starts — and the
1909
- JS→YAML migration table are in
1910
- [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).**
1911
-
1912
- Every case carries two mandatory prose fields, both length-capped: a `title`
1913
- (a short label, ≤ 60 chars, printed beside the verdict) and a `description`
1914
- (≤ 200 chars — the scenario driven and what is asserted). `archmax validate`
1915
- **errors** on a missing or over-long field; longer rationale belongs in a YAML
1916
- comment above the document (see
1917
- [Code description docblocks](#code-description-docblocks)). A case is one
1918
- conversation on one session; `steps` is one flat list where actions (`send`,
1919
- `decide`) and assertions are peers, each assertion evaluating against the
1920
- nearest action above it. Unknown keys anywhere in a case are validation
1921
- errors — fail closed.
1922
-
1923
- ```yaml
1924
- # Nothing is mocked — the case reads the real orders.json fixture, so the
1925
- # denial is re-derived by the check-refund code state rather than asserted directly.
1926
- title: Refund denied for a shipped order
1927
- description: >-
1928
- A refund for shipped A-1002 parks at refund-review; the reviewer confirms the
1929
- denial via refund-closed, the run succeeds, and the reply names the order.
1930
- steps:
1931
- - send: "Hi, I'm support@acmecorp.com. Please refund order A-1002."
1932
- - parked: true
1933
- - reachedState: refund-review
1934
- - decide:
1935
- to: refund-closed
1936
- comment: "The denial matches policy."
1937
- - succeeded: true
1938
- - reply:
1939
- includes: "A-1002"
588
+ - { to: reply, description: The order is enriched. }
1940
589
  ```
1941
590
 
1942
- ## Code description docblocks
1943
-
1944
- **Every** code file in the workspace — both kinds: code-state scripts and
1945
- `archmax_run` sources — MUST begin with a leading JSDoc block (`/** ... */`): a
1946
- title line, a blank line, then prose stating what the code does **and how**.
1947
- Cases are YAML documents, not code — they carry no JSDoc block;
1948
- their mandatory `title` and `description` fields are short and capped instead,
1949
- with any longer rationale in a YAML comment above the document. The canonical
1950
- statement of the
1951
- rule, with the required depth per file kind, is
1952
- [SKILL.md → Workspace layout](../SKILL.md#workspace-layout-complete--nothing-else-exists).
1953
-
1954
- - The block is the **human-facing contract**: admin UIs display only this
1955
- description, and the code below it must honor it. Author
1956
- description-first; when behavior changes, update the description, then edit
1957
- the code to match — never leave the two contradicting each other.
1958
- - Write it deep enough to review on its own: inputs read, the sequence of
1959
- checks or assertions, and each outcome the code can produce (for a code-state
1960
- script: each `archmax_advance` target and why, each `archmax_raise` code). A
1961
- one-line label is not enough.
1962
- - It is inert at runtime — the QuickJS sandbox executes the source verbatim
1963
- (typed `@archmax-ai/harness/*` imports excepted; they are stripped).
1964
- - The prose ends at the first `@tag` line (tags are allowed but not part of
1965
- the description), so keep the title and body above any tag. There is no
1966
- `@specification` tag and no generation marker: the block is a human
1967
- description, not a machine contract.
1968
- - `archmax validate` **errors** when an cases is missing its
1969
- `description` (the case schema requires it). Code-state scripts and `archmax_run`
1970
- sources are **not** checked — there the JSDoc rule is an authoring standard
1971
- you uphold yourself.
1972
- - Backends/UIs extract it with the package's exported
1973
- `parseCodeDescription(source)` helper rather than scraping comments.
1974
-
1975
- ## Read-only zones
1976
-
1977
- Writes (via `write_file`/`edit_file`) targeting any read-only authored mount —
1978
- the conventional directories plus root files such as `AGENTS.md`, or whatever the
1979
- wiring code mounted — are blocked, by the mount itself and by governance
1980
- (`zone.read-only`). A mount key of several segments (`catalogs/eu`) is matched by
1981
- **longest prefix**, so a nested mount is a read-only zone exactly as a
1982
- single-segment one is, and a sibling under the same first segment (`catalogs/uk`)
1983
- is not.
1984
-
1985
- A mount the host mounted **writable** is not a read-only zone — its paths are
1986
- governed like ordinary run paths — but a governed one is written only where a grant
1987
- of the workflow says `access: read_write`, and a state may still be given it
1988
- read-only, with `mounts: { allow: [{ mount: <name>, access: read }] }`. A write
1989
- refused by a grant is `mount.read-only`; one refused by the wiring is
1990
- `zone.read-only`.
1991
- See [Mount governance](#mount-governance).
1992
-
1993
- The run root is writable, and it has exactly one named working area:
1994
- `scratchpad/**`, permitted (read and write) in **every** state regardless of
1995
- `tools.allow`. Agent-visible `scratchpad/…` resolves to
1996
- `sessions/<sessionId>/scratchpad/…`, so concurrent sessions never collide.
1997
-
1998
- Because that permission is unconditional, an allow entry naming a path *inside*
1999
- `scratchpad/` does not constrain where within the area a write lands — `archmax
2000
- validate` warns when a write entry's paths are all scratchpad paths. To govern
2001
- *where* a result lands, name an ordinary run path instead (any run-root path
2002
- outside the reserved areas — `reports/x.json`, or a root-level file); those are
2003
- matched against the state's `tools.allow` like any other argument. The framework
2004
- owns no second artifact area.
2005
-
2006
- ## Annotated example: order-lookup
2007
-
2008
- The bundled reference workspace lives at
2009
- `examples/customer-support/workflows/order-lookup/` and demonstrates the full
2010
- v3 layout: `workflow.yaml` (the machine, including the `tests:` block and its
2011
- grader and code states), the code-state scripts under `scripts/`, plus a
2012
- frontmatter-free `WORKFLOW.md` prose addendum (ASCII graph + domain constraints).
2013
- Read them in full for a real, working example. It declares a top-level `title`
2014
- and a `title` on every state, with the slugs below as the reference targets, plus
2015
- a top-level `instructions` block holding the rules that hold across every state —
2016
- where the data and artifacts live, the tenancy rule, and naming an order's exact
2017
- status — so no state repeats them. Its shape:
2018
-
2019
- - `screen-request` (code, `triggers: { manual: , email_reply: }`,
2020
- `scripts/screen-request.js`, enables `order-data`) — a check **in front**: it
2021
- merges the requester and tenancy checks before any model call, and advances to
2022
- `identify-case`, or to `decline` (a terminal agent state that tells the person
2023
- why) with the reason.
2024
- - `identify-case` routes to one of four paths via transition descriptions.
2025
- - `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`:
2026
- a live answer ends where it is given.
2027
- - `refund-request` records a decision to `scratchpad/refund.json` (no allow entry
2028
- narrows it — the working area is open to `write_file` wherever it is granted;
2029
- what holds the agent to a correct file is the check) and advances to
2030
- `check-refund`, a code state **behind** it (`scripts/check-refund.js`): it
2031
- re-derives the policy, advances to `refund-review` when the decision matches,
2032
- sends it back with the fix otherwise, and escalates after two send-backs.
2033
- `refund-request` pairs `budget.maxTurns: 12` with `on_error: escalation`.
2034
- - `refund-review` (`type: human`, `evidence: [scratchpad/refund.json]`) — a person
2035
- approves (→ terminal `refund-closed`) or refines (→ back to `refund-request`).
2036
- - `escalation` (terminal) is the error handler entered via `on_error` (and from
2037
- `check-refund`'s give-up edge).
2038
- - `general-question` (terminal) answers and writes `scratchpad/answer.json`.
2039
- - `report-requested` (terminal, `triggers: { report_requested: }`) writes
2040
- `scratchpad/report.json` for the requested `company` and summarizes it; when the
2041
- order records cannot be read, the agent ends the session with `archmax_raise`
2042
- instead (case `orders-unavailable`).
2043
- - `enrich-order` (`triggers: { enrichment_requested: }`) — a host-fired path no
2044
- person waits on — runs the `order-enrichment` fan-out script with `archmax_run`,
2045
- records its result as `enrichments` (`requires: [enrichments]`), and advances to
2046
- `review-enrichment`, a **grader** behind it: it reads `orders_to_enrich` and
2047
- `enrichments` with `archmax_get_variables` and each returned `enrichment_file`
2048
- with `read_file`. It approves into `enrichment-reported` (a terminal agent state
2049
- that summarizes the enrichments), sends them back to `enrich-order` naming what
2050
- to fix, or raises `enrichment-incorrect` once it has sent them back twice,
2051
- counted on `archmax_get_history`'s trail. Case `enrichment-fan-out` pins the
2052
- approval as a `grader` trail step.
2053
-
2054
- ### Source of truth in code
2055
-
2056
- If you need to confirm a detail, these files define the schema in the runtime:
2057
-
2058
- | Concern | File |
591
+ - **The child's `manual` trigger is the call's signature.** Its `requires` are the
592
+ parameters (typed where the entries are typed), its `returns` the result, and its
593
+ `description` leads the tool's description. Its `instructions` never reach the caller.
594
+ - **The call answers** `{ message, returns: { … } }`, or the closing message alone when
595
+ the child declares no `returns`. Nothing records the answer: the caller's agent sets
596
+ what it needs, and the calling state's `requires` makes that mandatory.
597
+ - **The child is a separate session** (`<parent>~<n>`) with a fresh transcript, its own
598
+ `scratchpad/`, and its own grader and code states. It receives only its arguments, as
599
+ locked variables. A `file` argument or return is copied into the receiving session's
600
+ `scratchpad/attachments/`, so type file entries `file`: a bare path names a file the
601
+ other session cannot read.
602
+ - **One call runs one child.** Fan out with parallel tool calls, or with `Promise.all` in
603
+ a script. Depth is bounded at 3 and concurrency at 4.
604
+ - **A refused call ran nothing**: a missing, mistyped or unresolvable argument, a
605
+ disabled target, a cycle, a chain too deep. Correct it and retry.
606
+ - **A failed call is the calling state's failure**: the child failed or raised, or
607
+ returned a mistyped value. It routes through `on_error` when the agent finishes, unless
608
+ a later successful call of the same workflow from the same state recovers it. One
609
+ success recovers one failure: the one with the same arguments, else the oldest.
610
+ - **A whole-argument reference keeps its type**: `{ quantity: "${{count}}" }` passes the
611
+ number.
612
+ - **A child's human state parks the child and its caller.** Deciding it finishes the
613
+ child, and the caller carries on. A script's call cannot park: a child that reaches a
614
+ human state fails it.
615
+ - **`forbid_always` binds every child**, naming the workflow that declared it. A child
616
+ inherits no per-state list.
617
+ - **Develop a child on its own**, on the same contract:
618
+ `archmax run <child> --variables '{"order_id":"ORD-1003"}'`.
619
+
620
+ ## Models
621
+
622
+ - `settings.model` is the id every state runs on, and a state's `model` overrides it.
623
+ Each is an id over the endpoint the host configured. The order is the state's, then the
624
+ workflow's, then `ARCHMAX_MODEL`.
625
+ - Name one only where the work differs in kind: a small model for classification and
626
+ extraction, the default for drafting, judgment, and states with many tools or
627
+ transitions. A small model that routes badly costs more in retries than it saves.
628
+ - A grader state's model is resolved under the `grader` model role. A sub-workflow uses
629
+ its own spec's ids. A host that passes an explicit `model` instance to `createAgent`
630
+ outranks every declared id.
631
+
632
+ ## Prompt economy
633
+
634
+ The static prompt and the tool schemas are sent on every model call, and the static part
635
+ is cached. It carries the workflow's `title` and top-level `instructions`, and no state.
636
+ A state's `instructions` and transitions are sent only while it is active.
637
+
638
+ - Put rules that hold everywhere in the top-level `instructions`, once. Put everything
639
+ else in the state that needs it: splitting a broad state into narrow ones costs nothing
640
+ in the prefix.
641
+ - `title` and `summary` cost nothing: no model reads them.
642
+ - `WORKFLOW.md` carries only what the spec cannot say.
643
+ - Every model call states the current date and time in UTC, rounded down to ten minutes,
644
+ so date arithmetic needs no authored "today".
645
+
646
+ ## Removed keys
647
+
648
+ Each key below is a load error that names it. The
649
+ [changelog](https://harness.archmax.ai/reference/changelog/) says what changed and when.
650
+
651
+ | Key | Write instead |
2059
652
  | --- | --- |
2060
- | The schema (every key, `script` grammar, `refineSpec`) | `src/machine/spec-schema.ts` |
2061
- | Per-type lint errors and warnings | `src/machine/lint-spec.ts` |
2062
- | Spec types (`MachineState`, `Transition`, `AllowEntry`, `WorkflowTestsConfig`) | `src/machine/types.ts` |
2063
- | Two-file loading (`workflow.yaml` + prose addendum) | `src/machine/load-spec.ts` |
2064
- | Type-aware surfaces (`controlTools`, `disclosedTools`, `checkAllowed`) | `src/machine/machine.ts`, `tool-names.ts` |
2065
- | allow-entry matching | `src/machine/allow.ts` |
2066
- | Rendered workflow header + per-state graph block | `src/workflow/render-prompt.ts` |
2067
- | Automated states: running, outcomes, controls, code and grader runs, history | `src/workflow/automated.ts`, `automated-controls.ts`, `code-state.ts`, `grader-state.ts`, `history.ts` |
2068
- | Governance kernel (precedence) | `src/kernel/kernel.ts` |
2069
- | `archmax_advance` and the other control tools | `src/workflow/control-tools.ts` |
2070
- | Checkpointed state, `WORKFLOW_STATUSES` | `src/workflow/state.ts` |
653
+ | root `name:` / `description:` | `title:` / `instructions:`, reread as direction |
654
+ | root `policy:` | `tools.forbid_always` and `skills.forbid_always` |
655
+ | root `skills.allow` (a ceiling) | `skills.allow_always` plus the state lists |
656
+ | root `budget` | Nothing: the `recursionLimit` bounds a turn |
657
+ | state `budget.maxTurns` | Nothing: the `recursionLimit` bounds a turn |
658
+ | state `before:` / `after:` (any kind, `rubric:` included), `max_iterations`, `max_corrections`, root `extensions:` | A code state in front of the state for a `before` check, a code state behind it for an `after` check, a grader state behind it for a rubric. Count send-backs on the trail. Scripts move from `hooks/` to `scripts/` and call `tools.archmax_advance` or `tools.archmax_raise`. |
659
+ | state `type: wait`, transition `on:` | `archmax_wait` in the state that needs the answer |
660
+ | state `type: trigger` | `triggers:` on the state it entered |
661
+ | state `type: workflow`, `workflow:`, `params:`, `for_each:`, `result:` | An agent state allowing `archmax_workflow_<slug>`, with `requires` |
662
+ | state `parallel:` / `join:` | Sequenced states |
663
+ | `runtime.version` `1` or `2` | `"3"`, or no `runtime` block |
664
+ | a single-file `WORKFLOW.md` with frontmatter | `workflow.yaml` plus a plain `WORKFLOW.md` |
665
+ | `tests.config.js` | The `tests:` block |
666
+
667
+ ## Paths and zones
668
+
669
+ - The agent's workspace root is the session. `scratchpad/` is its working zone, writable
670
+ in every state that is granted a file tool. `large_tool_results/` and
671
+ `conversation_history/` are offload zones the agent only reads. `checkpoints/` and
672
+ `artifacts/` belong to the runtime.
673
+ - Mounts are read-only unless the host serves them writable. A write into a read-only
674
+ mount is refused (`zone.read-only`).
675
+ - `workflows/**` is served by the authoring backend: no state may read it, an allow entry
676
+ naming it is a validate error, and a host cannot mount it.
677
+ - Write paths without a leading slash, and never author a `run/` prefix.