@archmax-ai/harness 0.7.1 → 0.9.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 (140) hide show
  1. package/README.md +11 -4
  2. package/dist/agent.d.ts +4 -2
  3. package/dist/agent.js +4 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/assembly/compose.d.ts +5 -3
  6. package/dist/assembly/compose.js +24 -8
  7. package/dist/assembly/compose.js.map +1 -1
  8. package/dist/assembly/index.d.ts +13 -2
  9. package/dist/assembly/index.js +35 -10
  10. package/dist/assembly/index.js.map +1 -1
  11. package/dist/authoring-skill/archmax-harness/SKILL.md +9 -5
  12. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +12 -8
  13. package/dist/authoring-skill/archmax-harness/references/cases.md +3 -2
  14. package/dist/authoring-skill/archmax-harness/references/diagnostics.md +5 -3
  15. package/dist/authoring-skill/archmax-harness/references/hook-and-test-scripts.md +52 -7
  16. package/dist/authoring-skill/archmax-harness/references/hooks-and-scripts.md +2 -1
  17. package/dist/authoring-skill/archmax-harness/references/wiring.md +1 -1
  18. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +107 -46
  19. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +43 -17
  20. package/dist/cli/turn.js +1 -2
  21. package/dist/cli/turn.js.map +1 -1
  22. package/dist/core/clock.d.ts +26 -0
  23. package/dist/core/clock.js +64 -0
  24. package/dist/core/clock.js.map +1 -0
  25. package/dist/core/deepagents.d.ts +9 -2
  26. package/dist/core/deepagents.js +15 -2
  27. package/dist/core/deepagents.js.map +1 -1
  28. package/dist/core/message-readers.d.ts +21 -0
  29. package/dist/core/message-readers.js +36 -0
  30. package/dist/core/message-readers.js.map +1 -1
  31. package/dist/core/messages.d.ts +8 -8
  32. package/dist/core/messages.js +11 -13
  33. package/dist/core/messages.js.map +1 -1
  34. package/dist/core/platform-prompt.generated.js +1 -1
  35. package/dist/core/platform-prompt.generated.js.map +1 -1
  36. package/dist/core/workspace-prompt.js +2 -1
  37. package/dist/core/workspace-prompt.js.map +1 -1
  38. package/dist/kernel/kernel.js +15 -10
  39. package/dist/kernel/kernel.js.map +1 -1
  40. package/dist/lifecycle/runner.d.ts +14 -0
  41. package/dist/lifecycle/runner.js +50 -4
  42. package/dist/lifecycle/runner.js.map +1 -1
  43. package/dist/lifecycle/tool-calls.d.ts +9 -0
  44. package/dist/lifecycle/tool-calls.js +57 -0
  45. package/dist/lifecycle/tool-calls.js.map +1 -0
  46. package/dist/lifecycle/transcript.d.ts +10 -6
  47. package/dist/lifecycle/transcript.js +15 -10
  48. package/dist/lifecycle/transcript.js.map +1 -1
  49. package/dist/machine/allow.d.ts +17 -2
  50. package/dist/machine/allow.js +16 -6
  51. package/dist/machine/allow.js.map +1 -1
  52. package/dist/machine/lint-spec.js +77 -29
  53. package/dist/machine/lint-spec.js.map +1 -1
  54. package/dist/machine/machine.d.ts +38 -17
  55. package/dist/machine/machine.js +64 -32
  56. package/dist/machine/machine.js.map +1 -1
  57. package/dist/machine/spec-schema.d.ts +24 -13
  58. package/dist/machine/spec-schema.js +91 -15
  59. package/dist/machine/spec-schema.js.map +1 -1
  60. package/dist/machine/tool-names.d.ts +16 -7
  61. package/dist/machine/tool-names.js +17 -8
  62. package/dist/machine/tool-names.js.map +1 -1
  63. package/dist/machine/tool-paths.d.ts +10 -3
  64. package/dist/machine/tool-paths.js +14 -3
  65. package/dist/machine/tool-paths.js.map +1 -1
  66. package/dist/machine/types.d.ts +23 -9
  67. package/dist/machine/types.js +2 -0
  68. package/dist/machine/types.js.map +1 -1
  69. package/dist/machine/variables.d.ts +2 -0
  70. package/dist/machine/variables.js.map +1 -1
  71. package/dist/public/sandbox.d.ts +35 -3
  72. package/dist/public/sandbox.js +1 -1
  73. package/dist/public/sandbox.js.map +1 -1
  74. package/dist/public/spec.d.ts +5 -5
  75. package/dist/public/spec.js +4 -4
  76. package/dist/public/spec.js.map +1 -1
  77. package/dist/sandbox/executor.js +10 -0
  78. package/dist/sandbox/executor.js.map +1 -1
  79. package/dist/sessions/operations.js +4 -5
  80. package/dist/sessions/operations.js.map +1 -1
  81. package/dist/sessions/resume.d.ts +10 -5
  82. package/dist/sessions/resume.js +11 -8
  83. package/dist/sessions/resume.js.map +1 -1
  84. package/dist/sessions/scope.d.ts +10 -0
  85. package/dist/sessions/scope.js +17 -0
  86. package/dist/sessions/scope.js.map +1 -1
  87. package/dist/testing/case-document.d.ts +2 -0
  88. package/dist/testing/case-document.js.map +1 -1
  89. package/dist/testing/case-schema.d.ts +1 -1
  90. package/dist/testing/case-schema.js +17 -1
  91. package/dist/testing/case-schema.js.map +1 -1
  92. package/dist/testing/driver.d.ts +4 -0
  93. package/dist/testing/driver.js +19 -3
  94. package/dist/testing/driver.js.map +1 -1
  95. package/dist/testing/grade.js +3 -3
  96. package/dist/testing/grade.js.map +1 -1
  97. package/dist/testing/interpreter.js +1 -0
  98. package/dist/testing/interpreter.js.map +1 -1
  99. package/dist/workflow/checkpointer.d.ts +7 -0
  100. package/dist/workflow/checkpointer.js +28 -0
  101. package/dist/workflow/checkpointer.js.map +1 -1
  102. package/dist/workflow/control-tools.js +26 -2
  103. package/dist/workflow/control-tools.js.map +1 -1
  104. package/dist/workflow/governance.d.ts +3 -1
  105. package/dist/workflow/governance.js +27 -6
  106. package/dist/workflow/governance.js.map +1 -1
  107. package/dist/workflow/middleware.js +22 -4
  108. package/dist/workflow/middleware.js.map +1 -1
  109. package/dist/workflow/on-error.js +2 -0
  110. package/dist/workflow/on-error.js.map +1 -1
  111. package/dist/workflow/parks.js +2 -0
  112. package/dist/workflow/parks.js.map +1 -1
  113. package/dist/workflow/render-prompt.d.ts +2 -1
  114. package/dist/workflow/render-prompt.js +24 -5
  115. package/dist/workflow/render-prompt.js.map +1 -1
  116. package/dist/workflow/state.d.ts +41 -0
  117. package/dist/workflow/state.js +33 -4
  118. package/dist/workflow/state.js.map +1 -1
  119. package/dist/workflow/sub-workflow.js +7 -3
  120. package/dist/workflow/sub-workflow.js.map +1 -1
  121. package/dist/workflow/tool-service.d.ts +1 -1
  122. package/dist/workflow/tool-service.js +1 -1
  123. package/dist/workflow/tool-service.js.map +1 -1
  124. package/dist/workflow/turn-boundary.js +8 -0
  125. package/dist/workflow/turn-boundary.js.map +1 -1
  126. package/dist/workflow/turn-runner.js +36 -7
  127. package/dist/workflow/turn-runner.js.map +1 -1
  128. package/dist/workflow/visits.d.ts +21 -0
  129. package/dist/workflow/visits.js +45 -0
  130. package/dist/workflow/visits.js.map +1 -0
  131. package/package.json +1 -1
  132. package/skills/archmax-harness/SKILL.md +9 -5
  133. package/skills/archmax-harness/references/backend-integration.md +12 -8
  134. package/skills/archmax-harness/references/cases.md +3 -2
  135. package/skills/archmax-harness/references/diagnostics.md +5 -3
  136. package/skills/archmax-harness/references/hook-and-test-scripts.md +52 -7
  137. package/skills/archmax-harness/references/hooks-and-scripts.md +2 -1
  138. package/skills/archmax-harness/references/wiring.md +1 -1
  139. package/skills/archmax-harness/references/workflow-schema.md +107 -46
  140. package/skills/archmax-harness/references/workflow-yaml.md +43 -17
