@archmax-ai/harness 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +58 -14
  2. package/dist/agent.d.ts +4 -4
  3. package/dist/agent.js +4 -4
  4. package/dist/assembly/compose.d.ts +5 -24
  5. package/dist/assembly/compose.js +40 -41
  6. package/dist/assembly/compose.js.map +1 -1
  7. package/dist/assembly/delegation-registry.js +3 -14
  8. package/dist/assembly/delegation-registry.js.map +1 -1
  9. package/dist/assembly/index.d.ts +18 -15
  10. package/dist/assembly/index.js +50 -32
  11. package/dist/assembly/index.js.map +1 -1
  12. package/dist/assembly/plain.js +1 -2
  13. package/dist/assembly/plain.js.map +1 -1
  14. package/dist/authoring-skill/archmax-harness/README.md +5 -5
  15. package/dist/authoring-skill/archmax-harness/SKILL.md +72 -30
  16. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +35 -23
  17. package/dist/authoring-skill/archmax-harness/references/cases.md +44 -10
  18. package/dist/authoring-skill/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +617 -313
  19. package/dist/authoring-skill/archmax-harness/references/code-states-and-scripts.md +115 -0
  20. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +40 -7
  21. package/dist/authoring-skill/archmax-harness/references/wiring.md +19 -10
  22. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +416 -276
  23. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +161 -62
  24. package/dist/cli/state-flow.d.ts +1 -1
  25. package/dist/cli/state-flow.js +18 -28
  26. package/dist/cli/state-flow.js.map +1 -1
  27. package/dist/cli/turn.d.ts +1 -1
  28. package/dist/cli/turn.js +2 -3
  29. package/dist/cli/turn.js.map +1 -1
  30. package/dist/core/clock.d.ts +26 -0
  31. package/dist/core/clock.js +64 -0
  32. package/dist/core/clock.js.map +1 -0
  33. package/dist/core/code-description.d.ts +3 -3
  34. package/dist/core/code-description.js +3 -3
  35. package/dist/core/deepagents.d.ts +5 -5
  36. package/dist/core/deepagents.js +4 -4
  37. package/dist/core/events.d.ts +23 -42
  38. package/dist/core/events.js +21 -24
  39. package/dist/core/events.js.map +1 -1
  40. package/dist/core/message-readers.d.ts +22 -2
  41. package/dist/core/message-readers.js +31 -0
  42. package/dist/core/message-readers.js.map +1 -1
  43. package/dist/core/messages.d.ts +1 -23
  44. package/dist/core/messages.js +2 -35
  45. package/dist/core/messages.js.map +1 -1
  46. package/dist/core/mounts.js +3 -3
  47. package/dist/core/platform-prompt.generated.js +1 -1
  48. package/dist/core/platform-prompt.generated.js.map +1 -1
  49. package/dist/core/session-zone.d.ts +1 -1
  50. package/dist/core/session-zone.js +1 -1
  51. package/dist/core/workspace-context.d.ts +1 -1
  52. package/dist/core/workspace.d.ts +1 -1
  53. package/dist/core/workspace.js +1 -1
  54. package/dist/core/zones.d.ts +4 -4
  55. package/dist/core/zones.js +5 -5
  56. package/dist/core/zones.js.map +1 -1
  57. package/dist/env.d.ts +9 -9
  58. package/dist/index.d.ts +3 -7
  59. package/dist/index.js +2 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/kernel/kernel.d.ts +31 -61
  62. package/dist/kernel/kernel.js +57 -114
  63. package/dist/kernel/kernel.js.map +1 -1
  64. package/dist/machine/allow.d.ts +17 -2
  65. package/dist/machine/allow.js +16 -6
  66. package/dist/machine/allow.js.map +1 -1
  67. package/dist/machine/lint-spec.js +68 -12
  68. package/dist/machine/lint-spec.js.map +1 -1
  69. package/dist/machine/machine.d.ts +39 -16
  70. package/dist/machine/machine.js +72 -49
  71. package/dist/machine/machine.js.map +1 -1
  72. package/dist/machine/spec-schema.d.ts +34 -179
  73. package/dist/machine/spec-schema.js +93 -106
  74. package/dist/machine/spec-schema.js.map +1 -1
  75. package/dist/machine/tool-names.d.ts +34 -10
  76. package/dist/machine/tool-names.js +41 -10
  77. package/dist/machine/tool-names.js.map +1 -1
  78. package/dist/machine/tool-paths.d.ts +6 -0
  79. package/dist/machine/tool-paths.js +10 -0
  80. package/dist/machine/tool-paths.js.map +1 -1
  81. package/dist/machine/types.d.ts +14 -38
  82. package/dist/machine/types.js.map +1 -1
  83. package/dist/machine/validate-spec.d.ts +1 -1
  84. package/dist/machine/variables.d.ts +2 -0
  85. package/dist/machine/variables.js.map +1 -1
  86. package/dist/public/sandbox.d.ts +62 -66
  87. package/dist/public/sandbox.js +22 -23
  88. package/dist/public/sandbox.js.map +1 -1
  89. package/dist/public/spec.d.ts +12 -19
  90. package/dist/public/spec.js +11 -19
  91. package/dist/public/spec.js.map +1 -1
  92. package/dist/runtime/contract.d.ts +11 -12
  93. package/dist/runtime/contract.js +16 -16
  94. package/dist/runtime/contract.js.map +1 -1
  95. package/dist/sandbox/assets/parts/code-state.js +17 -0
  96. package/dist/sandbox/executor.d.ts +25 -7
  97. package/dist/sandbox/executor.js +56 -47
  98. package/dist/sandbox/executor.js.map +1 -1
  99. package/dist/sandbox/imports.d.ts +10 -10
  100. package/dist/sandbox/imports.js +11 -11
  101. package/dist/sandbox/imports.js.map +1 -1
  102. package/dist/sandbox/prelude.d.ts +5 -6
  103. package/dist/sandbox/prelude.js +38 -35
  104. package/dist/sandbox/prelude.js.map +1 -1
  105. package/dist/sandbox/ptc-gateway.d.ts +19 -18
  106. package/dist/sandbox/ptc-gateway.js +26 -22
  107. package/dist/sandbox/ptc-gateway.js.map +1 -1
  108. package/dist/sessions/operations.js +4 -5
  109. package/dist/sessions/operations.js.map +1 -1
  110. package/dist/sessions/resume.d.ts +12 -7
  111. package/dist/sessions/resume.js +12 -9
  112. package/dist/sessions/resume.js.map +1 -1
  113. package/dist/sessions/summary.d.ts +1 -1
  114. package/dist/testing/assertions.js +9 -4
  115. package/dist/testing/assertions.js.map +1 -1
  116. package/dist/testing/case-document.d.ts +2 -0
  117. package/dist/testing/case-document.js.map +1 -1
  118. package/dist/testing/case-schema.d.ts +1 -1
  119. package/dist/testing/case-schema.js +17 -1
  120. package/dist/testing/case-schema.js.map +1 -1
  121. package/dist/testing/driver.d.ts +4 -0
  122. package/dist/testing/driver.js +22 -3
  123. package/dist/testing/driver.js.map +1 -1
  124. package/dist/testing/grade.js +3 -3
  125. package/dist/testing/grade.js.map +1 -1
  126. package/dist/testing/interpreter.d.ts +2 -2
  127. package/dist/testing/interpreter.js +1 -0
  128. package/dist/testing/interpreter.js.map +1 -1
  129. package/dist/testing/runner.d.ts +2 -2
  130. package/dist/validate/validate.d.ts +1 -1
  131. package/dist/validate/validate.js +39 -70
  132. package/dist/validate/validate.js.map +1 -1
  133. package/dist/workflow/automated-controls.d.ts +63 -0
  134. package/dist/workflow/automated-controls.js +158 -0
  135. package/dist/workflow/automated-controls.js.map +1 -0
  136. package/dist/workflow/automated.d.ts +112 -0
  137. package/dist/workflow/automated.js +163 -0
  138. package/dist/workflow/automated.js.map +1 -0
  139. package/dist/workflow/checkpointer.d.ts +7 -0
  140. package/dist/workflow/checkpointer.js +28 -0
  141. package/dist/workflow/checkpointer.js.map +1 -1
  142. package/dist/workflow/code-state.d.ts +46 -0
  143. package/dist/workflow/code-state.js +26 -0
  144. package/dist/workflow/code-state.js.map +1 -0
  145. package/dist/workflow/control-tools.d.ts +54 -23
  146. package/dist/workflow/control-tools.js +121 -77
  147. package/dist/workflow/control-tools.js.map +1 -1
  148. package/dist/workflow/file-values.d.ts +4 -4
  149. package/dist/workflow/file-values.js +1 -1
  150. package/dist/workflow/file-values.js.map +1 -1
  151. package/dist/workflow/governance.d.ts +13 -22
  152. package/dist/workflow/governance.js +51 -87
  153. package/dist/workflow/governance.js.map +1 -1
  154. package/dist/workflow/grader-state.d.ts +55 -0
  155. package/dist/workflow/grader-state.js +182 -0
  156. package/dist/workflow/grader-state.js.map +1 -0
  157. package/dist/workflow/history.d.ts +11 -0
  158. package/dist/workflow/history.js +64 -0
  159. package/dist/workflow/history.js.map +1 -0
  160. package/dist/workflow/middleware.d.ts +14 -17
  161. package/dist/workflow/middleware.js +106 -74
  162. package/dist/workflow/middleware.js.map +1 -1
  163. package/dist/workflow/move-events.d.ts +21 -0
  164. package/dist/workflow/move-events.js +20 -0
  165. package/dist/workflow/move-events.js.map +1 -0
  166. package/dist/workflow/parks.d.ts +9 -2
  167. package/dist/workflow/parks.js +16 -8
  168. package/dist/workflow/parks.js.map +1 -1
  169. package/dist/workflow/paths.d.ts +9 -9
  170. package/dist/workflow/paths.js +19 -19
  171. package/dist/workflow/paths.js.map +1 -1
  172. package/dist/workflow/render-prompt.d.ts +7 -5
  173. package/dist/workflow/render-prompt.js +22 -37
  174. package/dist/workflow/render-prompt.js.map +1 -1
  175. package/dist/workflow/signature-checks.d.ts +2 -2
  176. package/dist/workflow/snapshot.js +5 -29
  177. package/dist/workflow/snapshot.js.map +1 -1
  178. package/dist/workflow/state.d.ts +19 -6
  179. package/dist/workflow/state.js +24 -8
  180. package/dist/workflow/state.js.map +1 -1
  181. package/dist/workflow/sub-workflow.d.ts +3 -3
  182. package/dist/workflow/tool-service.d.ts +1 -9
  183. package/dist/workflow/tool-service.js +21 -24
  184. package/dist/workflow/tool-service.js.map +1 -1
  185. package/dist/workflow/turn-boundary.d.ts +2 -2
  186. package/dist/workflow/turn-boundary.js +10 -7
  187. package/dist/workflow/turn-boundary.js.map +1 -1
  188. package/dist/workflow/turn-runner.js +30 -2
  189. package/dist/workflow/turn-runner.js.map +1 -1
  190. package/package.json +1 -1
  191. package/skills/archmax-harness/README.md +5 -5
  192. package/skills/archmax-harness/SKILL.md +72 -30
  193. package/skills/archmax-harness/references/backend-integration.md +35 -23
  194. package/skills/archmax-harness/references/cases.md +44 -10
  195. package/skills/archmax-harness/references/{hook-and-test-scripts.md → code-state-and-test-scripts.md} +617 -313
  196. package/skills/archmax-harness/references/code-states-and-scripts.md +115 -0
  197. package/skills/archmax-harness/references/diagnostics.md +40 -7
  198. package/skills/archmax-harness/references/wiring.md +19 -10
  199. package/skills/archmax-harness/references/workflow-schema.md +416 -276
  200. package/skills/archmax-harness/references/workflow-yaml.md +161 -62
  201. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +0 -88
  202. package/dist/lifecycle/hook-shape.d.ts +0 -44
  203. package/dist/lifecycle/hook-shape.js +0 -66
  204. package/dist/lifecycle/hook-shape.js.map +0 -1
  205. package/dist/lifecycle/runner.d.ts +0 -183
  206. package/dist/lifecycle/runner.js +0 -373
  207. package/dist/lifecycle/runner.js.map +0 -1
  208. package/dist/lifecycle/transcript.d.ts +0 -27
  209. package/dist/lifecycle/transcript.js +0 -43
  210. package/dist/lifecycle/transcript.js.map +0 -1
  211. package/dist/rubrics/rubrics.d.ts +0 -135
  212. package/dist/rubrics/rubrics.js +0 -145
  213. package/dist/rubrics/rubrics.js.map +0 -1
  214. package/dist/sandbox/assets/parts/core.js +0 -22
  215. package/dist/sandbox/assets/parts/lifecycle-hook.js +0 -47
  216. package/dist/sandbox/bridge.d.ts +0 -16
  217. package/dist/sandbox/bridge.js +0 -95
  218. package/dist/sandbox/bridge.js.map +0 -1
  219. package/skills/archmax-harness/references/hooks-and-scripts.md +0 -88
