@archmax-ai/harness 0.9.0 → 1.0.1

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 (207) hide show
  1. package/README.md +58 -14
  2. package/dist/agent.d.ts +4 -6
  3. package/dist/agent.js +4 -6
  4. package/dist/agent.js.map +1 -1
  5. package/dist/assembly/compose.d.ts +5 -24
  6. package/dist/assembly/compose.js +28 -42
  7. package/dist/assembly/compose.js.map +1 -1
  8. package/dist/assembly/delegation-registry.js +3 -14
  9. package/dist/assembly/delegation-registry.js.map +1 -1
  10. package/dist/assembly/index.d.ts +8 -15
  11. package/dist/assembly/index.js +21 -28
  12. package/dist/assembly/index.js.map +1 -1
  13. package/dist/assembly/plain.js +1 -2
  14. package/dist/assembly/plain.js.map +1 -1
  15. package/dist/authoring-skill/archmax-harness/README.md +5 -5
  16. package/dist/authoring-skill/archmax-harness/SKILL.md +72 -30
  17. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +33 -24
  18. package/dist/authoring-skill/archmax-harness/references/cases.md +41 -8
  19. package/dist/authoring-skill/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
  20. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +115 -0
  21. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +40 -7
  22. package/dist/authoring-skill/archmax-harness/references/wiring.md +19 -10
  23. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +404 -293
  24. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +151 -62
  25. package/dist/cli/state-flow.d.ts +1 -1
  26. package/dist/cli/state-flow.js +18 -28
  27. package/dist/cli/state-flow.js.map +1 -1
  28. package/dist/cli/turn.d.ts +1 -1
  29. package/dist/cli/turn.js +1 -1
  30. package/dist/core/code-description.d.ts +3 -3
  31. package/dist/core/code-description.js +3 -3
  32. package/dist/core/deepagents.d.ts +5 -12
  33. package/dist/core/deepagents.js +4 -17
  34. package/dist/core/deepagents.js.map +1 -1
  35. package/dist/core/events.d.ts +23 -42
  36. package/dist/core/events.js +21 -24
  37. package/dist/core/events.js.map +1 -1
  38. package/dist/core/message-readers.d.ts +4 -5
  39. package/dist/core/message-readers.js +3 -8
  40. package/dist/core/message-readers.js.map +1 -1
  41. package/dist/core/messages.d.ts +0 -22
  42. package/dist/core/messages.js +1 -32
  43. package/dist/core/messages.js.map +1 -1
  44. package/dist/core/mounts.js +3 -3
  45. package/dist/core/platform-prompt.generated.js +1 -1
  46. package/dist/core/platform-prompt.generated.js.map +1 -1
  47. package/dist/core/session-zone.d.ts +1 -1
  48. package/dist/core/session-zone.js +1 -1
  49. package/dist/core/workspace-context.d.ts +1 -1
  50. package/dist/core/workspace.d.ts +1 -1
  51. package/dist/core/workspace.js +1 -1
  52. package/dist/core/zones.d.ts +4 -4
  53. package/dist/core/zones.js +5 -5
  54. package/dist/core/zones.js.map +1 -1
  55. package/dist/env.d.ts +9 -9
  56. package/dist/index.d.ts +3 -7
  57. package/dist/index.js +2 -4
  58. package/dist/index.js.map +1 -1
  59. package/dist/kernel/kernel.d.ts +31 -61
  60. package/dist/kernel/kernel.js +54 -112
  61. package/dist/kernel/kernel.js.map +1 -1
  62. package/dist/machine/lint-spec.js +53 -12
  63. package/dist/machine/lint-spec.js.map +1 -1
  64. package/dist/machine/machine.d.ts +36 -18
  65. package/dist/machine/machine.js +62 -51
  66. package/dist/machine/machine.js.map +1 -1
  67. package/dist/machine/spec-schema.d.ts +32 -180
  68. package/dist/machine/spec-schema.js +52 -113
  69. package/dist/machine/spec-schema.js.map +1 -1
  70. package/dist/machine/tool-names.d.ts +34 -16
  71. package/dist/machine/tool-names.js +41 -16
  72. package/dist/machine/tool-names.js.map +1 -1
  73. package/dist/machine/types.d.ts +14 -46
  74. package/dist/machine/types.js +0 -2
  75. package/dist/machine/types.js.map +1 -1
  76. package/dist/machine/validate-spec.d.ts +1 -1
  77. package/dist/public/sandbox.d.ts +59 -95
  78. package/dist/public/sandbox.js +22 -23
  79. package/dist/public/sandbox.js.map +1 -1
  80. package/dist/public/spec.d.ts +13 -20
  81. package/dist/public/spec.js +12 -20
  82. package/dist/public/spec.js.map +1 -1
  83. package/dist/runtime/contract.d.ts +11 -12
  84. package/dist/runtime/contract.js +16 -16
  85. package/dist/runtime/contract.js.map +1 -1
  86. package/dist/sandbox/assets/parts/code-state.js +17 -0
  87. package/dist/sandbox/executor.d.ts +25 -7
  88. package/dist/sandbox/executor.js +56 -57
  89. package/dist/sandbox/executor.js.map +1 -1
  90. package/dist/sandbox/imports.d.ts +10 -10
  91. package/dist/sandbox/imports.js +11 -11
  92. package/dist/sandbox/imports.js.map +1 -1
  93. package/dist/sandbox/prelude.d.ts +5 -6
  94. package/dist/sandbox/prelude.js +38 -35
  95. package/dist/sandbox/prelude.js.map +1 -1
  96. package/dist/sandbox/ptc-gateway.d.ts +19 -18
  97. package/dist/sandbox/ptc-gateway.js +26 -22
  98. package/dist/sandbox/ptc-gateway.js.map +1 -1
  99. package/dist/sessions/resume.d.ts +2 -2
  100. package/dist/sessions/resume.js +1 -1
  101. package/dist/sessions/resume.js.map +1 -1
  102. package/dist/sessions/scope.d.ts +0 -10
  103. package/dist/sessions/scope.js +0 -17
  104. package/dist/sessions/scope.js.map +1 -1
  105. package/dist/sessions/summary.d.ts +1 -1
  106. package/dist/testing/assertions.js +9 -4
  107. package/dist/testing/assertions.js.map +1 -1
  108. package/dist/testing/driver.js +3 -0
  109. package/dist/testing/driver.js.map +1 -1
  110. package/dist/testing/interpreter.d.ts +2 -2
  111. package/dist/testing/runner.d.ts +2 -2
  112. package/dist/validate/validate.d.ts +1 -1
  113. package/dist/validate/validate.js +39 -70
  114. package/dist/validate/validate.js.map +1 -1
  115. package/dist/workflow/automated-controls.d.ts +63 -0
  116. package/dist/workflow/automated-controls.js +158 -0
  117. package/dist/workflow/automated-controls.js.map +1 -0
  118. package/dist/workflow/automated.d.ts +112 -0
  119. package/dist/workflow/automated.js +163 -0
  120. package/dist/workflow/automated.js.map +1 -0
  121. package/dist/workflow/code-state.d.ts +46 -0
  122. package/dist/workflow/code-state.js +26 -0
  123. package/dist/workflow/code-state.js.map +1 -0
  124. package/dist/workflow/control-tools.d.ts +54 -23
  125. package/dist/workflow/control-tools.js +99 -79
  126. package/dist/workflow/control-tools.js.map +1 -1
  127. package/dist/workflow/file-values.d.ts +4 -4
  128. package/dist/workflow/file-values.js +1 -1
  129. package/dist/workflow/file-values.js.map +1 -1
  130. package/dist/workflow/governance.d.ts +11 -22
  131. package/dist/workflow/governance.js +47 -104
  132. package/dist/workflow/governance.js.map +1 -1
  133. package/dist/workflow/grader-state.d.ts +55 -0
  134. package/dist/workflow/grader-state.js +182 -0
  135. package/dist/workflow/grader-state.js.map +1 -0
  136. package/dist/workflow/history.d.ts +11 -0
  137. package/dist/{lifecycle/transcript.js → workflow/history.js} +24 -8
  138. package/dist/workflow/history.js.map +1 -0
  139. package/dist/workflow/middleware.d.ts +14 -17
  140. package/dist/workflow/middleware.js +109 -95
  141. package/dist/workflow/middleware.js.map +1 -1
  142. package/dist/workflow/move-events.d.ts +21 -0
  143. package/dist/workflow/move-events.js +20 -0
  144. package/dist/workflow/move-events.js.map +1 -0
  145. package/dist/workflow/on-error.js +0 -2
  146. package/dist/workflow/on-error.js.map +1 -1
  147. package/dist/workflow/parks.d.ts +9 -2
  148. package/dist/workflow/parks.js +16 -10
  149. package/dist/workflow/parks.js.map +1 -1
  150. package/dist/workflow/paths.d.ts +9 -9
  151. package/dist/workflow/paths.js +19 -19
  152. package/dist/workflow/paths.js.map +1 -1
  153. package/dist/workflow/render-prompt.d.ts +7 -5
  154. package/dist/workflow/render-prompt.js +22 -37
  155. package/dist/workflow/render-prompt.js.map +1 -1
  156. package/dist/workflow/signature-checks.d.ts +2 -2
  157. package/dist/workflow/snapshot.js +5 -29
  158. package/dist/workflow/snapshot.js.map +1 -1
  159. package/dist/workflow/state.d.ts +14 -42
  160. package/dist/workflow/state.js +20 -33
  161. package/dist/workflow/state.js.map +1 -1
  162. package/dist/workflow/sub-workflow.d.ts +3 -3
  163. package/dist/workflow/sub-workflow.js +3 -7
  164. package/dist/workflow/sub-workflow.js.map +1 -1
  165. package/dist/workflow/tool-service.d.ts +1 -9
  166. package/dist/workflow/tool-service.js +21 -24
  167. package/dist/workflow/tool-service.js.map +1 -1
  168. package/dist/workflow/turn-boundary.d.ts +2 -2
  169. package/dist/workflow/turn-boundary.js +10 -15
  170. package/dist/workflow/turn-boundary.js.map +1 -1
  171. package/dist/workflow/turn-runner.js +5 -6
  172. package/dist/workflow/turn-runner.js.map +1 -1
  173. package/package.json +2 -2
  174. package/skills/archmax-harness/README.md +5 -5
  175. package/skills/archmax-harness/SKILL.md +72 -30
  176. package/skills/archmax-harness/references/backend-integration.md +33 -24
  177. package/skills/archmax-harness/references/cases.md +41 -8
  178. package/skills/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +599 -339
  179. package/skills/archmax-harness/references/code-states-and-scripts.md +115 -0
  180. package/skills/archmax-harness/references/diagnostics.md +40 -7
  181. package/skills/archmax-harness/references/wiring.md +19 -10
  182. package/skills/archmax-harness/references/workflow-schema.md +404 -293
  183. package/skills/archmax-harness/references/workflow-yaml.md +151 -62
  184. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +0 -89
  185. package/dist/lifecycle/hook-shape.d.ts +0 -44
  186. package/dist/lifecycle/hook-shape.js +0 -66
  187. package/dist/lifecycle/hook-shape.js.map +0 -1
  188. package/dist/lifecycle/runner.d.ts +0 -197
  189. package/dist/lifecycle/runner.js +0 -419
  190. package/dist/lifecycle/runner.js.map +0 -1
  191. package/dist/lifecycle/tool-calls.d.ts +0 -9
  192. package/dist/lifecycle/tool-calls.js +0 -57
  193. package/dist/lifecycle/tool-calls.js.map +0 -1
  194. package/dist/lifecycle/transcript.d.ts +0 -31
  195. package/dist/lifecycle/transcript.js.map +0 -1
  196. package/dist/rubrics/rubrics.d.ts +0 -135
  197. package/dist/rubrics/rubrics.js +0 -145
  198. package/dist/rubrics/rubrics.js.map +0 -1
  199. package/dist/sandbox/assets/parts/core.js +0 -22
  200. package/dist/sandbox/assets/parts/lifecycle-hook.js +0 -47
  201. package/dist/sandbox/bridge.d.ts +0 -16
  202. package/dist/sandbox/bridge.js +0 -95
  203. package/dist/sandbox/bridge.js.map +0 -1
  204. package/dist/workflow/visits.d.ts +0 -21
  205. package/dist/workflow/visits.js +0 -45
  206. package/dist/workflow/visits.js.map +0 -1
  207. package/skills/archmax-harness/references/hooks-and-scripts.md +0 -89