@@ -80,8 +80,8 @@ instructions?: string # the workflow's STANDING instructions — free-fo
80
80
  # simply ignored; use `instructions:`, rereading the
81
81
  # text as direction, not narration.
82
82
  # A test case's `description` is UNAFFECTED. A
83
- # TRANSITION's `description` is now REQUIRED and
84
- # non-empty (see `transitions` below).
83
+ # TRANSITION's `description` is what the agent routes
84
+ # on (see `transitions` below).
85
85
 
86
86
  disabled?: boolean # `true` = this machine STARTS NO NEW TURNS. Bounds
87
87
  # STARTING, not finishing.
@@ -115,6 +115,17 @@ runtime?:
115
115
  engine?: string # "archmax-harness"
116
116
  version?: string | number # target contract — use "2"
117
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).
128
+
118
129
  settings?:
119
130
  model?: string # MODEL ID ONLY — the id every state of this workflow runs
120
131
  # on, over the endpoint/credentials the assembly already
@@ -146,7 +157,13 @@ tests?: # cases suite config (declarative data;
146
157
  # factory's `judge` role; other keys are errors
147
158
 
148
159
  tools?:
149
- allow_always?: AllowEntry[] # grants added to every state's allowlist
160
+ allow_always?: AllowEntry[] # the WORKFLOW-WIDE GRANT, in every state. ABSENT = the
161
+ # default tools (ls, read_file, write_file, edit_file,
162
+ # glob, grep, copy_file, move_file, remove_file,
163
+ # write_todos, archmax_eval, archmax_run). DECLARED
164
+ # ([] included) = exactly what it lists: it REPLACES
165
+ # the default tools, so list each one you want. A
166
+ # new workflow lists all twelve.
150
167
  forbid_always?: ForbidEntry[]