@@ -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,7 +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"
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).
117
127
 
118
128
  settings?:
119
129
  model?: string # MODEL ID ONLY — the id every state of this workflow runs
@@ -122,8 +132,12 @@ settings?:
122
132
  # it. NOT a block: `{ id, temperature }` is a load error —
123
133
  # sampling and endpoint are env/host configuration.
124
134
  # Opaque: nothing validates the id against a list.
125
- # Does NOT change the case grader (tests.judge.model).
126
- 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
127
141
  memoryLimitBytes?: number
128
142
  maxPtcCalls?: number | null
129
143
  maxResultChars?: number
@@ -139,8 +153,9 @@ tests?: # cases suite config (declarative data;
139
153
  caseTimeoutMs?: number # per-case WALL-CLOCK budget, default 120000 —
140
154
  # deliberately named differently from
141
155
  # settings.timeoutMs (per-script sandbox budget)
142
- judge?: # {} = enable the grader with the
143
- 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
144
159
  modelOptions?: { temperature?: number, maxTokens?: number }
145
160
  # applied over the env model config, via the
146
161
  # factory's `judge` role; other keys are errors
@@ -212,20 +227,15 @@ mounts?:
212
227
  # validate ERRORS on a name the table does not mount.
213
228
  forbid_always?: string[] # MOUNT NAMES out of reach of EVERY state, governed or