@@ -2,7 +2,7 @@
2
2
 
3
3
  The complete, field-by-field reference for authoring a `workflow.yaml` (and its
4
4
  optional `WORKFLOW.md` prose addendum) plus the surrounding workspace. Targets
5
- **runtime contract version `"2"`**.
5
+ **runtime contract version `"3"`**.
6
6
 
7
7
  ## Contents
8
8
 
@@ -14,9 +14,9 @@ optional `WORKFLOW.md` prose addendum) plus the surrounding workspace. Targets
14
14
  - [Tool governance](#tool-governance)
15
15
  - [Skill governance](#skill-governance)
16
16
  - [Mount governance](#mount-governance)
17
- - [Lifecycle hooks](#lifecycle-hooks)
17
+ - [Grader and code states](#grader-and-code-states)
18
18
  — full script contract + worked examples in
19
- [`hook-and-test-scripts.md`](hook-and-test-scripts.md)
19
+ [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md)
20
20
  - [Budgets & error routing](#budgets--error-routing)
21
21
  - [Human states](#human-states)
22
22
  - [Sub-workflows are tools](#sub-workflows-are-tools)
@@ -36,8 +36,8 @@ optional `WORKFLOW.md` prose addendum) plus the surrounding workspace. Targets
36
36
  - **`workflow.yaml`** — the canonical, enforced machine spec: a **pure YAML
37
37
  mapping** (no `---` frontmatter fences, no markdown). Parsed with YAML 1.2
38
38
  semantics and validated against the schema. Top-level keys: `title`,
39
- `instructions`, `runtime`, `settings`, `tests`, `tools`, `skills`, `mounts`,
40
- `extensions`, `editor`, `states`.
39
+ `instructions`, `disabled`, `runtime`, `settings`, `tests`, `tools`, `skills`,
40
+ `mounts`, `metadata`, `states`.
41
41
  - **`WORKFLOW.md`** (optional, beside it) — a plain-markdown prose addendum with
42
42
  **no frontmatter**. A frontmatter block in `WORKFLOW.md` when `workflow.yaml`
43
43
  exists is a **validation error**. The runtime renders the system prompt's
@@ -91,7 +91,7 @@ disabled?: boolean # `true` = this machine STARTS NO NEW TURNS. Bound
91
91
  # (a new turn is new work) — all rejected AT THE TURN
92
92
  # BOUNDARY with `status: rejected` and
93
93
  # "Refusing to start: workflow '<slug>' is disabled.",
94
- # before any state, hook or model call; and a caller's
94
+ # before any state or model call; and a caller's
95
95
  # `archmax_workflow_<slug>` call (blocked call, refusal
96
96
  # kind `disabled`, before a child is composed — it also
97
97
  # beats a declared MOCK, so a case can never assert a
@@ -113,18 +113,17 @@ disabled?: boolean # `true` = this machine STARTS NO NEW TURNS. Bound
113
113
 
114
114
  runtime?:
115
115
  engine?: string # "archmax-harness"
116
- version?: string | number # target contract — use "2"
117
-
118
- budget?:
119
- maxModelCalls?: number # default 50 — model calls ONE TURN may make across EVERY
120
- # state (a turn = one message, decision, reply or delivery;
121
- # each counts from zero). A state's maxTurns restarts on
122
- # every entry, so only this ends an agent cycling between
123
- # states. Exhaustion routes once through the current
124
- # state's on_error, else the turn ends rejected; the agent
125
- # is warned once a fifth is left. The step limit follows
126
- # it. RAISE it for turns that legitimately run long (a long
127
- # state chain, a self-driven review cycle).
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).
128
127
 
129
128
  settings?:
130
129
  model?: string # MODEL ID ONLY — the id every state of this workflow runs
@@ -133,8 +132,12 @@ settings?:
133
132
  # it. NOT a block: `{ id, temperature }` is a load error —
134
133
  # sampling and endpoint are env/host configuration.
135
134
  # Opaque: nothing validates the id against a list.
136
- # Does NOT change the case grader (tests.judge.model).
137
- timeoutMs?: number # default 15000; per eval/lifecycle script (sandbox budget)
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
138
141
  memoryLimitBytes?: number
139
142
  maxPtcCalls?: number | null
140
143
  maxResultChars?: number
@@ -150,8 +153,9 @@ tests?: # cases suite config (declarative data;
150
153
  caseTimeoutMs?: number # per-case WALL-CLOCK budget, default 120000 —
151
154
  # deliberately named differently from
152
155
  # settings.timeoutMs (per-script sandbox budget)
153
- judge?: # {} = enable the grader with the
154
- model?: string # env-configured default model
156
+ judge?: # {} = enable the case judge (`grade:`) with the
157
+ model?: string # env-configured default model — unrelated to
158
+ # grader states
155
159
  modelOptions?: { temperature?: number, maxTokens?: number }
156
160
  # applied over the env model config, via the
157
161
  # factory's `judge` role; other keys are errors
@@ -223,20 +227,15 @@ mounts?:
223
227
  # validate ERRORS on a name the table does not mount.
224
228
  forbid_always?: string[] # MOUNT NAMES out of reach of EVERY state, governed or
225
229
  # not, whatever any list grants. Deny beats allow.
226
- # Binds this workflow's own HOOKS too, and is
227
- # inherited by every sub-workflow, naming the workflow
230
+ # Binds this workflow's grader and code states too,
231
+ # and is inherited by every sub-workflow, naming the workflow
228
232
  # that declared it. A NAME only — a denial is total, so
229
233
  # there is no `access` to qualify. (A root `mounts.allow`
230
234
  # is a load error: one mounts model.)
231
235
 
232
- extensions?:
233
- hooks?: string[] # custom hook kinds (beyond script/rubric) the
234
- # runtime registers executors for; undeclared
235
- # unknown hook kinds are validation errors
236
-
237
- editor?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
238
- # UI's own state (canvas state positions keyed by
239
- # slug, edge routing). The one key the runtime
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
240
239
  # never reads: not shape-checked, not prompted,
241
240
  # not governed, and excluded from the spec hash.
242
241
  # Do NOT author it by hand and do NOT invent a
@@ -244,7 +243,8 @@ editor?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
244
243
  # rewrite a spec that already carries one, or the
245
244
  # builder that wrote it loses every state position.
246
245
  # The root is otherwise STRICT: any other
247
- # unrecognized key is a load error.
246
+ # unrecognized key is a load error — `extensions:`
247
+ # included.
248
248
 
249
249
  states: Record<slug, MachineState> # REQUIRED; ≥1 state, ≥1 with a trigger
250
250
  # the KEY is the state's SLUG (see below)
@@ -277,7 +277,7 @@ states:
277
277
  # behavior. Name here anything a downstream guard
278
278
  # reads.
279
279
  locks?: [string] # RUN VARIABLES this state OWNS: only it may write them,
280
- # on any visit, before or after they are set; a write
280
+ # whenever it is active, before or after they are set; a write
281
281
  # from another state is refused naming the owner. To
282
282
  # change one (the person meant another order), the
283
283
  # session goes BACK to the owner, whose rules apply
@@ -296,11 +296,26 @@ states:
296
296
  # NOT a substitute for `instructions` (behavior
297
297
  # guidance) or a transition `description` (routing).
298
298
 
299
- type?: "agent" | "human"
300
- # default "agent". "human" = a person routes.
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.
301
308
  # (`workflow` was removed: a state CALLS another
302
309
  # workflow by allowing `archmax_workflow_<slug>`.)
303
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
+
304
319
 
305
320
  triggers?: Record<triggerId, TriggerDeclaration | null>
306
321
  # marks a start state. The KEY is the trigger id
@@ -346,16 +361,19 @@ states:
346
361
  # target's summary is never disclosed.
347
362
 
348
363
  instructions?: string # behavior guidance.
349
- # agent node: surfaced to the LLM while active.
364
+ # agent state: surfaced to the LLM while active.
350
365
  # Write the state's behavior INLINE here; a
351
366
  # skill bundle (skills/<slug>/) is a capability
352
367
  # with data or scripts, enabled by slug — not a
353
368
  # per-state instruction sheet.
354
- # human state: describes the decision for the packet
355
-
356
- before?: HookSpec # gate ENTRY to this state (fail-closed)
357
- after?: HookSpec # gate the archmax_advance OUT (or run at completion
358
- # for a terminal state)
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).
359
377
 
360
378
  tools?:
361
379
  allow?: AllowEntry[] # grants beyond the workflow-wide grant (plus the
@@ -366,8 +384,8 @@ states:
366
384
  forbid?: ForbidEntry[] # what this state DENIES while active — beating the
367
385
  # workflow-wide grant, this state's own allow,
368
386
  # and a host's essentialTools alike. Deny beats allow.
369
- # Not inherited by a sub-workflow; a lifecycle hook
370
- # is exempt from it (runtime authority).
387
+ # Not inherited by a sub-workflow. Binds a grader's
388
+ # and a code state's calls exactly as the agent's.
371
389
 
372
390
  skills?:
373
391
  allow?: string[] # which CAPABILITIES this state ADDS, by SLUG — on top
@@ -389,11 +407,11 @@ states:
389
407
  # listing of skills/ does not show it.
390
408
  # A tools.allow entry still NARROWS within the enabled
391
409
  # set: the two compose as enabled AND allowed.
392
- # Lifecycle HOOKS are exempt — a `before` hook reading
393
- # skills/order-data/assets/orders.json still works in a
394
- # state that enables nothing, so you can gate the
395
- # agent's use of a capability while keeping the hook
396
- # that enforces it.
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.
397
415
 
398
416
  mounts?:
399
417
  allow?: MountGrant[] # which MOUNTS this state ADDS, by NAME — on top of the
@@ -422,27 +440,32 @@ states:
422
440
  # every declaration) AND undisclosed — absent from the
423
441
  # volatile prompt block, and filtered out of every
424
442
  # ls/glob/grep result, the WORKSPACE ROOT LISTING
425
- # included. Lifecycle HOOKS are exempt from this key
426
- # (runtime authority), but not from `forbid_always`
427
- # nor from the WORKFLOW's grants: a hook reaches a
428
- # governed mount only if allow_always or SOME state's
429
- # allow names it, in any state, and writes it only
430
- # where a grant of the workflow asks `read_write`.
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.
431
446
 
432
447
  model?: string # MODEL ID ONLY — this state's turns run on it (and the
433
448
  # handoff message of a park it holds). Most specific of
434
449
  # state.model > settings.model > the assembly's default
435
450
  # (createAgent `model`/`modelFactory`, else ARCHMAX_MODEL).
436
451
  # Same id as settings.model ⇒ validate WARNS (inert).
437
- # A rubric keeps its own `model`; a sub-workflow resolves
438
- # its OWN spec's ids — nothing is inherited across it.
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.
439
457
  # A host passing an explicit `model` INSTANCE to
440
458
  # createAgent outranks every declared id (one assembly
441
459
  # warning names them); `modelFactory` is what honours one.
442
460
 
443
461
  budget?: # per-state execution budget
444
- maxTurns?: number # bounds this turn's agent loop
445
- timeoutMs?: number # aborts the turn when exceeded
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.
446
469
  on_error?: string # state to route to on a TERMINAL failure
447
470
  # (see Budgets & error routing)
448
471
 
@@ -458,20 +481,24 @@ states:
458
481
  #
459
482
  # THE AGENT SEES THIS STATE'S EDGES AND NOTHING
460
483
  # ELSE OF THE GRAPH — no other state's slug, title,
461
- # summary, hooks or transitions, and no count of
462
- # the states. So `description` is what it knows
463
- # about where an edge leads. An UNDESCRIBED edge
464
- # (absent/empty/blank) routes on the TARGET's
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
465
488
  # `instructions` instead: the edge's line carries
466
489
  # them, quoted, and nothing else of the target.
467
490
  # LOAD ERRORS (states.<slug>.transitions.<i>.description):
468
491
  # an undescribed edge whose target has no
469
492
  # instructions; an undescribed transition of a
470
- # HUMAN state (it is the button label); a
471
- # non-string. An undescribed edge INTO a human
472
- # state discloses that state's reviewer-facing
473
- # instructions to the agent — describe it to keep
474
- # them private.
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.
475
502
  #
476
503
  # Write it for a reader standing in THIS state,
477
504
  # which knows nothing about the target:
@@ -483,10 +510,15 @@ states:
483
510
  # undescribed.
484
511
  #
485
512
  # The rendered line marks a target that is a human
486
- # decision node or terminal (each changes the call
487
- # or what follows it); nothing else of the target.
488
-
489
- # human states only (ignored on agent states):
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):
490
522
  evidence?: string[] # workspace-relative paths always shown in the packet —
491
523
  # a baseline the advancing agent adds to (see Human states)
492
524
  approvers?: string[] # informational; not enforced
@@ -599,8 +631,9 @@ states:
599
631
  overwrite a value the session already holds. So do not list a return in a
600
632
  state's `requires:` unless that state must settle it, and do not tell the
601
633
  agent to lock one before its value is final. `note` is reserved: a `returns`
602
- list may not declare it. Want the model to try again? Check the returns in a terminal
603
- `after` hook, which can `correct`. Each entry is a bare
634
+ list may not declare it. Want the model to try again? Route the finishing
635
+ state through a code state that checks the returns and sends the work back
636
+ with what is missing. Each entry is a bare
604
637
  run-variable name (**untyped**: any value, `null` included) or a strict object
605
638
  `{ name, type?, description? }` naming one; the spellings mix in one list and
606
639
  names are distinct across both. What the agent must *do* to produce a
@@ -621,14 +654,15 @@ states:
621
654
  refused before any model call), **at the write** (`archmax_set_variables` of a
622
655
  mistyped typed return of the current trigger is a correctable refusal,
623
656
  nothing written) and **at completion** (a mistyped typed return rejects the
624
- run — in practice a script or hook write). There are no enums, item or
657
+ run — in practice a code state's write). There are no enums, item or
625
658
  property schemas, ranges or optional inputs. An unknown `type`, an unknown
626
659
  entry key (`default:`) or an empty `description` fails load.
627
660
  - **A `file` value must name a readable file.** The pure rule checks only the
628
661
  path's shape (a host runs it in a browser). At the start, at the write, at
629
662
  completion and at a delegation call the runtime also requires a **file** (not a
630
663
  folder) at the path that the session may read **on runtime authority** — the
631
- rules a hook is bound by: zone rules (`checkpoints/`, `artifacts/`, `_specs/`
664
+ runtime's own `file`-value read check, the one call that runs outside a state's
665
+ surface: zone rules (`checkpoints/`, `artifacts/`, `_specs/`
632
666
  never), the workflow's mount grants taken across all its states (a governed
633
667
  mount no state is granted is out of reach), and every workflow-wide
634
668
  `forbid_always`, inherited ones included; the state's allow list and its own
@@ -684,8 +718,8 @@ states:
684
718
  connection: acme-slack
685
719
  ```
686
720
 
687
- One state, one behavior, one governance block; `${{trigger}}` and a hook's
688
- `args.trigger` still read which one actually fired.
721
+ One state, one behavior, one governance block; `${{trigger}}` and a code
722
+ state's `trigger` input still read which one actually fired.
689
723
  - Every declared trigger **starts** a session. An event that must only ever reach
690
724
  a live one — a ticket closing, a payment settling — is declared nowhere: the
691
725
  host names the session it delivers into (`archmax deliver <session> --trigger
@@ -701,7 +735,8 @@ along the chosen edge, a delivery resumes the parked state.
701
735
 
702
736
  **Runtime control tools** carry the `archmax_` prefix — `archmax_advance`,
703
737
  `archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_run`,
704
- `archmax_get_variables`, `archmax_set_variables` — and are the names to write in `tools.allow`,
738
+ `archmax_get_variables`, `archmax_set_variables`, and `archmax_get_history`
739
+ (grader states only) — and are the names to write in `tools.allow`,
705
740
  `tools.allow_always`, and the `forbid`/`forbid_always` lists. These are the only spellings:
706
741
  an entry naming an unprefixed variant governs a tool that does not exist,
707
742
  silently granting nothing. A host tool may never claim the prefix. Deep Agents' built-ins
@@ -712,7 +747,9 @@ silently granting nothing. A host tool may never claim the prefix. Deep Agents'
712
747
  `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
713
748
  `archmax_eval`, `archmax_run`) when the key is absent, plus the always-allowed
714
749
  `archmax_advance` and a host's `essentialTools`, minus whatever a `forbid` list
715
- denies there, and read-only-zone protection.
750
+ denies there, and read-only-zone protection. A grader or code state never has
751
+ `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or `archmax_reset`,
752
+ even when `allow_always` lists one; a grader also has `archmax_get_history`.
716
753
  Governance is closed by default; there is no way to declare a fully open state.
717
754
  - `tools.forbid` on a state **denies** while that state is active, beating every
718
755
  grant that would otherwise reach it (see [Tool governance](#tool-governance)).
@@ -730,8 +767,8 @@ success.
730
767
  - The call must be the **only** tool call in its message; one sent beside other
731
768
  calls is refused, and the model may raise again on its own.
732
769
  - The session ends **at once** with status `failed` (classified **finished**): no
733
- further model call, no `on_error` route, no `before`/`after` hook (a terminal
734
- state's included), no check of the trigger's `returns`. A rejection still
770
+ further model call, no `on_error` route, no further state (an automated one
771
+ included), no check of the trigger's `returns`. A rejection still
735
772
  pending from earlier in the turn is replaced. The position stays where the
736
773
  agent raised; the trail records no step.
737
774
  - The host reads `Outcome.kind === "failed"` and `Outcome.exit`
@@ -745,9 +782,11 @@ success.
745
782
  its own `on_error`, or raise in turn.
746
783
  - Ban it with `tools: { forbid_always: [archmax_raise] }` (or one state's
747
784
  `tools.forbid`) where the agent must not end the run itself — say, a state that
748
- must always reach a person instead. Scripts and hooks cannot call it.
785
+ must always reach a person instead. An agent's script cannot call it; a code
786
+ state's script (`tools.archmax_raise`) and a grader can, and their raise ends the
787
+ session the same way (the note and `exit` carry their code).
749
788
  - Test it with the structural `raised` assertion; `succeeded: true` fails on a
750
- raise.
789
+ raise (it passes only on a `completed` outcome).
751
790
 
752
791
  ## Transitions
753
792
 
@@ -755,12 +794,17 @@ success.
755
794
  transitions:
756
795
  - to: string # REQUIRED target state SLUG (must exist); never a title
757
796
  description?: string # shown to the agent (or human) to pick the edge; absent =
758
- # the agent is shown the target's instructions instead
797
+ # the agent is shown the target's instructions instead.
798
+ # REQUIRED on a human state's edges and on an edge INTO
799
+ # a grader or code state; a code state's own may omit it
759
800
  type?: "approve" | "reject" | "refine" | "none" # default "none"
760
801
  ```
761
802
 
762
803
  - On **agent** states the agent chooses one edge via
763
804
  `archmax_advance({ to, reason })`; `type` is metadata only.
805
+ - On **grader** and **code** states the grader or the script chooses one the same
806
+ way (`archmax_advance` / `tools.archmax_advance`), and its `reason` is what the
807
+ next state reads.
764
808
  - On **human** states each transition is a selectable outcome (a UI renders one
765
809
  button per edge, labeled by `type`). `refine` conventionally loops back to a
766
810
  prior state with the reviewer's comment as correction. Max one transition per
@@ -796,16 +840,19 @@ declare that key, the **default tools** (`ls`, `read_file`, `write_file`, `edit_
796
840
  `archmax_run`). A declared list **replaces** the default tools, never adds to them:
797
841
  `allow_always: []` grants no tool workflow-wide, and a list without `write_file` leaves
798
842
  every state that does not name it unable to write. So a new workflow lists all twelve and
799
- removes only what it must not have. Every state also has the always-allowed controls and
800
- any tool the host passes as `essentialTools`, whatever the workflow declares. One built-in is **never
801
- disclosed** and grantable by nothing: `task`, the runtime's own dispatch for
802
- grading rubrics — a rubric grades the agent rather than serving it, so naming
843
+ removes only what it must not have. Every state also has its type's control tools and
844
+ any tool the host passes as `essentialTools`, whatever the workflow declares. A grader or
845
+ code state is never offered `write_todos`, `archmax_eval`, `archmax_run`, `archmax_wait` or
846
+ `archmax_reset`, even when `allow_always` lists them, and naming one in its own `tools.allow`
847
+ is a `validate` error.
848
+ One built-in is **never disclosed** and grantable by nothing: `task`, Deep Agents'
849
+ subagent dispatch — grading is a grader state and delegating is a sub-workflow, so naming
803
850
  `task` in an allow entry is a `validate` error and the kernel refuses the call.
804
851
  The planning scratchpad `write_todos` and the sandbox
805
852
  `archmax_eval` are default tools: granted wherever the workflow-wide grant includes
806
853
  them (everywhere, in a workflow without `allow_always`). `write_todos`'s prompt
807
- guidance appears only when some state is granted it, and hooks run in the sandbox
808
- whether or not the agent is granted `archmax_eval`. The interpreter is safe as a
854
+ guidance appears only when some state is granted it, and code states run in the
855
+ sandbox whether or not any state is granted `archmax_eval`. The interpreter is safe as a
809
856
  default because the `tools.*` calls its code makes are decided against the active
810
857
  state by the same kernel: it adds computation over what the state permits, not
811
858
  access. `archmax_run` — executing an authored script **file** — is a default tool on
@@ -839,10 +886,11 @@ entries: `tools.forbid_always` at the root denies in every state,
839
886
  `tools.forbid` on a state denies while it is active — subtracting from the
840
887
  workflow-wide grant, the state's own `allow` and a host's `essentialTools` alike.
841
888
  **Deny beats allow, and no narrower level widens a denial.** A forbid entry may
842
- name the tool `*` (every tool); `*` in an allow list is a load error. A lifecycle
843
- hook is exempt from the state's `allow` **and** its `forbid` — runtime authority,
844
- outside that surface both ways — but never from `forbid_always` (the workflow's
845
- own `tools.`, `skills.` and `mounts.forbid_always`, and every ancestor's).
889
+ name the tool `*` (every tool); `*` in an allow list is a load error. A grader's
890
+ calls and a code state's script are governed against **their own state** exactly
891
+ as the agent is — its `allow`, its `forbid`, `forbid_always` and every ancestor's.
892
+ The only call on runtime authority is the runtime's own `file`-value read check,
893
+ held to the workflow's grants across all its states and to every `forbid_always`.
846
894
 
847
895
  Precedence (highest first):
848
896
 
@@ -856,20 +904,27 @@ Precedence (highest first):
856
904
  `tools.forbid` denies in the active state, both regardless of any allow list
857
905
  (and regardless of always-allowed): deny beats allow, and no narrower level
858
906
  widens a denial.
859
- 2. **Always allowed**: `archmax_advance` (baked in, every state).
907
+ 2. **The state type's control tools** (baked in): an agent or human state has
908
+ `archmax_advance`, `archmax_reset`, `archmax_wait`, `archmax_raise` and the
909
+ variable tools; a code state `archmax_advance`, `archmax_raise` and the
910
+ variable tools; a grader state those plus `archmax_get_history`.
860
911
  3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for a
861
912
  tool the workflow-wide grant includes takes precedence (**narrower wins** —
862
913
  the state's arg globs apply, not the broader grant); an entry for a default
863
914
  tool a declared `allow_always` omits grants it in this state.
864
915
  4. **The workflow-wide grant** — the declared `tools.allow_always`, guards
865
- included, or the default tools, unconstrained, when the key is absent.
916
+ included, or the default tools, unconstrained, when the key is absent; in a
917
+ grader or code state, minus `write_todos`, `archmax_eval` and `archmax_run`.
866
918
  5. **The host's `essentialTools`** — always permitted when no state entry
867
919
  narrows them, whatever the workflow declares.
868
920
  6. **Default**: everything else is blocked.
869
921
 
870
922
  **Progressive disclosure.** The model is only shown the active state's tool
871
- surface: the workflow-wide grant + a host's `essentialTools` + the state's entries + `archmax_advance`
872
- (hidden in terminal states), minus every tool denied there by name. Disclosure is
923
+ surface: the workflow-wide grant + a host's `essentialTools` + the state's entries +
924
+ its control tools (`archmax_advance` hidden in terminal states), minus every tool
925
+ denied there by name. A grader is offered, and a code state's script reaches,
926
+ exactly the same derivation for its own state — anything else is absent
927
+ (`undefined` on `tools`). Disclosure is
873
928
  name-level (arg-constrained tools stay visible; the kernel enforces args at
874
929
  call time) and never replaces enforcement. **Skills are disclosed the same way**:
875
930
  each model call names only the skills the active state enables, with their
@@ -923,7 +978,7 @@ Why by slug, and not with `tools.allow` paths:
923
978
  Capability governance and execution governance are different questions.
924
979
  - **`archmax_run` can only ever be narrowed.** A non-overridable kernel rule
925
980
  (`script.skill-only`) confines it to skill bundles, so a script the agent writes
926
- into `scratchpad/` is unrunnable, a lifecycle hook script is unrunnable, and
981
+ into `scratchpad/` is unrunnable, a code-state script is unrunnable, and
927
982
  `{ tool: archmax_run, paths: ["**"] }` grants nothing — `validate` reports such
928
983
  an entry as inert.
929
984
 
@@ -939,11 +994,12 @@ that serves bundles, naming what the workflow cannot reach. Any root block silen
939
994
  that last one — write `skills: { allow_always: [] }` when a workflow grants no
940
995
  capability workflow-wide.
941
996
 
942
- Hooks are **exempt**: a `before`/`after` script reads authored data on runtime
943
- authority, so a state may enable nothing and still be gated by a hook that reads
944
- the very bundle the model cannot. That is the shape to reach for when a capability
945
- should *enforce* something without being *usable*. A bundle the workflow's
946
- `skills.forbid_always` denies is refused to its hooks too.
997
+ Grader and code states are **not exempt**: their calls are governed against their
998
+ own state, so a code state that reads a bundle enables it in its own
999
+ `skills.allow`. That is the shape to reach for when a capability should *enforce*
1000
+ something without being *usable* by the agent: the agent's state enables nothing,
1001
+ and a code state in front of or behind it enables the bundle and runs the check. A
1002
+ bundle the workflow's `skills.forbid_always` denies is refused to every state.
947
1003
 
948
1004
  **`tools.forbid` on a state denies there**, and `tools.forbid_always` at the
949
1005
  root denies everywhere. Both beat every grant. Migrating a workflow off the
@@ -1083,20 +1139,13 @@ so `AGENTS.md.bak` is not captured.
1083
1139
  governs mounts, naming what the workflow cannot reach. Any root block silences
1084
1140
  it — write `mounts: { allow_always: [] }` when a workflow reaches none.
1085
1141
 
1086
- Hooks and rubric graders are bound by `mounts.forbid_always` and **not** by a
1087
- state's `forbid` or its enabled set: they run on runtime authority, exactly as for
1088
- tools, so a `before` hook can read a mount the model in that state cannot. A hook
1089
- is still held to its **workflow's** grants, taken across every state:
1090
-
1091
- - it reads a governed mount only if `allow_always` or some state's `allow` names
1092
- it, else `mount.not-allowed`. The table serves every child too, so it can hold a
1093
- mount only a child grants, and the caller's hooks cannot reach that;
1094
- - it writes one only where a grant of the workflow asks `access: read_write`, and an
1095
- `access: read` in `allow_always` narrows any mount for it (`mount.read-only`).
1096
-
1097
- So when a hook needs a mount no state's model should see, grant it in a state the
1098
- hook's workflow has anyway, or in `allow_always` with the model's states
1099
- forbidding it.
1142
+ Grader and code states are bound by **their own state's** mount grants, exactly
1143
+ like the agent: a code state reads a governed mount only where `allow_always` or
1144
+ its own `mounts.allow` grants it, and writes one only where that grant asks
1145
+ `access: read_write`. So when a check needs a mount no agent state should see,
1146
+ grant it on the check's code state alone. The one read on runtime authority is
1147
+ the runtime's own `file`-value check, held to the **workflow's** grants taken
1148
+ across every state and to every `forbid_always`.
1100
1149
 
1101
1150
  ## Session variables in guards
1102
1151
 
@@ -1224,89 +1273,149 @@ the tool's effect instead (the file it wrote, the variable it set).
1224
1273
  To write the characters `${{…}}` literally — authoring a guard through
1225
1274
  `write_file`, or writing about the syntax — double the dollar: `$${{name}}`.
1226
1275
 
1227
- This is **model-facing only**. An `archmax_eval` or hook script already holds the
1228
- run's variables as `args.variables` and composes strings in JavaScript, so
1229
- `${{…}}` in a script's `tools.*` call is ordinary text.
1276
+ This is **model-facing**: the agent's calls and a grader's get it. An
1277
+ `archmax_run` script (`args.variables`) or a code state's script (`variables`)
1278
+ already holds the run's variables and composes strings in JavaScript, so `${{…}}`
1279
+ in a script's `tools.*` call is ordinary text.
1230
1280
 
1231
- ## Lifecycle hooks
1281
+ ## Grader and code states
1232
1282
 
1233
- ```yaml
1234
- # single hooks (the yaml wires the hook, nothing more):
1235
- before:
1236
- script: hooks/check.js # hand-authored, self-contained JS file;
1237
- # resolves to workflows/<slug>/hooks/check.js
1238
- after: { rubric: reply-tone, max_iterations: 2 } # per hook, not per state
1239
- # a script after hook may carry the same budget:
1240
- # after: { script: hooks/review.js, max_iterations: 2 }
1241
-
1242
- # or a list, run in sequence (short-circuits on veto):
1243
- before:
1244
- - { script: hooks/pre.js }
1245
- - rubric: { instructions: judge the tone }
1246
- ```
1283
+ Two state types are run by the runtime itself, together called **automated
1284
+ states**. Both are ordinary states on the graph: entered by a transition, leaving
1285
+ by `archmax_advance` along one of their own transitions, ending the session with
1286
+ `archmax_raise`, governed by their own tool surface.
1247
1287
 
1248
- - A `HookSpec` is one hook or a list, run in order and short-circuiting on the
1249
- first non-ok. Each hook is either `{ script: <path> }` (JS in the QuickJS
1250
- sandbox) or `{ rubric: <declaration> }` (the grader itself, inline).
1251
- - **A script hook's path is workflow-relative and lives under `hooks/`.**
1252
- `{ script: hooks/check.js }` resolves to `workflows/<slug>/hooks/check.js` on the
1253
- authoring plane. An absolute path, a `..` climb, or anything outside `hooks/`
1254
- (including the former `skills/<name>/scripts/…` spelling) is a load error and a
1255
- `validate` error. Hook scripts are **not** agent-readable and **not** runnable
1256
- with `archmax_run`: the runtime runs them, and that separation is what stops an
1257
- agent probing the guard that gates it.
1258
- - **Reserved sidecar key** on a script or custom hook: `max_iterations` (see
1259
- Verdicts below). It is **refused beside a `rubric`**, whose budget is inside its
1260
- declaration. There is no `instructions`, `model` or `specification` sidecar on
1261
- any hook.
1262
- - **Script hooks are hand-authored.** The script is an ordinary,
1263
- self-contained `.js` file you design and write directly against the sandbox
1264
- contract; it **must** open with a leading JSDoc title + description (plain
1265
- prose, no special tags) — see
1266
- [Code description docblocks](#code-description-docblocks).
1267
- `archmax validate` errors on a missing script file or a
1268
- foreign (non-`@archmax-ai/harness/*`) import; it does not check the docblock on a
1269
- hook script. The runtime never generates: a missing
1270
- script at run time vetoes (fail-closed).
1271
- (A rubric's contract is its own `instructions`.)
1272
- - **Phases**: `before` runs before the agent acts in the state and gates entry;
1273
- `after` runs after the agent calls `archmax_advance`, before the transition
1274
- commits (the "veto window"), or at completion for a terminal state.
1275
- - **Verdicts**: `ok` (proceed), `correct` (agent gets another attempt), `veto`
1276
- (block). Return the vocabulary or nothing: a bare `false` is **not** a veto, it
1277
- is no verdict, which fails closed. Both hook kinds
1278
- can return `correct` from an `after` hook — a script by evaluating to
1279
- `{ verdict: "correct", reason: "..." }`, a rubric in its response —
1280
- and both get the same bounded flow.
1281
- - **`max_iterations`** bounds how many times a `correct` may send the agent back
1282
- before the hook becomes a hard veto: inside the declaration for a rubric, a
1283
- sidecar for a script or custom hook, else 0. It is counted **per hook by
1284
- position**, so two graders on one state never share a budget. An
1285
- exhausted budget is a *terminal failure* — see
1286
- [Budgets & error routing](#budgets--error-routing).
1287
- - **`before` hooks are ok/veto only**: a `correct` verdict from a `before` hook
1288
- is treated as veto, and `max_iterations` on a `before` hook is a validation
1289
- error.
1290
- - **Fail-closed**: a hook that *errors* vetoes the transition. A rubric
1291
- whose response cannot be parsed into a verdict also fails closed (**veto**),
1292
- never coerced to ok — as does a `model` id the host cannot serve.
1293
-
1294
- The hook contract — a default-export function called with one object:
1295
-
1296
- ```js
1297
- export default async function hook({
1298
- state, phase /* "before"|"after" */, trigger, variables, messages,
1299
- from, to, reason, // from/to/reason present on "after"
1300
- tools, // privileged tool bridge, e.g. tools.readFile
1301
- }) {
1302
- return ok(); // or veto(reason) / correct(reason)
1303
- }
1288
+ ```yaml
1289
+ states:
1290
+ check-refund: # CODE state: runs its script, makes no model call
1291
+ type: code
1292
+ script: scripts/check-refund.js
1293
+ skills: { allow: [order-data] } # what the SCRIPT reads is granted HERE
1294
+ on_error: escalation
1295
+ transitions:
1296
+ - to: refund-review
1297
+ description: The recorded decision matches the refund policy.
1298
+ - to: refund-request
1299
+ description: The recorded decision contradicts the policy; send it back with the fix.
1300
+
1301
+ review-enrichment: # GRADER state: an isolated grader decides the edge
1302
+ type: grader
1303
+ budget: { maxTurns: 8 } # the grader's own model calls
1304
+ on_error: escalation
1305
+ skills: { allow: [] } # the GRADER is governed HERE too: no bundle, and
1306
+ # read_file (workflow-wide) reads the returned files
1307
+ instructions: |- # REQUIRED — its criteria
1308
+ Read `orders_to_enrich` and `enrichments` with archmax_get_variables, then
1309
+ each entry's `enrichment_file` with read_file.
1310
+ - One entry per requested `order_id`, none missing or extra, each `delayed`
1311
+ matching its file: advance to `enrichment-reported`.
1312
+ - Otherwise advance back to `enrich-order`, naming what to fix. Before
1313
+ that, count the steps in archmax_get_history's trail with kind "grader"
1314
+ and to "enrich-order"; if there are already two, raise
1315
+ `enrichment-incorrect` instead.
1316
+ transitions: # REQUIRED — at least one
1317
+ - to: enrichment-reported
1318
+ description: Every requested order has exactly one enrichment, and each matches its file.
1319
+ - to: enrich-order
1320
+ description: The enrichments are incomplete or do not match their files; send them back.
1304
1321
  ```
1305
1322
 
1306
- Returning nothing is `ok`; a thrown error vetoes (fail-closed); the reason of a
1307
- `veto` is shown to the agent so it can correct itself. Full contract, field
1308
- semantics, and complete worked `before`/`after` examples:
1309
- [`hook-and-test-scripts.md`](hook-and-test-scripts.md).
1323
+ - **When it runs.** Whenever the session's position reaches it — the agent's
1324
+ `archmax_advance`, another automated state's advance, a human decision, an
1325
+ `on_error` route, a turn that opens there, an `archmax_reset` back to it — the
1326
+ runtime runs it **in place of the next model call** of the session's agent. The
1327
+ agent's model is not called while the session stands there (and an automated
1328
+ state spends none of the agent's `maxTurns`). Consecutive automated states run
1329
+ one after another, each committed as its own checkpointed step, so **a chain of
1330
+ code states makes no model call at all**. A chain that never reaches an agent
1331
+ state, a human state or a completion is stopped by the turn's recursion limit
1332
+ (`GraphRecursionError`: the session ends `failed` with `recursion-limit`, not an
1333
+ `on_error` route).
1334
+ - **What it is offered** — exactly its own state's surface: its `tools.allow`, the
1335
+ workflow-wide grant (`allow_always`, or the default tools when it is absent), the
1336
+ host's essential tools, and its controls — one set for both types:
1337
+ `archmax_advance` (absent when terminal), `archmax_raise`,
1338
+ `archmax_get_variables`, `archmax_set_variables`, `archmax_get_history` (which
1339
+ no agent or human state is offered). Never `write_todos`, `archmax_eval`,
1340
+ `archmax_run`, `archmax_wait` or `archmax_reset` — not even when `allow_always`
1341
+ lists one (it is left out), and naming one in its own `tools.allow` is a
1342
+ `validate` error. An automated state **cannot park**. Every call is decided by
1343
+ the kernel against that state, like the agent's: its `forbid`, `forbid_always`,
1344
+ its skill and mount grants and every safety rule bind it, and a tool another
1345
+ state grants is not reachable.
1346
+ - **How it moves.** `archmax_advance({ to, reason })` is checked at the call (the
1347
+ edge exists, the state's `requires` hold against the variables at that moment;
1348
+ `evidence` only into a human state); a refusal can be retried. The **first
1349
+ accepted** advance or raise is the decision and every later one is refused. The
1350
+ decision takes effect when the run finishes **cleanly**.
1351
+ - **How it settles** — exactly one of:
1352
+ - **advanced** → the session moves; the trail records `{ to, kind: grader | code,
1353
+ reason }`; a runtime note carries the reason to the next state
1354
+ (`[grader] The '<state>' grader moved the run to '<to>': <reason>. …` /
1355
+ `[code] '<state>' moved the run to '<to>': <reason>. …`); a human target parks
1356
+ after its one reply-only handoff message.
1357
+ - **raised** → the session ends `failed`, its `exit` carrying the code and reason.
1358
+ - **completed** → a terminal state finished without either call; the session
1359
+ completes, held to the trigger's `returns`.
1360
+ - **failed** → it threw, timed out, exhausted its budget, or a **non-terminal**
1361
+ state finished without either call (`<type> state '<slug>' finished without
1362
+ calling archmax_advance or archmax_raise`) — routed through `on_error`, else the
1363
+ session ends rejected. A raise wins over an error; an error discards a recorded
1364
+ advance. Finishing undecided is never read as approval.
1365
+ - **The history.** Both types read the session with `archmax_get_history`, which
1366
+ answers `{ messages, trail }` (`SessionHistory`) as JSON: the session as it
1367
+ stood when the state was entered, built from the checkpointed session, whole
1368
+ and oldest first, nothing windowed. A message is `{ role, text, toolCalls?,
1369
+ tool?, id?, note? }`: an `assistant` entry lists its calls (`{ id?, name, args
1370
+ }`), the `tool` entry with the matching `id` carries the result inline in
1371
+ `text`, and a runtime note is `role: "runtime"` with its `note` kind.
1372
+ - **Code state.** `script` (required, `scripts/<file>.js|.mjs`) runs in the QuickJS
1373
+ sandbox in a fresh REPL per run. Its default export receives `{ state, trigger?,
1374
+ variables, tools }` and reads the history with
1375
+ `JSON.parse(await tools.archmax_get_history())`. Its return value is never
1376
+ read. `instructions`, `model`, `budget`, `evidence` and `approvers` on a code
1377
+ state are `validate` errors; `settings.timeoutMs` bounds the script. A
1378
+ **terminal** code state completes the session when its script returns: approve
1379
+ into one to end without another model call. Script contract and worked
1380
+ examples: [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).
1381
+ - **Grader state.** A subagent with its own isolated context, built per run: its
1382
+ system prompt is a compiled-in grader preamble followed by the state's
1383
+ `instructions`; its task names the state and lists its transitions; it reads the
1384
+ session only through `archmax_get_history`, and the work with its own state's
1385
+ tools. Its messages never enter the session's transcript, its tokens never
1386
+ stream, it writes no checkpoint; the session reads its decision as the trail
1387
+ step and the note. `instructions` (non-empty) and at least one transition are
1388
+ required; `evidence`, `approvers` and `budget.maxParks` are errors. Every edge
1389
+ into a grader needs a `description` (its instructions are never shown to the
1390
+ work it judges). `budget.maxTurns` bounds its own model calls and
1391
+ `budget.timeoutMs` its run. `model` (else `settings.model`) is resolved under the
1392
+ **`grader`** model role.
1393
+ - **No iteration budget.** Nothing caps how often a state is entered. A
1394
+ state that gives up after N rounds counts them in the trail — each earlier
1395
+ send-back is `{ kind: grader | code, to: <graded state> }` — and raises (or routes
1396
+ to an escalation) on the round after.
1397
+ - **Events.** `code-start { state, script }`, `code-output { state, line }`,
1398
+ `code-result { state, status, durationMs, reason? }`, `grader-start { state }`,
1399
+ `grader-result { state, status, durationMs, reason? }`; a grader's model calls are
1400
+ `model-usage` events under its state, added to the session's usage.
1401
+
1402
+ **Patterns.**
1403
+
1404
+ | Want | Shape |
1405
+ | --- | --- |
1406
+ | 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 |
1407
+ | 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 |
1408
+ | **Approve, then end** | Approve into a terminal code state (records the result, completes the session, no model call) |
1409
+ | 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 |
1410
+
1411
+ **Migrating hooks.** `before:`/`after:` are removed, with every hook kind
1412
+ (`{ script }`, `{ rubric }`, custom kinds), `max_iterations` and the root
1413
+ `extensions:` block; each is a load error naming the key. A `before` check becomes
1414
+ a code state in front of the state; an `after` check a code state behind it; a
1415
+ rubric a grader state behind it. Move `workflows/<slug>/hooks/*.js` to
1416
+ `scripts/` and rewrite each to the code-state contract (it advances or raises
1417
+ instead of returning `ok`/`veto`/`correct`), and grant on the code state whatever
1418
+ the script reads — there is no runtime authority any more.
1310
1419
 
1311
1420
  ## Budgets & error routing
1312
1421
 
@@ -1320,29 +1429,35 @@ states:
1320
1429
  escalation: { instructions: "Explain the failure and hand off." } # terminal
1321
1430
  ```
1322
1431
 
1323
- The workflow's root `budget: { maxModelCalls }` (default 50) bounds a whole
1324
- turn across states, beside each state's own `maxTurns`; its exhaustion routes the
1325
- same way. A turn that throws (model failure, cancellation, the step limit) ends
1326
- the session `failed` with exit code `turn-error`, `turn-cancelled` or
1327
- `recursion-limit`; the next message starts a fresh turn.
1432
+ A state's `budget` bounds that state (a grader's own calls are bounded by its
1433
+ `budget.maxTurns`). There is no workflow-wide budget: a whole turn is bounded by
1434
+ LangGraph's `recursionLimit`, 400 unless the host's invoke config sets its own (a
1435
+ sub-workflow's child session runs under the same bound). A turn that throws (model
1436
+ failure, cancellation, the recursion limit) ends the session `failed` with exit
1437
+ code `turn-error`, `turn-cancelled` or `recursion-limit`; the next message starts
1438
+ a fresh turn.
1328
1439
 
1329
1440
  Two failure classes, routed differently:
1330
1441
 
1331
- - **Recoverable rejections keep the agent in place**: an invalid edge in
1332
- `archmax_advance`, a deliberate hook `veto`, or an in-budget `correct` — the
1333
- agent sees the reason and retries within the same state. A **tool that
1334
- throws** is answered the same way: its error message is the call's
1335
- error-status answer, and it never routes to `on_error`.
1336
- - **Terminal failures route to `on_error`**: a hook *execution error*, an
1337
- exhausted iteration budget (`max_iterations` used up), or an exhausted
1338
- `budget` (`maxTurns`/`timeoutMs`). With `on_error: <state>` declared, the run
1339
- routes to that state (the failure reason arrives as an `[error]` runtime note)
1340
- instead of ending; without it, the failure ends the run.
1341
-
1342
- **Recommendation**: declare `on_error` on states with strict `after` hooks, so
1343
- an exhausted judge doesn't hard-fail the run. The bundled example demonstrates
1344
- this: its `refund-request` state pairs `budget.maxTurns: 24` with
1345
- `on_error: escalation`, a terminal escalation state.
1442
+ - **Recoverable rejections keep the mover in place**: an invalid edge in
1443
+ `archmax_advance` or an unmet `requires` — the agent (or a grader, or a code
1444
+ state's script, which may catch it) sees the reason and retries within the same
1445
+ state. A **tool that throws** is answered the same way: its error message is the
1446
+ call's error-status answer, and it never routes to `on_error`. A grader or code
1447
+ state *sending the work back* is a transition, not a failure: the agent reads
1448
+ the reason as a note.
1449
+ - **Terminal failures route to `on_error`**: an exhausted `budget`
1450
+ (`maxTurns`/`timeoutMs`/`maxParks`), a failed sub-workflow call nothing
1451
+ recovered, and a grader or code state that **fails** — threw, timed out, spent
1452
+ its budget, or finished without advancing or raising. With `on_error: <state>`
1453
+ declared, the run routes to that state (the failure reason arrives as an
1454
+ `[error]` runtime note) instead of ending; without it, the failure ends the run.
1455
+
1456
+ **Recommendation**: declare `on_error` on every grader and code state, so a script
1457
+ error or an undecided grader routes somewhere instead of ending the session
1458
+ rejected. The bundled example demonstrates this: `refund-request` pairs
1459
+ `budget.maxTurns: 12` with `on_error: escalation`, and its `screen-request`,
1460
+ `check-refund` and `review-enrichment` route to the same terminal escalation state.
1346
1461
 
1347
1462
  ## Human states
1348
1463
 
@@ -1523,7 +1638,7 @@ enrich:
1523
1638
  (`.platform/system/GRAPH_STATE.md`) keeps that by wrapping what children skip
1524
1639
  in `<!-- top-level-only -->` … `<!-- /top-level-only -->` lines.
1525
1640
  - **Same agent, isolated context, own session.** Same model, backend, host
1526
- tools; its **own** grading rubrics, from its own spec; a fresh transcript; the child's *own* per-state
1641
+ tools; its **own** grader and code states, from its own spec; a fresh transcript; the child's *own* per-state
1527
1642
  `tools.allow`. It is a separate session (`<parent>~<n>`, `n` the dispatch's
1528
1643
  number in the caller, counted across its turns) with its own checkpoints and its own `scratchpad/`: a path it reads or writes
1529
1644
  resolves inside its own session, so it can neither see nor overwrite a file of
@@ -1658,6 +1773,14 @@ This is a change: these keys used to be ignored without a diagnostic. A spec tha
1658
1773
  | state `for_each:` | One call per entry — parallel tool calls from the agent, or `Promise.all` in a script |
1659
1774
  | state `result:` | The tool result; add `requires: [<name>]` to the state so it must record what it needs |
1660
1775
  | `archmax_advance({ params })` | The call's arguments |
1776
+ | 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)) |
1777
+ | 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` |
1778
+ | `{ rubric: … }` hook | A **grader state** behind the graded state with an edge back; the criteria become its `instructions` |
1779
+ | `max_iterations` / `max_corrections` | Nothing: the grader's `instructions` (or the script) count send-backs in the trail and raise |
1780
+ | root `extensions:` (custom hook kinds) and `createAgent({ hookExecutors })` | Nothing: a custom check is a code state |
1781
+ | `workflows/<slug>/hooks/*.js` returning `ok`/`veto`/`correct` | `workflows/<slug>/scripts/*.js` that call `tools.archmax_advance` / `tools.archmax_raise` |
1782
+ | `runtime.version: "1"` / `"2"` | `"3"` (or omit `runtime`) — 1 and 2 are refused as unsupported |
1783
+ | 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` |
1661
1784
 
1662
1785
  **No fan-out.** There is no concurrent branching: a run traverses **one state at a
1663
1786
  time**, moved only by `archmax_advance`, a human decision, or `on_error` routing.
@@ -1689,7 +1812,8 @@ it costs latency, not correctness.
1689
1812
  ```
1690
1813
  workflows/<name>/workflow.yaml # the machine (canonical spec)
1691
1814
  workflows/<name>/WORKFLOW.md # OPTIONAL prose addendum (no frontmatter)
1692
- workflows/<name>/hooks/*.js # lifecycle hook scripts — run BY THE HARNESS.
1815
+ workflows/<name>/scripts/*.js # code-state scripts — run BY THE RUNTIME when a
1816
+ # `type: code` state is entered.
1693
1817
  # Authoring plane: the agent cannot read or run
1694
1818
  # anything under workflows/
1695
1819
  workflows/<name>/tests/ # cases (*.test.yaml, one case per
@@ -1700,9 +1824,10 @@ skills/<capability>/SKILL.md # a capability, bundled (Agent Skills format).
1700
1824
  # behavior goes inline in that state's
1701
1825
  # `instructions` (never one SKILL.md per state)
1702
1826
  skills/<capability>/scripts/*.js # its archmax_run sources — run BY THE AGENT
1703
- # (lifecycle hook scripts are NOT here: they
1704
- # live in workflows/<name>/hooks/, are run by
1705
- # the runtime, and the agent cannot read them)
1827
+ # (code-state scripts are NOT here: they live in
1828
+ # workflows/<name>/scripts/ — same directory
1829
+ # name, other plane — are run by the runtime,
1830
+ # and the agent cannot read them)
1706
1831
  skills/<capability>/assets/* # its inputs (read-only)
1707
1832
  scratchpad/ # THE working area: intermediate files and the
1708
1833
  # artifacts a state produces. The run IS the
@@ -1718,7 +1843,7 @@ This is the **complete** directory contract — there are no other magic
1718
1843
  locations, so design the full file set up front instead of exploring.
1719
1844
  Reference these paths **workspace-relative, without a leading slash**
1720
1845
  (`skills/order-data/assets/orders.json`, `scratchpad/refund.json`) — in `instructions`, `evidence`,
1721
- allow-entry globs, and paths referenced inside hook scripts alike.
1846
+ allow-entry globs, and paths referenced inside code-state scripts alike.
1722
1847
 
1723
1848
  The authoring-plane prefix (`workflows/`) is **reserved**: a
1724
1849
  host cannot mount it (`MountCollisionError` at assembly), an allow entry naming
@@ -1726,48 +1851,13 @@ it is a `validate` **error**, and a script that reads it is blocked with a
1726
1851
  diagnostic naming the prefix. Never write an allow entry, an `evidence` path, or a
1727
1852
  case `workspace:` key under it.
1728
1853
 
1729
- ### Grading rubrics — inline on the hook
1730
-
1731
- A grader is the **value** of the hook that applies it, not a file beside the spec
1732
- and not a named entry a hook points at:
1733
-
1734
- ```yaml
1735
- states:
1736
- respond:
1737
- after:
1738
- - rubric:
1739
- instructions: |- # REQUIRED — the criteria and the verdict
1740
- Judge the tone of the last assistant message. Return `ok`,
1741
- `correct` or `veto`, always with a concise `reason`.
1742
- max_iterations?: number # the grade-and-retry budget, counted per hook
1743
- model?: string # a model id to grade on, resolved through the
1744
- # assembly's modelFactory (third argument)
1745
- metadata?: object # host data the runtime never reads
1746
- ```
1747
-
1748
- A rubric has **no name**, and there is deliberately **no `title`, `description`
1749
- or `response_format`**: nothing routes to a rubric, nothing labels one in a prompt
1750
- (a display label goes in `metadata`), and a rubric only ever grades — so every
1751
- dispatch requests the verdict schema unconditionally.
1752
-
1753
- Verdict shape: `{ verdict: "ok"|"correct"|"veto", reason: string }`.
1754
-
1755
- Two states needing the same grader declare it **twice**: a state legible on its
1756
- own beats a de-duplicated document. Its identity is its position (state, phase,
1757
- index), which is what the runtime dispatches it under and what its retry budget
1758
- is keyed against — so two graders on one state never share a budget.
1759
-
1760
- The agent learns nothing of it: the rendered graph says a phase carries a hook of
1761
- kind `rubric`, there is no `task` tool to call one with, and no sandbox context
1762
- has `task()`.
1763
-
1764
1854
  ## Cases
1765
1855
 
1766
1856
  Declarative YAML case documents under `workflows/<name>/tests/*.test.yaml`
1767
1857
  (or `.test.yml`) — **one case per file**; the case id is the tests/-relative
1768
1858
  path minus the extension. `archmax test <workflow>` interprets them
1769
1859
  **host-side**: no QuickJS sandbox runs a case (the sandbox serves only
1770
- lifecycle hooks and `archmax_run` inside the driven agent). These are distinct
1860
+ code-state scripts and `archmax_run` inside the driven agent). These are distinct
1771
1861
  from the runtime's own colocated unit tests. Only `*.test.yaml`/`*.test.yml`
1772
1862
  files are discovered — a leftover `*.test.js` is ignored without a diagnostic.
1773
1863
 
@@ -1780,14 +1870,14 @@ the static feedback while a workflow is out of service.
1780
1870
  (`maxConcurrency`, `caseTimeoutMs`, `judge` — see [Root fields](#root-fields)).
1781
1871
  `tests.config.js` is **REMOVED**: its presence is a hard error in
1782
1872
  `archmax test` and `archmax validate`. There is no `reporters` option.
1783
- `judge: {}` enables the grader with the env-configured default model.
1873
+ `judge: {}` enables the case judge (`grade:`) with the env-configured default model.
1784
1874
 
1785
1875
  **The complete case schema (top-level keys, the flat step-entry list, the
1786
1876
  assertion grammar, start conditions, mocks, `from:` fixture references) plus
1787
1877
  worked examples — routing,
1788
1878
  tool mocking, human-in-the-loop decide flows, triggered starts — and the
1789
1879
  JS→YAML migration table are in
1790
- [`hook-and-test-scripts.md`](hook-and-test-scripts.md).**
1880
+ [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).**
1791
1881
 
1792
1882
  Every case carries two mandatory prose fields, both length-capped: a `title`
1793
1883
  (a short label, ≤ 60 chars, printed beside the verdict) and a `description`
@@ -1802,7 +1892,7 @@ errors — fail closed.
1802
1892
 
1803
1893
  ```yaml
1804
1894
  # Nothing is mocked — the case reads the real orders.json fixture, so the
1805
- # denial is re-derived by the refund policy hook rather than asserted directly.
1895
+ # denial is re-derived by the check-refund code state rather than asserted directly.
1806
1896
  title: Refund denied for a shipped order
1807
1897
  description: >-
1808
1898
  A refund for shipped A-1002 parks at refund-review; the reviewer confirms the
@@ -1821,7 +1911,7 @@ steps:
1821
1911
 
1822
1912
  ## Code description docblocks
1823
1913
 
1824
- **Every** code file in the workspace — both kinds: lifecycle hook scripts and
1914
+ **Every** code file in the workspace — both kinds: code-state scripts and
1825
1915
  `archmax_run` sources — MUST begin with a leading JSDoc block (`/** ... */`): a
1826
1916
  title line, a blank line, then prose stating what the code does **and how**.
1827
1917
  Cases are YAML documents, not code — they carry no JSDoc block;
@@ -1829,15 +1919,16 @@ their mandatory `title` and `description` fields are short and capped instead,
1829
1919
  with any longer rationale in a YAML comment above the document. The canonical
1830
1920
  statement of the
1831
1921
  rule, with the required depth per file kind, is
1832
- [SKILL.md → Code files are description-first](../SKILL.md#code-files-are-description-first).
1922
+ [SKILL.md → Workspace layout](../SKILL.md#workspace-layout-complete--nothing-else-exists).
1833
1923
 
1834
1924
  - The block is the **human-facing contract**: admin UIs display only this
1835
1925
  description, and the code below it must honor it. Author
1836
1926
  description-first; when behavior changes, update the description, then edit
1837
1927
  the code to match — never leave the two contradicting each other.
1838
1928
  - Write it deep enough to review on its own: inputs read, the sequence of
1839
- checks or assertions, and each outcome the code can produce. A one-line label
1840
- is not enough.
1929
+ checks or assertions, and each outcome the code can produce (for a code-state
1930
+ script: each `archmax_advance` target and why, each `archmax_raise` code). A
1931
+ one-line label is not enough.
1841
1932
  - It is inert at runtime — the QuickJS sandbox executes the source verbatim
1842
1933
  (typed `@archmax-ai/harness/*` imports excepted; they are stripped).
1843
1934
  - The prose ends at the first `@tag` line (tags are allowed but not part of
@@ -1845,7 +1936,7 @@ rule, with the required depth per file kind, is
1845
1936
  `@specification` tag and no generation marker: the block is a human
1846
1937
  description, not a machine contract.
1847
1938
  - `archmax validate` **errors** when an cases is missing its
1848
- `description` (the case schema requires it). Hook scripts and `archmax_run`
1939
+ `description` (the case schema requires it). Code-state scripts and `archmax_run`
1849
1940
  sources are **not** checked — there the JSDoc rule is an authoring standard
1850
1941
  you uphold yourself.
1851
1942
  - Backends/UIs extract it with the package's exported
@@ -1886,30 +1977,49 @@ owns no second artifact area.
1886
1977
 
1887
1978
  The bundled reference workspace lives at
1888
1979
  `examples/customer-support/workflows/order-lookup/` and demonstrates the full
1889
- v2 layout: `workflow.yaml` (the machine, including the `tests:` block and
1890
- script/rubric hook wiring) plus a frontmatter-free `WORKFLOW.md` prose addendum
1891
- (ASCII graph + domain constraints). Read both in full for a real, working
1892
- example. It declares a top-level `title` and a `title` on every state, with the
1893
- slugs below as the reference targets, plus a top-level `instructions` block
1894
- holding the rules that hold across every state — where the data and artifacts
1895
- live, the tenancy rule, and naming an order's exact status — so no state repeats
1896
- them. Its shape:
1897
-
1898
- - `identify-case` (`triggers: { manual: , email_reply: }`, `before: check-requester.js`) routes to one
1899
- of three cases via transition descriptions.
1900
- - `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`; an
1901
- `after: { rubric: order-reply-tone }` grades the reply at completion.
1980
+ v3 layout: `workflow.yaml` (the machine, including the `tests:` block and its
1981
+ grader and code states), the code-state scripts under `scripts/`, plus a
1982
+ frontmatter-free `WORKFLOW.md` prose addendum (ASCII graph + domain constraints).
1983
+ Read them in full for a real, working example. It declares a top-level `title`
1984
+ and a `title` on every state, with the slugs below as the reference targets, plus
1985
+ a top-level `instructions` block holding the rules that hold across every state —
1986
+ where the data and artifacts live, the tenancy rule, and naming an order's exact
1987
+ status — so no state repeats them. Its shape:
1988
+
1989
+ - `screen-request` (code, `triggers: { manual: , email_reply: }`,
1990
+ `scripts/screen-request.js`, enables `order-data`) — a check **in front**: it
1991
+ merges the requester and tenancy checks before any model call, and advances to
1992
+ `identify-case`, or to `decline` (a terminal agent state that tells the person
1993
+ why) with the reason.
1994
+ - `identify-case` routes to one of four paths via transition descriptions.
1995
+ - `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`:
1996
+ a live answer ends where it is given.
1902
1997
  - `refund-request` records a decision to `scratchpad/refund.json` (no allow entry
1903
- narrows it — the working area is open to `write_file` wherever it is granted; what holds the agent to a
1904
- correct file is the hook), runs `after: check-refund.js`, and pairs
1905
- `budget.maxTurns: 24` with `on_error: escalation` so a terminal failure
1906
- (hook error, exhausted corrections, exhausted budget) routes to escalation
1907
- instead of ending the run. On success it advances to `refund-review`.
1998
+ narrows it — the working area is open to `write_file` wherever it is granted;
1999
+ what holds the agent to a correct file is the check) and advances to
2000
+ `check-refund`, a code state **behind** it (`scripts/check-refund.js`): it
2001
+ re-derives the policy, advances to `refund-review` when the decision matches,
2002
+ sends it back with the fix otherwise, and escalates after two send-backs.
2003
+ `refund-request` pairs `budget.maxTurns: 12` with `on_error: escalation`.
1908
2004
  - `refund-review` (`type: human`, `evidence: [scratchpad/refund.json]`) — a person
1909
2005
  approves (→ terminal `refund-closed`) or refines (→ back to `refund-request`).
1910
- - `escalation` (terminal) is the error handler entered only via
1911
- `refund-request`'s `on_error`.
2006
+ - `escalation` (terminal) is the error handler entered via `on_error` (and from
2007
+ `check-refund`'s give-up edge).
1912
2008
  - `general-question` (terminal) answers and writes `scratchpad/answer.json`.
2009
+ - `report-requested` (terminal, `triggers: { report_requested: }`) writes
2010
+ `scratchpad/report.json` for the requested `company` and summarizes it; when the
2011
+ order records cannot be read, the agent ends the session with `archmax_raise`
2012
+ instead (case `orders-unavailable`).
2013
+ - `enrich-order` (`triggers: { enrichment_requested: }`) — a host-fired path no
2014
+ person waits on — runs the `order-enrichment` fan-out script with `archmax_run`,
2015
+ records its result as `enrichments` (`requires: [enrichments]`), and advances to
2016
+ `review-enrichment`, a **grader** behind it: it reads `orders_to_enrich` and
2017
+ `enrichments` with `archmax_get_variables` and each returned `enrichment_file`
2018
+ with `read_file`. It approves into `enrichment-reported` (a terminal agent state
2019
+ that summarizes the enrichments), sends them back to `enrich-order` naming what
2020
+ to fix, or raises `enrichment-incorrect` once it has sent them back twice,
2021
+ counted on `archmax_get_history`'s trail. Case `enrichment-fan-out` pins the
2022
+ approval as a `grader` trail step.
1913
2023
 
1914
2024
  ### Source of truth in code
1915
2025
 
@@ -1917,13 +2027,14 @@ If you need to confirm a detail, these files define the schema in the runtime:
1917
2027
 
1918
2028
  | Concern | File |
1919
2029
  | --- | --- |
1920
- | Spec types (`MachineState`, `Transition`, `HookSpec`, `AllowEntry`, `WorkflowTestsConfig`) | `src/machine/types.ts` |
2030
+ | The schema (every key, `script` grammar, `refineSpec`) | `src/machine/spec-schema.ts` |
2031
+ | Per-type lint errors and warnings | `src/machine/lint-spec.ts` |
2032
+ | Spec types (`MachineState`, `Transition`, `AllowEntry`, `WorkflowTestsConfig`) | `src/machine/types.ts` |
1921
2033
  | Two-file loading (`workflow.yaml` + prose addendum) | `src/machine/load-spec.ts` |
1922
- | Spec parsing, `ALWAYS_ALLOWED_TOOLS` | `src/machine/machine.ts` |
1923
- | allow-entry matching, hook normalization | `src/machine/allow.ts` |
2034
+ | Type-aware surfaces (`controlTools`, `disclosedTools`, `checkAllowed`) | `src/machine/machine.ts`, `tool-names.ts` |
2035
+ | allow-entry matching | `src/machine/allow.ts` |
1924
2036
  | Rendered workflow header + per-state graph block | `src/workflow/render-prompt.ts` |
1925
- | Lifecycle running + verdicts | `src/lifecycle/` |
2037
+ | 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` |
1926
2038
  | Governance kernel (precedence) | `src/kernel/kernel.ts` |
1927
- | `archmax_advance` tool | `src/workflow/advance-state.ts` |
2039
+ | `archmax_advance` and the other control tools | `src/workflow/control-tools.ts` |
1928
2040
  | Checkpointed state, `WORKFLOW_STATUSES` | `src/workflow/state.ts` |
1929
- | Grading rubrics, verdict schema | `src/rubrics/rubrics.ts` |