151
168
  # DENIED in EVERY state, and absolute: nothing reaches
152
169
  # past it — no state grant, allow_always entry,
@@ -259,6 +276,18 @@ states:
259
276
  # reference GUARANTEED rather than dependent on agent
260
277
  # behavior. Name here anything a downstream guard
261
278
  # reads.
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
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.
262
291
  title?: string # HUMAN-READABLE label. Presentational only: carried
263
292
  # in a human state's pending-decision record and shown
264
293
  # in host surfaces. REACHES NO MODEL CALL. Never a
@@ -312,8 +341,9 @@ states:
312
341
  # NO PART OF THE PROMPT — the model never reads it,
313
342
  # so it is free, and it can NOT carry routing logic.
314
343
  # The routing text the agent reads is the SOURCE
315
- # state's transition `description`s; nothing about a
316
- # target state is disclosed at all.
344
+ # state's transition `description`s, or a target's
345
+ # `instructions` where an edge is undescribed; a
346
+ # target's summary is never disclosed.
317
347
 
318
348
  instructions?: string # behavior guidance.
319
349
  # agent node: surfaced to the LLM while active.
@@ -328,13 +358,14 @@ states:
328
358
  # for a terminal state)
329
359
 
330
360
  tools?:
331
- allow?: AllowEntry[] # grants beyond the always-on tools (plus
332
- # allow_always grants and the always-allowed
333
- # archmax_advance); an entry naming
334
- # an always-on tool NARROWS it.
361
+ allow?: AllowEntry[] # grants beyond the workflow-wide grant (plus the
362
+ # always-allowed archmax_advance); an entry naming
363
+ # a tool that grant includes NARROWS it, and one
364
+ # naming a default tool a declared allow_always
365
+ # leaves out GRANTS it here.
335
366
  forbid?: ForbidEntry[] # what this state DENIES while active — beating the