214
229
  # not, whatever any list grants. Deny beats allow.
215
- # Binds this workflow's own HOOKS too, and is
216
- # 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
217
232
  # that declared it. A NAME only — a denial is total, so
218
233
  # there is no `access` to qualify. (A root `mounts.allow`
219
234
  # is a load error: one mounts model.)
220
235
 
221
- extensions?:
222
- hooks?: string[] # custom hook kinds (beyond script/rubric) the
223
- # runtime registers executors for; undeclared
224
- # unknown hook kinds are validation errors
225
-
226
- editor?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
227
- # UI's own state (canvas state positions keyed by
228
- # 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
229
239
  # never reads: not shape-checked, not prompted,
230
240
  # not governed, and excluded from the spec hash.
231
241
  # Do NOT author it by hand and do NOT invent a
@@ -233,7 +243,8 @@ editor?: Record<string, unknown> # HOST PRESENTATION METADATA — an authoring
233
243
  # rewrite a spec that already carries one, or the
234
244
  # builder that wrote it loses every state position.
235
245
  # The root is otherwise STRICT: any other
236
- # unrecognized key is a load error.
246
+ # unrecognized key is a load error — `extensions:`
247
+ # included.
237
248
 
238
249
  states: Record<slug, MachineState> # REQUIRED; ≥1 state, ≥1 with a trigger
239
250
  # the KEY is the state's SLUG (see below)
@@ -265,6 +276,18 @@ states:
265
276
  # reference GUARANTEED rather than dependent on agent
266
277
  # behavior. Name here anything a downstream guard
267
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.
268
291
  title?: string # HUMAN-READABLE label. Presentational only: carried
269
292
  # in a human state's pending-decision record and shown
270
293
  # in host surfaces. REACHES NO MODEL CALL. Never a
@@ -273,11 +296,26 @@ states:
273
296
  # NOT a substitute for `instructions` (behavior
274
297
  # guidance) or a transition `description` (routing).
275
298
 
276
- type?: "agent" | "human"
277
- # 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.
278
308
  # (`workflow` was removed: a state CALLS another
279
309
  # workflow by allowing `archmax_workflow_<slug>`.)
280
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
+
281
319
 
282
320
  triggers?: Record<triggerId, TriggerDeclaration | null>
283
321
  # marks a start state. The KEY is the trigger id
@@ -323,16 +361,19 @@ states:
323
361
  # target's summary is never disclosed.
324
362
 
325
363
  instructions?: string # behavior guidance.
326
- # agent node: surfaced to the LLM while active.
364
+ # agent state: surfaced to the LLM while active.
327
365
  # Write the state's behavior INLINE here; a
328
366
  # skill bundle (skills/<slug>/) is a capability
329
367
  # with data or scripts, enabled by slug — not a
330
368
  # per-state instruction sheet.
331
- # human state: describes the decision for the packet
332
-
333
- before?: HookSpec # gate ENTRY to this state (fail-closed)
334
- after?: HookSpec # gate the archmax_advance OUT (or run at completion
335
- # 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).
336
377
 
337
378
  tools?:
338
379
  allow?: AllowEntry[] # grants beyond the workflow-wide grant (plus the
@@ -343,8 +384,8 @@ states:
343
384
  forbid?: ForbidEntry[] # what this state DENIES while active — beating the
344
385
  # workflow-wide grant, this state's own allow,
345
386
  # and a host's essentialTools alike. Deny beats allow.
346
- # Not inherited by a sub-workflow; a lifecycle hook
347
- # 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.
348
389
 
349
390
  skills?:
350
391
  allow?: string[] # which CAPABILITIES this state ADDS, by SLUG — on top
@@ -366,11 +407,11 @@ states:
366
407
  # listing of skills/ does not show it.
367
408
  # A tools.allow entry still NARROWS within the enabled
368
409
  # set: the two compose as enabled AND allowed.
369
- # Lifecycle HOOKS are exempt — a `before` hook reading
370
- # skills/order-data/assets/orders.json still works in a
371
- # state that enables nothing, so you can gate the
372
- # agent's use of a capability while keeping the hook
373
- # 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.
374
415
 
375
416
  mounts?:
376
417
  allow?: MountGrant[] # which MOUNTS this state ADDS, by NAME — on top of the
@@ -399,27 +440,32 @@ states:
399
440
  # every declaration) AND undisclosed — absent from the
400
441
  # volatile prompt block, and filtered out of every
401
442
  # ls/glob/grep result, the WORKSPACE ROOT LISTING
402
- # included. Lifecycle HOOKS are exempt from this key
403
- # (runtime authority), but not from `forbid_always`
404
- # nor from the WORKFLOW's grants: a hook reaches a
405
- # governed mount only if allow_always or SOME state's
406
- # allow names it, in any state, and writes it only
407
- # 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.
408
446
 
409
447
  model?: string # MODEL ID ONLY — this state's turns run on it (and the
410
448
  # handoff message of a park it holds). Most specific of
411
449
  # state.model > settings.model > the assembly's default
412
450
  # (createAgent `model`/`modelFactory`, else ARCHMAX_MODEL).
413
451
  # Same id as settings.model ⇒ validate WARNS (inert).
414
- # A rubric keeps its own `model`; a sub-workflow resolves
415
- # 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.
416
457
  # A host passing an explicit `model` INSTANCE to
417
458
  # createAgent outranks every declared id (one assembly
418
459
  # warning names them); `modelFactory` is what honours one.
419
460
 
420
461
  budget?: # per-state execution budget
421
- maxTurns?: number # bounds this turn's agent loop
422
- 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.
423
469
  on_error?: string # state to route to on a TERMINAL failure
424
470
  # (see Budgets & error routing)
425
471
 
@@ -435,20 +481,24 @@ states:
435
481
  #
436
482
  # THE AGENT SEES THIS STATE'S EDGES AND NOTHING
437
483
  # ELSE OF THE GRAPH — no other state's slug, title,
438
- # summary, hooks or transitions, and no count of
439
- # the states. So `description` is what it knows
440
- # about where an edge leads. An UNDESCRIBED edge
441
- # (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
442
488
  # `instructions` instead: the edge's line carries
443
489
  # them, quoted, and nothing else of the target.
444
490
  # LOAD ERRORS (states.<slug>.transitions.<i>.description):
445
491
  # an undescribed edge whose target has no
446
492
  # instructions; an undescribed transition of a
447
- # HUMAN state (it is the button label); a
448
- # non-string. An undescribed edge INTO a human
449
- # state discloses that state's reviewer-facing
450
- # instructions to the agent — describe it to keep
451
- # 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.
452
502
  #
453
503
  # Write it for a reader standing in THIS state,
454
504
  # which knows nothing about the target:
@@ -460,10 +510,15 @@ states:
460
510
  # undescribed.
461
511
  #
462
512
  # The rendered line marks a target that is a human
463
- # decision node or terminal (each changes the call
464
- # or what follows it); nothing else of the target.
465
-
466
- # 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):
467
522
  evidence?: string[] # workspace-relative paths always shown in the packet —
468
523
  # a baseline the advancing agent adds to (see Human states)
469
524
  approvers?: string[] # informational; not enforced
@@ -576,8 +631,9 @@ states:
576
631
  overwrite a value the session already holds. So do not list a return in a
577
632
  state's `requires:` unless that state must settle it, and do not tell the
578
633
  agent to lock one before its value is final. `note` is reserved: a `returns`
579
- list may not declare it. Want the model to try again? Check the returns in a terminal
580
- `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
581
637
  run-variable name (**untyped**: any value, `null` included) or a strict object
582
638
  `{ name, type?, description? }` naming one; the spellings mix in one list and
583
639
  names are distinct across both. What the agent must *do* to produce a
@@ -598,14 +654,15 @@ states:
598
654
  refused before any model call), **at the write** (`archmax_set_variables` of a
599
655
  mistyped typed return of the current trigger is a correctable refusal,
600
656
  nothing written) and **at completion** (a mistyped typed return rejects the
601
- 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
602
658
  property schemas, ranges or optional inputs. An unknown `type`, an unknown
603
659
  entry key (`default:`) or an empty `description` fails load.
604
660
  - **A `file` value must name a readable file.** The pure rule checks only the
605
661
  path's shape (a host runs it in a browser). At the start, at the write, at
606
662
  completion and at a delegation call the runtime also requires a **file** (not a
607
663
  folder) at the path that the session may read **on runtime authority** — the
608
- 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/`
609
666
  never), the workflow's mount grants taken across all its states (a governed
610
667
  mount no state is granted is out of reach), and every workflow-wide
611
668
  `forbid_always`, inherited ones included; the state's allow list and its own
@@ -661,8 +718,8 @@ states:
661
718
  connection: acme-slack
662
719
  ```
663
720
 
664
- One state, one behavior, one governance block; `${{trigger}}` and a hook's
665
- `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.
666
723
  - Every declared trigger **starts** a session. An event that must only ever reach
667
724
  a live one — a ticket closing, a payment settling — is declared nowhere: the
668
725
  host names the session it delivers into (`archmax deliver <session> --trigger
@@ -678,7 +735,8 @@ along the chosen edge, a delivery resumes the parked state.
678
735
 
679
736
  **Runtime control tools** carry the `archmax_` prefix — `archmax_advance`,
680
737
  `archmax_reset`, `archmax_wait`, `archmax_raise`, `archmax_run`,
681
- `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`,
682
740
  `tools.allow_always`, and the `forbid`/`forbid_always` lists. These are the only spellings:
683
741
  an entry naming an unprefixed variant governs a tool that does not exist,
684
742
  silently granting nothing. A host tool may never claim the prefix. Deep Agents' built-ins
@@ -689,7 +747,9 @@ silently granting nothing. A host tool may never claim the prefix. Deep Agents'
689
747
  `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
690
748
  `archmax_eval`, `archmax_run`) when the key is absent, plus the always-allowed
691
749
  `archmax_advance` and a host's `essentialTools`, minus whatever a `forbid` list
692
- 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`.
693
753
  Governance is closed by default; there is no way to declare a fully open state.
694
754
  - `tools.forbid` on a state **denies** while that state is active, beating every
695
755
  grant that would otherwise reach it (see [Tool governance](#tool-governance)).
@@ -707,8 +767,8 @@ success.
707
767
  - The call must be the **only** tool call in its message; one sent beside other
708
768
  calls is refused, and the model may raise again on its own.
709
769
  - The session ends **at once** with status `failed` (classified **finished**): no
710
- further model call, no `on_error` route, no `before`/`after` hook (a terminal
711
- 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
712
772
  pending from earlier in the turn is replaced. The position stays where the
713
773
  agent raised; the trail records no step.
714
774
  - The host reads `Outcome.kind === "failed"` and `Outcome.exit`
@@ -722,9 +782,11 @@ success.
722
782
  its own `on_error`, or raise in turn.
723
783
  - Ban it with `tools: { forbid_always: [archmax_raise] }` (or one state's
724
784
  `tools.forbid`) where the agent must not end the run itself — say, a state that
725
- 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).
726
788
  - Test it with the structural `raised` assertion; `succeeded: true` fails on a
727
- raise.
789
+ raise (it passes only on a `completed` outcome).
728
790
 
729
791
  ## Transitions
730
792
 
@@ -732,12 +794,17 @@ success.
732
794
  transitions:
733
795
  - to: string # REQUIRED target state SLUG (must exist); never a title
734
796
  description?: string # shown to the agent (or human) to pick the edge; absent =
735
- # 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
736
800
  type?: "approve" | "reject" | "refine" | "none" # default "none"
737
801
  ```