336
- # workflow's allow_always, this state's own allow,
337
- # and the always-on surface alike. Deny beats allow.
367
+ # workflow-wide grant, this state's own allow,
368
+ # and a host's essentialTools alike. Deny beats allow.
338
369
  # Not inherited by a sub-workflow; a lifecycle hook
339
370
  # is exempt from it (runtime authority).
340
371
 
@@ -421,25 +452,35 @@ states:
421
452
  #
422
453
  # Transition = {
423
454
  # to: string # a declared state's SLUG
424
- # description: string # REQUIRED, non-empty
455
+ # description?: string # what the agent routes on
425
456
  # type?: "approve" | "reject" | "refine" | "none"
426
457
  # }
427
458
  #
428
459
  # THE AGENT SEES THIS STATE'S EDGES AND NOTHING
429
460
  # ELSE OF THE GRAPH — no other state's slug, title,
430
461
  # summary, hooks or transitions, and no count of
431
- # the states. So `description` is the whole of what
432
- # it knows about where an edge leads, and a missing
433
- # or blank one is a LOAD ERROR naming
434
- # states.<slug>.transitions.<i>.description.
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
465
+ # `instructions` instead: the edge's line carries
466
+ # them, quoted, and nothing else of the target.
467
+ # LOAD ERRORS (states.<slug>.transitions.<i>.description):
468
+ # an undescribed edge whose target has no
469
+ # 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.
435
475
  #
436
476
  # Write it for a reader standing in THIS state,
437
477
  # which knows nothing about the target:
438
478
  # GOOD: "The refund is over $50." (when)
439
479
  # BAD: "Go to refund-review." (where)
440
480
  # A routing condition CANNOT live in the target's
441
- # summary/instructions — those are never disclosed
442
- # to the state routing into it.
481
+ # summary, which is never disclosed; it lives in the
482
+ # target's instructions only when the edge is left
483
+ # undescribed.
443
484
  #
444
485
  # The rendered line marks a target that is a human
445
486
  # decision node or terminal (each changes the call
@@ -666,11 +707,12 @@ an entry naming an unprefixed variant governs a tool that does not exist,
666
707
  silently granting nothing. A host tool may never claim the prefix. Deep Agents' built-ins
667
708
  (`read_file`, `task`, …) keep their own names.
668
709
 