738
802
 
739
803
  - On **agent** states the agent chooses one edge via
740
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.
741
808
  - On **human** states each transition is a selectable outcome (a UI renders one
742
809
  button per edge, labeled by `type`). `refine` conventionally loops back to a
743
810
  prior state with the reviewer's comment as correction. Max one transition per
@@ -773,16 +840,19 @@ declare that key, the **default tools** (`ls`, `read_file`, `write_file`, `edit_
773
840
  `archmax_run`). A declared list **replaces** the default tools, never adds to them:
774
841
  `allow_always: []` grants no tool workflow-wide, and a list without `write_file` leaves
775
842
  every state that does not name it unable to write. So a new workflow lists all twelve and
776
- removes only what it must not have. Every state also has the always-allowed controls and
777
- any tool the host passes as `essentialTools`, whatever the workflow declares. One built-in is **never
778
- disclosed** and grantable by nothing: `task`, the runtime's own dispatch for
779
- 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
780
850
  `task` in an allow entry is a `validate` error and the kernel refuses the call.
781
851
  The planning scratchpad `write_todos` and the sandbox
782
852
  `archmax_eval` are default tools: granted wherever the workflow-wide grant includes
783
853
  them (everywhere, in a workflow without `allow_always`). `write_todos`'s prompt
784
- guidance appears only when some state is granted it, and hooks run in the sandbox
785
- 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
786
856
  default because the `tools.*` calls its code makes are decided against the active
787
857
  state by the same kernel: it adds computation over what the state permits, not
788
858
  access. `archmax_run` — executing an authored script **file** — is a default tool on
@@ -816,10 +886,11 @@ entries: `tools.forbid_always` at the root denies in every state,
816
886
  `tools.forbid` on a state denies while it is active — subtracting from the
817
887
  workflow-wide grant, the state's own `allow` and a host's `essentialTools` alike.
818
888
  **Deny beats allow, and no narrower level widens a denial.** A forbid entry may
819
- name the tool `*` (every tool); `*` in an allow list is a load error. A lifecycle
820
- hook is exempt from the state's `allow` **and** its `forbid` — runtime authority,
821
- outside that surface both ways — but never from `forbid_always` (the workflow's
822
- 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`.
823
894
 
824
895
  Precedence (highest first):
825
896
 
@@ -833,20 +904,27 @@ Precedence (highest first):
833
904
  `tools.forbid` denies in the active state, both regardless of any allow list
834
905
  (and regardless of always-allowed): deny beats allow, and no narrower level
835
906
  widens a denial.
836
- 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`.
837
911
  3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for a
838
912
  tool the workflow-wide grant includes takes precedence (**narrower wins** —
839
913
  the state's arg globs apply, not the broader grant); an entry for a default
840
914
  tool a declared `allow_always` omits grants it in this state.
841
915
  4. **The workflow-wide grant** — the declared `tools.allow_always`, guards
842
- 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`.
843
918
  5. **The host's `essentialTools`** — always permitted when no state entry
844
919
  narrows them, whatever the workflow declares.
845
920
  6. **Default**: everything else is blocked.
846
921
 
847
922
  **Progressive disclosure.** The model is only shown the active state's tool
848
- surface: the workflow-wide grant + a host's `essentialTools` + the state's entries + `archmax_advance`
849
- (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
850
928
  name-level (arg-constrained tools stay visible; the kernel enforces args at
851
929
  call time) and never replaces enforcement. **Skills are disclosed the same way**:
852
930
  each model call names only the skills the active state enables, with their
@@ -900,7 +978,7 @@ Why by slug, and not with `tools.allow` paths:
900
978
  Capability governance and execution governance are different questions.
901
979
  - **`archmax_run` can only ever be narrowed.** A non-overridable kernel rule
902
980
  (`script.skill-only`) confines it to skill bundles, so a script the agent writes
903
- into `scratchpad/` is unrunnable, a lifecycle hook script is unrunnable, and
981
+ into `scratchpad/` is unrunnable, a code-state script is unrunnable, and
904
982
  `{ tool: archmax_run, paths: ["**"] }` grants nothing — `validate` reports such
905
983
  an entry as inert.
906
984
 
@@ -916,11 +994,12 @@ that serves bundles, naming what the workflow cannot reach. Any root block silen
916
994
  that last one — write `skills: { allow_always: [] }` when a workflow grants no
917
995
  capability workflow-wide.
918
996
 
919
- Hooks are **exempt**: a `before`/`after` script reads authored data on runtime
920
- authority, so a state may enable nothing and still be gated by a hook that reads
921
- the very bundle the model cannot. That is the shape to reach for when a capability
922
- should *enforce* something without being *usable*. A bundle the workflow's
923
- `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.
924
1003
 
925
1004
  **`tools.forbid` on a state denies there**, and `tools.forbid_always` at the
926
1005
  root denies everywhere. Both beat every grant. Migrating a workflow off the
@@ -1060,20 +1139,13 @@ so `AGENTS.md.bak` is not captured.
1060
1139
  governs mounts, naming what the workflow cannot reach. Any root block silences
1061
1140
  it — write `mounts: { allow_always: [] }` when a workflow reaches none.
1062
1141
 
1063
- Hooks and rubric graders are bound by `mounts.forbid_always` and **not** by a
1064
- state's `forbid` or its enabled set: they run on runtime authority, exactly as for
1065
- tools, so a `before` hook can read a mount the model in that state cannot. A hook
1066
- is still held to its **workflow's** grants, taken across every state:
1067
-
1068
- - it reads a governed mount only if `allow_always` or some state's `allow` names
1069
- it, else `mount.not-allowed`. The table serves every child too, so it can hold a
1070
- mount only a child grants, and the caller's hooks cannot reach that;
1071
- - it writes one only where a grant of the workflow asks `access: read_write`, and an
1072
- `access: read` in `allow_always` narrows any mount for it (`mount.read-only`).
1073
-
1074
- So when a hook needs a mount no state's model should see, grant it in a state the
1075
- hook's workflow has anyway, or in `allow_always` with the model's states
1076
- 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`.
1077
1149
 
1078
1150
  ## Session variables in guards
1079
1151
 
@@ -1201,89 +1273,149 @@ the tool's effect instead (the file it wrote, the variable it set).
1201
1273
  To write the characters `${{…}}` literally — authoring a guard through
1202
1274
  `write_file`, or writing about the syntax — double the dollar: `$${{name}}`.
1203
1275
 
1204
- This is **model-facing only**. An `archmax_eval` or hook script already holds the
1205
- run's variables as `args.variables` and composes strings in JavaScript, so
1206
- `${{…}}` 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.
1207
1280
 
1208
- ## Lifecycle hooks
1281
+ ## Grader and code states
1209
1282
 
1210
- ```yaml
1211
- # single hooks (the yaml wires the hook, nothing more):
1212
- before:
1213
- script: hooks/check.js # hand-authored, self-contained JS file;
1214
- # resolves to workflows/<slug>/hooks/check.js
1215
- after: { rubric: reply-tone, max_iterations: 2 } # per hook, not per state
1216
- # a script after hook may carry the same budget:
1217
- # after: { script: hooks/review.js, max_iterations: 2 }
1218
-
1219
- # or a list, run in sequence (short-circuits on veto):
1220
- before:
1221
- - { script: hooks/pre.js }
1222
- - rubric: { instructions: judge the tone }
1223
- ```
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.
1224
1287
 
1225
- - A `HookSpec` is one hook or a list, run in order and short-circuiting on the
1226
- first non-ok. Each hook is either `{ script: <path> }` (JS in the QuickJS
1227
- sandbox) or `{ rubric: <declaration> }` (the grader itself, inline).
1228
- - **A script hook's path is workflow-relative and lives under `hooks/`.**
1229
- `{ script: hooks/check.js }` resolves to `workflows/<slug>/hooks/check.js` on the
1230
- authoring plane. An absolute path, a `..` climb, or anything outside `hooks/`
1231
- (including the former `skills/<name>/scripts/…` spelling) is a load error and a
1232
- `validate` error. Hook scripts are **not** agent-readable and **not** runnable
1233
- with `archmax_run`: the runtime runs them, and that separation is what stops an
1234
- agent probing the guard that gates it.
1235
- - **Reserved sidecar key** on a script or custom hook: `max_iterations` (see
1236
- Verdicts below). It is **refused beside a `rubric`**, whose budget is inside its
1237
- declaration. There is no `instructions`, `model` or `specification` sidecar on
1238
- any hook.
1239
- - **Script hooks are hand-authored.** The script is an ordinary,
1240
- self-contained `.js` file you design and write directly against the sandbox
1241
- contract; it **must** open with a leading JSDoc title + description (plain
1242
- prose, no special tags) — see
1243
- [Code description docblocks](#code-description-docblocks).
1244
- `archmax validate` errors on a missing script file or a
1245
- foreign (non-`@archmax-ai/harness/*`) import; it does not check the docblock on a
1246
- hook script. The runtime never generates: a missing
1247
- script at run time vetoes (fail-closed).
1248
- (A rubric's contract is its own `instructions`.)
1249
- - **Phases**: `before` runs before the agent acts in the state and gates entry;
1250
- `after` runs after the agent calls `archmax_advance`, before the transition
1251
- commits (the "veto window"), or at completion for a terminal state.
1252
- - **Verdicts**: `ok` (proceed), `correct` (agent gets another attempt), `veto`
1253
- (block). Return the vocabulary or nothing: a bare `false` is **not** a veto, it
1254
- is no verdict, which fails closed. Both hook kinds
1255
- can return `correct` from an `after` hook — a script by evaluating to
1256
- `{ verdict: "correct", reason: "..." }`, a rubric in its response —
1257
- and both get the same bounded flow.
1258
- - **`max_iterations`** bounds how many times a `correct` may send the agent back
1259
- before the hook becomes a hard veto: inside the declaration for a rubric, a
1260
- sidecar for a script or custom hook, else 0. It is counted **per hook by
1261
- position**, so two graders on one state never share a budget. An
1262
- exhausted budget is a *terminal failure* — see
1263
- [Budgets & error routing](#budgets--error-routing).
1264
- - **`before` hooks are ok/veto only**: a `correct` verdict from a `before` hook
1265
- is treated as veto, and `max_iterations` on a `before` hook is a validation
1266
- error.
1267
- - **Fail-closed**: a hook that *errors* vetoes the transition. A rubric
1268
- whose response cannot be parsed into a verdict also fails closed (**veto**),
1269
- never coerced to ok — as does a `model` id the host cannot serve.
1270
-
1271
- The hook contract — a default-export function called with one object:
1272
-
1273
- ```js
1274
- export default async function hook({
1275
- state, phase /* "before"|"after" */, trigger, variables, messages,
1276
- from, to, reason, // from/to/reason present on "after"
1277
- tools, // privileged tool bridge, e.g. tools.readFile
1278
- }) {
1279
- return ok(); // or veto(reason) / correct(reason)
1280
- }
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.
1281
1321
  ```
1282
1322
 
1283
- Returning nothing is `ok`; a thrown error vetoes (fail-closed); the reason of a
1284
- `veto` is shown to the agent so it can correct itself. Full contract, field
1285
- semantics, and complete worked `before`/`after` examples:
1286
- [`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.
1287
1419
 
1288
1420
  ## Budgets & error routing
1289
1421
 
@@ -1297,23 +1429,35 @@ states:
1297
1429
  escalation: { instructions: "Explain the failure and hand off." } # terminal
1298
1430
  ```
1299
1431
 
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.
1439
+
1300
1440
  Two failure classes, routed differently:
1301
1441
 
1302
- - **Recoverable rejections keep the agent in place**: an invalid edge in
1303
- `archmax_advance`, a deliberate hook `veto`, or an in-budget `correct` — the
1304
- agent sees the reason and retries within the same state. A **tool that
1305
- throws** is answered the same way: its error message is the call's
1306
- error-status answer, and it never routes to `on_error`.
1307
- - **Terminal failures route to `on_error`**: a hook *execution error*, an
1308
- exhausted iteration budget (`max_iterations` used up), or an exhausted
1309
- `budget` (`maxTurns`/`timeoutMs`). With `on_error: <state>` declared, the run
1310
- routes to that state (the failure reason arrives as an `[error]` runtime note)
1311
- instead of ending; without it, the failure ends the run.
1312
-
1313
- **Recommendation**: declare `on_error` on states with strict `after` hooks, so
1314
- an exhausted judge doesn't hard-fail the run. The bundled example demonstrates
1315
- this: its `refund-request` state pairs `budget.maxTurns: 24` with
1316
- `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.
1317
1461
 
1318
1462
  ## Human states
1319
1463
 
@@ -1494,7 +1638,7 @@ enrich:
1494
1638
  (`.platform/system/GRAPH_STATE.md`) keeps that by wrapping what children skip
1495
1639
  in `<!-- top-level-only -->` … `<!-- /top-level-only -->` lines.
1496
1640
  - **Same agent, isolated context, own session.** Same model, backend, host
1497
- 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
1498
1642
  `tools.allow`. It is a separate session (`<parent>~<n>`, `n` the dispatch's
1499
1643
  number in the caller, counted across its turns) with its own checkpoints and its own `scratchpad/`: a path it reads or writes
1500
1644
  resolves inside its own session, so it can neither see nor overwrite a file of
@@ -1629,6 +1773,14 @@ This is a change: these keys used to be ignored without a diagnostic. A spec tha
1629
1773
  | state `for_each:` | One call per entry — parallel tool calls from the agent, or `Promise.all` in a script |
1630
1774
  | state `result:` | The tool result; add `requires: [<name>]` to the state so it must record what it needs |
1631
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` |
1632
1784
 
1633
1785
  **No fan-out.** There is no concurrent branching: a run traverses **one state at a
1634
1786
  time**, moved only by `archmax_advance`, a human decision, or `on_error` routing.
@@ -1660,7 +1812,8 @@ it costs latency, not correctness.
1660
1812
  ```
1661
1813
  workflows/<name>/workflow.yaml # the machine (canonical spec)
1662
1814
  workflows/<name>/WORKFLOW.md # OPTIONAL prose addendum (no frontmatter)
1663
- 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.
1664
1817
  # Authoring plane: the agent cannot read or run
1665
1818
  # anything under workflows/
1666
1819
  workflows/<name>/tests/ # cases (*.test.yaml, one case per
@@ -1671,9 +1824,10 @@ skills/<capability>/SKILL.md # a capability, bundled (Agent Skills format).
1671
1824
  # behavior goes inline in that state's
1672
1825
  # `instructions` (never one SKILL.md per state)
1673
1826
  skills/<capability>/scripts/*.js # its archmax_run sources — run BY THE AGENT
1674
- # (lifecycle hook scripts are NOT here: they
1675
- # live in workflows/<name>/hooks/, are run by
1676
- # 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)
1677
1831
  skills/<capability>/assets/* # its inputs (read-only)
1678
1832
  scratchpad/ # THE working area: intermediate files and the
1679
1833
  # artifacts a state produces. The run IS the
@@ -1689,7 +1843,7 @@ This is the **complete** directory contract — there are no other magic
1689
1843
  locations, so design the full file set up front instead of exploring.
1690
1844
  Reference these paths **workspace-relative, without a leading slash**
1691
1845
  (`skills/order-data/assets/orders.json`, `scratchpad/refund.json`) — in `instructions`, `evidence`,
1692
- allow-entry globs, and paths referenced inside hook scripts alike.
1846
+ allow-entry globs, and paths referenced inside code-state scripts alike.
1693
1847
 
1694
1848
  The authoring-plane prefix (`workflows/`) is **reserved**: a
1695
1849
  host cannot mount it (`MountCollisionError` at assembly), an allow entry naming
@@ -1697,48 +1851,13 @@ it is a `validate` **error**, and a script that reads it is blocked with a
1697
1851
  diagnostic naming the prefix. Never write an allow entry, an `evidence` path, or a
1698
1852
  case `workspace:` key under it.
1699
1853
 
1700
- ### Grading rubrics — inline on the hook
1701
-
1702
- A grader is the **value** of the hook that applies it, not a file beside the spec
1703
- and not a named entry a hook points at:
1704
-
1705
- ```yaml
1706
- states:
1707
- respond:
1708
- after:
1709
- - rubric:
1710
- instructions: |- # REQUIRED — the criteria and the verdict
1711
- Judge the tone of the last assistant message. Return `ok`,
1712
- `correct` or `veto`, always with a concise `reason`.
1713
- max_iterations?: number # the grade-and-retry budget, counted per hook
1714
- model?: string # a model id to grade on, resolved through the
1715
- # assembly's modelFactory (third argument)
1716
- metadata?: object # host data the runtime never reads
1717
- ```
1718
-
1719
- A rubric has **no name**, and there is deliberately **no `title`, `description`
1720
- or `response_format`**: nothing routes to a rubric, nothing labels one in a prompt
1721
- (a display label goes in `metadata`), and a rubric only ever grades — so every
1722
- dispatch requests the verdict schema unconditionally.
1723
-
1724
- Verdict shape: `{ verdict: "ok"|"correct"|"veto", reason: string }`.
1725
-
1726
- Two states needing the same grader declare it **twice**: a state legible on its
1727
- own beats a de-duplicated document. Its identity is its position (state, phase,
1728
- index), which is what the runtime dispatches it under and what its retry budget
1729
- is keyed against — so two graders on one state never share a budget.
1730
-
1731
- The agent learns nothing of it: the rendered graph says a phase carries a hook of
1732
- kind `rubric`, there is no `task` tool to call one with, and no sandbox context
1733
- has `task()`.
1734
-
1735
1854
  ## Cases
1736
1855
 
1737
1856
  Declarative YAML case documents under `workflows/<name>/tests/*.test.yaml`
1738
1857
  (or `.test.yml`) — **one case per file**; the case id is the tests/-relative
1739
1858
  path minus the extension. `archmax test <workflow>` interprets them
1740
1859
  **host-side**: no QuickJS sandbox runs a case (the sandbox serves only
1741
- 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
1742
1861
  from the runtime's own colocated unit tests. Only `*.test.yaml`/`*.test.yml`
1743
1862
  files are discovered — a leftover `*.test.js` is ignored without a diagnostic.
1744
1863
 
@@ -1751,14 +1870,14 @@ the static feedback while a workflow is out of service.
1751
1870
  (`maxConcurrency`, `caseTimeoutMs`, `judge` — see [Root fields](#root-fields)).
1752
1871
  `tests.config.js` is **REMOVED**: its presence is a hard error in
1753
1872
  `archmax test` and `archmax validate`. There is no `reporters` option.
1754
- `judge: {}` enables the grader with the env-configured default model.
1873
+ `judge: {}` enables the case judge (`grade:`) with the env-configured default model.
1755
1874
 
1756
1875
  **The complete case schema (top-level keys, the flat step-entry list, the
1757
1876
  assertion grammar, start conditions, mocks, `from:` fixture references) plus
1758
1877
  worked examples — routing,
1759
1878
  tool mocking, human-in-the-loop decide flows, triggered starts — and the
1760
1879
  JS→YAML migration table are in
1761
- [`hook-and-test-scripts.md`](hook-and-test-scripts.md).**
1880
+ [`code-state-and-test-scripts.md`](code-state-and-test-scripts.md).**
1762
1881
 
1763
1882
  Every case carries two mandatory prose fields, both length-capped: a `title`
1764
1883
  (a short label, ≤ 60 chars, printed beside the verdict) and a `description`
@@ -1773,7 +1892,7 @@ errors — fail closed.
1773
1892
 
1774
1893
  ```yaml
1775
1894
  # Nothing is mocked — the case reads the real orders.json fixture, so the
1776
- # 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.
1777
1896
  title: Refund denied for a shipped order
1778
1897
  description: >-
1779
1898
  A refund for shipped A-1002 parks at refund-review; the reviewer confirms the
@@ -1792,7 +1911,7 @@ steps:
1792
1911
 
1793
1912
  ## Code description docblocks
1794
1913
 
1795
- **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
1796
1915
  `archmax_run` sources — MUST begin with a leading JSDoc block (`/** ... */`): a
1797
1916
  title line, a blank line, then prose stating what the code does **and how**.
1798
1917
  Cases are YAML documents, not code — they carry no JSDoc block;
@@ -1800,15 +1919,16 @@ their mandatory `title` and `description` fields are short and capped instead,
1800
1919
  with any longer rationale in a YAML comment above the document. The canonical
1801
1920
  statement of the
1802
1921
  rule, with the required depth per file kind, is
1803
- [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).
1804
1923
 
1805
1924
  - The block is the **human-facing contract**: admin UIs display only this
1806
1925
  description, and the code below it must honor it. Author
1807
1926
  description-first; when behavior changes, update the description, then edit
1808
1927
  the code to match — never leave the two contradicting each other.
1809
1928
  - Write it deep enough to review on its own: inputs read, the sequence of
1810
- checks or assertions, and each outcome the code can produce. A one-line label
1811
- 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.
1812
1932
  - It is inert at runtime — the QuickJS sandbox executes the source verbatim
1813
1933
  (typed `@archmax-ai/harness/*` imports excepted; they are stripped).
1814
1934
  - The prose ends at the first `@tag` line (tags are allowed but not part of
@@ -1816,7 +1936,7 @@ rule, with the required depth per file kind, is
1816
1936
  `@specification` tag and no generation marker: the block is a human
1817
1937
  description, not a machine contract.
1818
1938
  - `archmax validate` **errors** when an cases is missing its
1819
- `description` (the case schema requires it). Hook scripts and `archmax_run`
1939
+ `description` (the case schema requires it). Code-state scripts and `archmax_run`
1820
1940
  sources are **not** checked — there the JSDoc rule is an authoring standard
1821
1941
  you uphold yourself.
1822
1942
  - Backends/UIs extract it with the package's exported
@@ -1857,30 +1977,49 @@ owns no second artifact area.
1857
1977
 
1858
1978
  The bundled reference workspace lives at
1859
1979
  `examples/customer-support/workflows/order-lookup/` and demonstrates the full
1860
- v2 layout: `workflow.yaml` (the machine, including the `tests:` block and
1861
- script/rubric hook wiring) plus a frontmatter-free `WORKFLOW.md` prose addendum
1862
- (ASCII graph + domain constraints). Read both in full for a real, working
1863
- example. It declares a top-level `title` and a `title` on every state, with the
1864
- slugs below as the reference targets, plus a top-level `instructions` block
1865
- holding the rules that hold across every state — where the data and artifacts
1866
- live, the tenancy rule, and naming an order's exact status — so no state repeats
1867
- them. Its shape:
1868
-
1869
- - `identify-case` (`triggers: { manual: , email_reply: }`, `before: check-requester.js`) routes to one
1870
- of three cases via transition descriptions.
1871
- - `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`; an
1872
- `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.
1873
1997
  - `refund-request` records a decision to `scratchpad/refund.json` (no allow entry
1874
- narrows it — the working area is open to `write_file` wherever it is granted; what holds the agent to a
1875
- correct file is the hook), runs `after: check-refund.js`, and pairs
1876
- `budget.maxTurns: 24` with `on_error: escalation` so a terminal failure
1877
- (hook error, exhausted corrections, exhausted budget) routes to escalation
1878
- 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`.
1879
2004
  - `refund-review` (`type: human`, `evidence: [scratchpad/refund.json]`) — a person
1880
2005
  approves (→ terminal `refund-closed`) or refines (→ back to `refund-request`).
1881
- - `escalation` (terminal) is the error handler entered only via
1882
- `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).
1883
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.
1884
2023
 
1885
2024
  ### Source of truth in code
1886
2025
 
@@ -1888,13 +2027,14 @@ If you need to confirm a detail, these files define the schema in the runtime:
1888
2027
 
1889
2028
  | Concern | File |
1890
2029
  | --- | --- |
1891
- | 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` |
1892
2033
  | Two-file loading (`workflow.yaml` + prose addendum) | `src/machine/load-spec.ts` |
1893
- | Spec parsing, `ALWAYS_ALLOWED_TOOLS` | `src/machine/machine.ts` |
1894
- | 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` |
1895
2036
  | Rendered workflow header + per-state graph block | `src/workflow/render-prompt.ts` |
1896
- | 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` |
1897
2038
  | Governance kernel (precedence) | `src/kernel/kernel.ts` |
1898
- | `archmax_advance` tool | `src/workflow/advance-state.ts` |
2039
+ | `archmax_advance` and the other control tools | `src/workflow/control-tools.ts` |
1899
2040
  | Checkpointed state, `WORKFLOW_STATUSES` | `src/workflow/state.ts` |
1900
- | Grading rubrics, verdict schema | `src/rubrics/rubrics.ts` |