669
- - A state with **no `tools` block** gets the **always-on tools only**
670
- (`ls`, `read_file`, `write_file`, `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`,
671
- `grep`, `write_todos`, `archmax_eval`, `archmax_run`, plus the always-allowed `archmax_advance` and `allow_always`
672
- grants — minus
673
- whatever a `forbid` list denies there, and read-only-zone protection).
710
+ - A state with **no `tools` block** gets the **workflow-wide grant only**: what
711
+ `tools.allow_always` lists, or the default tools (`ls`, `read_file`, `write_file`,
712
+ `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
713
+ `archmax_eval`, `archmax_run`) when the key is absent, plus the always-allowed
714
+ `archmax_advance` and a host's `essentialTools`, minus whatever a `forbid` list
715
+ denies there, and read-only-zone protection.
674
716
  Governance is closed by default; there is no way to declare a fully open state.
675
717
  - `tools.forbid` on a state **denies** while that state is active, beating every
676
718
  grant that would otherwise reach it (see [Tool governance](#tool-governance)).
@@ -712,7 +754,8 @@ success.
712
754
  ```yaml
713
755
  transitions:
714
756
  - to: string # REQUIRED target state SLUG (must exist); never a title
715
- description?: string # shown to the agent (or human) to pick the edge
757
+ description?: string # shown to the agent (or human) to pick the edge; absent =
758
+ # the agent is shown the target's instructions instead
716
759
  type?: "approve" | "reject" | "refine" | "none" # default "none"
717
760
  ```
718
761
 
@@ -746,22 +789,30 @@ platform-provided tool resolves through when its collection has several) and
746
789
  `source` (any string: a host's own label for where the tool came from) — the
747
790
  runtime matches governance by `tool` name alone and never reads `source`.
748
791
 
749
- **The model is closed by default, and a denial beats every grant.** Every state always has the
750
- **always-on tools** (`ls`, `read_file`, `write_file`, `edit_file`, `copy_file`, `move_file`,
751
- `remove_file`, `glob`, `grep`, `write_todos`, `archmax_eval`, `archmax_run`). One built-in is **never
792
+ **The model is closed by default, and a denial beats every grant.** Every state has the
793
+ **workflow-wide grant**: what `tools.allow_always` lists, or, when the workflow does not
794
+ declare that key, the **default tools** (`ls`, `read_file`, `write_file`, `edit_file`,
795
+ `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`, `archmax_eval`,
796
+ `archmax_run`). A declared list **replaces** the default tools, never adds to them:
797
+ `allow_always: []` grants no tool workflow-wide, and a list without `write_file` leaves
798
+ 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
752
801
  disclosed** and grantable by nothing: `task`, the runtime's own dispatch for
753
802
  grading rubrics — a rubric grades the agent rather than serving it, so naming
754
803
  `task` in an allow entry is a `validate` error and the kernel refuses the call.
755
804
  The planning scratchpad `write_todos` and the sandbox
756
- `archmax_eval` are standard equipment — always disclosed, always permitted,
757
- no setting; only a `forbid` list refuses them. The interpreter is safe as a
805
+ `archmax_eval` are default tools: granted wherever the workflow-wide grant includes
806
+ 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
758
809
  default because the `tools.*` calls its code makes are decided against the active
759
810
  state by the same kernel: it adds computation over what the state permits, not
760
- access. `archmax_run` — executing an authored script **file** — is always on on
811
+ access. `archmax_run` — executing an authored script **file** — is a default tool on
761
812
  the same terms (the script is a file the state could already read); declare it
762
813
  per state to **narrow** which files may run there. The file operations —
763
814
  `copy_file({ source, destination, overwrite? })`, `move_file` (same arguments)
764
- and `remove_file({ file_path })` — are always on on the same terms: every tool
815
+ and `remove_file({ file_path })` — are default tools on the same terms: every tool
765
816
  **declares its path arguments** with how the call uses each (`read`, `list`,
766
817
  `search`, `write`, `remove`, `execute`), and every path rule reads that
767
818
  declaration. `copy_file` reads `source` and writes `destination`; `move_file`
@@ -774,16 +825,19 @@ entry guards every declared path argument: a grant needs all of them to match
774
825
  declares its paths (`toolPaths`, or a descriptor's `paths`) is governed the same
775
826
  way; an argument may hold a list of paths, each one governed (and each one
776
827
  tested by a `paths:` guard), and any other shape is refused
777
- (`tool.path-argument`). `scratchpad/` is open in every state to the always-on
778
- tools only — a host tool a state must grant stays refused where it is not
779
- granted, whatever its paths. Any other tool
828
+ (`tool.path-argument`). `scratchpad/` is open in every state to each default file tool the state
829
+ is granted, whatever the granting entry narrows, and to a host's `essentialTools`.
830
+ A file tool the state is not granted gets nothing from it, and a host tool a state
831
+ must grant stays refused where it is not granted, whatever its paths. Any other tool
780
832
  must be declared in the state's `tools.allow` or workflow-level
781
- `tools.allow_always`. A `tools.allow` entry naming an always-on tool **narrows**
782
- it for that state.
833
+ `tools.allow_always`. A `tools.allow` entry naming a tool the workflow-wide grant
834
+ includes **narrows** it for that state. A guard on an `allow_always` entry is
835
+ enforced like any other (`{ tool: write_file, paths: [reports/**] }` binds in every
836
+ state that does not name `write_file`).
783
837
  Denial is declared beside the grant, in the same two positions and with the same
784
838
  entries: `tools.forbid_always` at the root denies in every state,
785
839
  `tools.forbid` on a state denies while it is active — subtracting from the
786
- workflow's grant, the state's own `allow` and the always-on surface alike.
840
+ workflow-wide grant, the state's own `allow` and a host's `essentialTools` alike.
787
841
  **Deny beats allow, and no narrower level widens a denial.** A forbid entry may
788
842
  name the tool `*` (every tool); `*` in an allow list is a load error. A lifecycle
789
843
  hook is exempt from the state's `allow` **and** its `forbid` — runtime authority,
@@ -803,17 +857,18 @@ Precedence (highest first):
803
857
  (and regardless of always-allowed): deny beats allow, and no narrower level
804
858
  widens a denial.
805
859
  2. **Always allowed**: `archmax_advance` (baked in, every state).
806
- 3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for an
807
- always-on tool (or one also in `allow_always`) takes precedence
808
- (**narrower wins** — the state's arg globs apply, not the broader grant).
809
- 4. **Essential built-ins** — always permitted when no state entry narrows
810
- them. Listing one in `allow_always` is inert (`archmax validate` warns).
811
- 5. **Workflow `tools.allow_always`** — grants tools that are not always on in every
812
- state.
860
+ 3. **Per-state `tools.allow`** — the listed tools/arg-shapes. An entry for a
861
+ tool the workflow-wide grant includes takes precedence (**narrower wins** —
862
+ the state's arg globs apply, not the broader grant); an entry for a default
863
+ tool a declared `allow_always` omits grants it in this state.
864
+ 4. **The workflow-wide grant** — the declared `tools.allow_always`, guards
865
+ included, or the default tools, unconstrained, when the key is absent.
866
+ 5. **The host's `essentialTools`** — always permitted when no state entry
867
+ narrows them, whatever the workflow declares.
813
868
  6. **Default**: everything else is blocked.
814
869
 
815
870
  **Progressive disclosure.** The model is only shown the active state's tool
816
- surface: always-on tools + `allow_always` + the state's entries + `archmax_advance`
871
+ surface: the workflow-wide grant + a host's `essentialTools` + the state's entries + `archmax_advance`
817
872
  (hidden in terminal states), minus every tool denied there by name. Disclosure is
818
873
  name-level (arg-constrained tools stay visible; the kernel enforces args at
819
874
  call time) and never replaces enforcement. **Skills are disclosed the same way**:
@@ -1265,6 +1320,12 @@ states:
1265
1320
  escalation: { instructions: "Explain the failure and hand off." } # terminal
1266
1321
  ```
1267
1322
 
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.
1328
+
1268
1329
  Two failure classes, routed differently:
1269
1330
 
1270
1331
  - **Recoverable rejections keep the agent in place**: an invalid edge in
@@ -1839,7 +1900,7 @@ them. Its shape:
1839
1900
  - `orders-question` (terminal) answers from `skills/order-data/assets/orders.json`; an
1840
1901
  `after: { rubric: order-reply-tone }` grades the reply at completion.
1841
1902
  - `refund-request` records a decision to `scratchpad/refund.json` (no allow entry
1842
- narrows it — the working area is open in every state; what holds the agent to a
1903
+ narrows it — the working area is open to `write_file` wherever it is granted; what holds the agent to a
1843
1904
  correct file is the hook), runs `after: check-refund.js`, and pairs
1844
1905
  `budget.maxTurns: 24` with `on_error: escalation` so a terminal failure
1845
1906
  (hook error, exhausted corrections, exhausted budget) routes to escalation
@@ -23,11 +23,24 @@ tests: # case suite config (no tests.config.js exists)
23
23
  maxConcurrency: 1 # only 1 accepted
24
24
  caseTimeoutMs: 120000 # per-case wall clock, distinct from settings.timeoutMs
25
25
  judge: {} # required for grade:; {} = env default model, or { model, modelOptions }
26
+ budget: { maxModelCalls: 50 } # model calls one turn may make across ALL states (default 50); raise for long turns
26
27
  metadata: { canvas: {} } # host data, never read; accepted at root, state, rubric — leave as found
27
28
  extensions: { hooks: [] } # host-registered hook kinds; rarely authored
28
29
 
29
30
  tools: # tool governance, workflow level
30
- allow_always: [] # granted in EVERY state; a state's allow adds to it
31
+ allow_always: # granted in EVERY state; a state's allow adds to or narrows it.
32
+ - ls # The DEFAULT TOOLS: declared, this list REPLACES them (absent,
33
+ - read_file # all twelve are granted), so a new workflow lists all twelve and
34
+ - write_file # removes only what it must not have. Workflow-wide host tools
35
+ - edit_file # go below them; state-specific ones under a state's tools.allow.
36
+ - glob
37
+ - grep
38
+ - copy_file
39
+ - move_file
40
+ - remove_file
41
+ - write_todos
42
+ - archmax_eval
43
+ - archmax_run
31
44
  forbid_always: [archmax_reset] # denied in EVERY state; beats every grant; "*" = every tool
32
45
  skills: # skill governance, workflow level — SLUGS, never paths
33
46
  allow_always: [order-data] # enabled in every state; [] = every capability is state-specific
@@ -67,23 +80,25 @@ states:
67
80
  tools:
68
81
  allow: # adds to allow_always; entry = name | { tool, args: {<arg>: [globs]} } | { tool, paths }
69
82
  - lookup_customer
70
- - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] } # narrows an always-on tool, within an enabled skill
83
+ - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] } # narrows the workflow-wide grant, within an enabled skill
71
84
  - { tool: write_file, args: { file_path: ["reports/${{case_id}}/*.json"] } } # variable-bound guard
72
85
  - archmax_workflow_enrich-order # runs a sibling workflow
73
- forbid: [archmax_wait] # denies here; beats every grant incl. the always-on surface
86
+ forbid: [archmax_wait] # denies here; beats every grant incl. allow_always
74
87
  skills:
75
88
  allow: [refund-policy] # adds to allow_always (bundle readable + scripts runnable, no path entries)
76
89
  forbid: [order-data] # subtracts, even from allow_always ([] never denies)
77
90
  requires: [case_id] # variables that must be set before this state may be left
91
+ locks: [case_id] # variables only this state may write (change = come back here)
78
92
  model: claude-opus-5 # MODEL ID ONLY — this state's turns; beats settings.model
79
93
  budget: { maxTurns: 12, timeoutMs: 60000, maxParks: 5 } # bounds the state's segment
80
94
  on_error: closed # where a terminal failure of this segment routes
81
95
  metadata: { x: 40, y: 180 }
82
96
  transitions: # none => terminal state
83
97
  - to: review # slug; must exist
84
- description: The decision is recorded and needs a person's approval. # REQUIRED, non-empty:
85
- # the ONLY thing the agent knows about this edge (on a human
86
- # state, also the button label). Say WHEN, never where.
98
+ description: The decision is recorded and needs a person's approval. # what the agent routes
99
+ # on. Say WHEN, never where. Omit it to route on the target's
100
+ # instructions instead (target must have some); REQUIRED on a
101
+ # human state's transitions, where it is the button label.
87
102
  type: none # approve | reject | refine | none — semantic on human states
88
103
 
89
104
  review: # human state: parks with a decision record; a PERSON picks the edge
@@ -119,7 +134,10 @@ states:
119
134
  - **Variables.** One flat checkpointed `name → value` store, never declared.
120
135
  Host seeds (`--variables`, `createAgent({ variables })`, a delivery) are
121
136
  **locked**; the agent uses `archmax_get_variables`/`archmax_set_variables`
122
- (`lock: true` to freeze). Reserved: `trigger` (current trigger id, always
137
+ (`lock: true` to freeze, until `archmax_reset` releases the agent's locks).
138
+ A value that must be decided in one place yet stay correctable belongs to a
139
+ state: `locks: [order_id]` lets only that state write it, so a change routes
140
+ back there. Reserved: `trigger` (current trigger id, always
123
141
  locked) and `title` (the run's one-line task label, agent-owned; a
124
142
  sub-workflow child is not asked for one). A
125
143
  `${{name}}` / `${{a.b.-1.c}}` reference works in `tools.allow` globs
@@ -174,7 +192,8 @@ states:
174
192
  bind them; `forbid_always`, safety rules and the workflow's mount grants
175
193
  (any state's) do. Human states may not declare hooks.
176
194
  - **Budgets and `on_error`.** `budget.maxTurns`/`timeoutMs`/`maxParks` bound
177
- a state's segment. A terminal failure (hook execution error, exhausted
195
+ a state's segment; the root `budget.maxModelCalls` (default 50) bounds a whole
196
+ turn across states, so a cycle between states ends there. A terminal failure (hook execution error, exhausted
178
197
  corrections, exhausted budget, exhausted parks) routes to `on_error`
179
198
  instead of failing the run; recoverable rejections (invalid edge, veto,
180
199
  in-budget correct) keep the agent in place. Declare `on_error` on states
@@ -252,16 +271,23 @@ no way to see more of the graph.
252
271
 
253
272
  Closed by default; **deny beats allow, and no narrower level widens a denial.**
254
273
 
255
- - **Always on, never declared**: `ls`, `read_file`, `write_file`,
256
- `edit_file`, `copy_file`, `move_file`, `remove_file`, `glob`, `grep`,
257
- `write_todos`, `archmax_eval`, `archmax_run`,
258
- and the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
259
- `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`. Any other
260
- tool needs an
261
- `allow`/`allow_always` entry. An entry naming an always-on tool **narrows**
262
- it. `task` is ungrantable (error).
274
+ - **The workflow-wide grant**: `tools.allow_always`, in every state. Absent,
275
+ it is the **default tools**: `ls`, `read_file`, `write_file`, `edit_file`,
276
+ `copy_file`, `move_file`, `remove_file`, `glob`, `grep`, `write_todos`,
277
+ `archmax_eval`, `archmax_run`. Declared (`[]` included), it is exactly what it
278
+ lists and **replaces** the default tools, so a new workflow lists all twelve
279
+ (as in the skeleton) and removes only what it must not have. Never declared:
280
+ the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
281
+ `archmax_raise`, `archmax_get_variables`, `archmax_set_variables`, and a host's
282
+ `essentialTools`. Any other tool needs an `allow`/`allow_always` entry. A
283
+ state entry naming a granted tool **narrows** it there, and one naming a
284
+ default tool the list omits grants it there. `task` is ungrantable (error).
263
285
  - **Entries**: bare name; `{ tool, args: { <arg>: ["glob", …] } }` (matches
264
- if any glob matches; `${{var}}` allowed); `{ tool, paths: [...] }`
286
+ if any glob matches; `${{var}}` allowed). An argument the tool declares as a
287
+ path (else `file_path`) is canonicalized first; any other argument (a URL, an
288
+ address, an email) is matched **as written**, and a denial also catches its
289
+ path form. `""` matches only an empty value. `*` never crosses `/` (`**` does).
290
+ `{ tool, paths: [...] }`
265
291
  (guards every path argument the tool declares — all must match to grant,
266
292
  any matching denies). `"*"` as the tool is legal only in a
267
293
  forbid list. Globs match dot-segments (`secrets/**` covers `secrets/.env`).