@archmax-ai/harness 0.1.0 → 0.2.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 (77) hide show
  1. package/README.md +45 -4
  2. package/dist/assembly/compose.js +3 -5
  3. package/dist/assembly/compose.js.map +1 -1
  4. package/dist/assembly/delegation-registry.js +4 -2
  5. package/dist/assembly/delegation-registry.js.map +1 -1
  6. package/dist/authoring-skill/archmax-harness/README.md +24 -16
  7. package/dist/authoring-skill/archmax-harness/SKILL.md +7 -1
  8. package/dist/authoring-skill/archmax-harness/references/backend-integration.md +58 -2
  9. package/dist/authoring-skill/archmax-harness/references/workflow-schema.md +66 -20
  10. package/dist/authoring-skill/archmax-harness/references/workflow-yaml.md +14 -4
  11. package/dist/cli/turn.d.ts +9 -4
  12. package/dist/cli/turn.js +35 -8
  13. package/dist/cli/turn.js.map +1 -1
  14. package/dist/cli.js +4 -5
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index.d.ts +12 -0
  17. package/dist/index.js +15 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/machine/machine.d.ts +9 -0
  20. package/dist/machine/machine.js +13 -0
  21. package/dist/machine/machine.js.map +1 -1
  22. package/dist/machine/signature.d.ts +104 -0
  23. package/dist/machine/signature.js +210 -0
  24. package/dist/machine/signature.js.map +1 -0
  25. package/dist/machine/spec-schema.d.ts +158 -8
  26. package/dist/machine/spec-schema.js +78 -6
  27. package/dist/machine/spec-schema.js.map +1 -1
  28. package/dist/machine/triggers.d.ts +6 -2
  29. package/dist/machine/triggers.js +7 -2
  30. package/dist/machine/triggers.js.map +1 -1
  31. package/dist/machine/types.d.ts +7 -1
  32. package/dist/machine/types.js.map +1 -1
  33. package/dist/machine/variables.d.ts +2 -0
  34. package/dist/machine/variables.js +4 -0
  35. package/dist/machine/variables.js.map +1 -1
  36. package/dist/public/spec.d.ts +17 -2
  37. package/dist/public/spec.js +15 -1
  38. package/dist/public/spec.js.map +1 -1
  39. package/dist/sessions/resume.d.ts +4 -0
  40. package/dist/sessions/resume.js +8 -1
  41. package/dist/sessions/resume.js.map +1 -1
  42. package/dist/workflow/control-tools.d.ts +7 -2
  43. package/dist/workflow/control-tools.js +27 -2
  44. package/dist/workflow/control-tools.js.map +1 -1
  45. package/dist/workflow/governance.js +28 -19
  46. package/dist/workflow/governance.js.map +1 -1
  47. package/dist/workflow/middleware.js +5 -10
  48. package/dist/workflow/middleware.js.map +1 -1
  49. package/dist/workflow/prompt-cache.d.ts +0 -2
  50. package/dist/workflow/prompt-cache.js +0 -7
  51. package/dist/workflow/prompt-cache.js.map +1 -1
  52. package/dist/workflow/render-prompt.js +28 -7
  53. package/dist/workflow/render-prompt.js.map +1 -1
  54. package/dist/workflow/signature-checks.d.ts +22 -0
  55. package/dist/workflow/signature-checks.js +52 -0
  56. package/dist/workflow/signature-checks.js.map +1 -0
  57. package/dist/workflow/sub-workflow.d.ts +23 -16
  58. package/dist/workflow/sub-workflow.js +78 -14
  59. package/dist/workflow/sub-workflow.js.map +1 -1
  60. package/dist/workflow/tool-service.js +1 -1
  61. package/dist/workflow/tool-service.js.map +1 -1
  62. package/dist/workflow/turn-boundary.js +4 -5
  63. package/dist/workflow/turn-boundary.js.map +1 -1
  64. package/dist/workflow/workflow-tools.d.ts +17 -9
  65. package/dist/workflow/workflow-tools.js +30 -17
  66. package/dist/workflow/workflow-tools.js.map +1 -1
  67. package/package.json +2 -1
  68. package/skills/archmax-harness/README.md +82 -0
  69. package/skills/archmax-harness/SKILL.md +247 -0
  70. package/skills/archmax-harness/references/backend-integration.md +681 -0
  71. package/skills/archmax-harness/references/cases.md +41 -0
  72. package/skills/archmax-harness/references/diagnostics.md +51 -0
  73. package/skills/archmax-harness/references/hook-and-test-scripts.md +1070 -0
  74. package/skills/archmax-harness/references/hooks-and-scripts.md +82 -0
  75. package/skills/archmax-harness/references/wiring.md +101 -0
  76. package/skills/archmax-harness/references/workflow-schema.md +1677 -0
  77. package/skills/archmax-harness/references/workflow-yaml.md +251 -0
@@ -0,0 +1,251 @@
1
+ # `workflow.yaml` — keys, machine rules, governance
2
+
3
+ Load this when writing or editing a `workflow.yaml`. Field-level detail and
4
+ precedence edge cases: [`workflow-schema.md`](workflow-schema.md).
5
+
6
+ ## Every key
7
+
8
+ The schema is strict: an unknown key anywhere is a load error naming it. This
9
+ skeleton is the complete vocabulary; omit what you do not need.
10
+
11
+ ```yaml
12
+ title: Triage example # label only; identity is the directory slug
13
+ instructions: |- # standing direction for the whole run (warning if absent)
14
+ Records live in skills/order-data/assets/orders.json; write artifacts under scratchpad/.
15
+
16
+ Answer only from records you have read; never fabricate.
17
+ runtime: { engine: archmax-harness, version: "2" }
18
+ disabled: false # true = out of service: no new turns; parked runs still drain
19
+ settings: # sandbox limits: timeoutMs, memoryLimitBytes, maxPtcCalls,
20
+ timeoutMs: 15000 # maxResultChars, prompt_cache: { enabled, ttl }
21
+ model: gpt-5-mini # MODEL ID ONLY — every state runs on it; overrides ARCHMAX_MODEL
22
+ tests: # case suite config (no tests.config.js exists)
23
+ maxConcurrency: 1 # only 1 accepted
24
+ caseTimeoutMs: 120000 # per-case wall clock, distinct from settings.timeoutMs
25
+ judge: {} # required for grade:; {} = env default model, or { model, modelOptions }
26
+ metadata: { canvas: {} } # host data, never read; accepted at root, state, rubric — leave as found
27
+ extensions: { hooks: [] } # host-registered hook kinds; rarely authored
28
+
29
+ tools: # tool governance, workflow level
30
+ allow_always: [] # granted in EVERY state; a state's allow adds to it
31
+ forbid_always: [archmax_reset] # denied in EVERY state; beats every grant; "*" = every tool
32
+ skills: # skill governance, workflow level — SLUGS, never paths
33
+ allow_always: [order-data] # enabled in every state; [] = every capability is state-specific
34
+ forbid_always: [] # out of reach everywhere
35
+
36
+ states:
37
+ triage: # key = slug = identity (^[a-z0-9]+(-[a-z0-9]+)*$)
38
+ type: agent # agent (default) | human
39
+ title: Triage request # label for people/hosts; reaches no model call
40
+ summary: Record the refund decision for review. # ditto: an authoring/host label, NOT in the prompt
41
+ triggers: # a trigger is declared on the state it ENTERS; key = id
42
+ manual: # CLI / caller / host entry; the prompt is its input
43
+ email_reply: # any other id = host event; the payload arrives as locked variables
44
+ session: conversation_id # dotted path over variables: which conversation a firing joins
45
+ message: false # or a dotted path to the message text; false = firings carry none
46
+ connection: mail # host access connection; other keys preserved with a warning
47
+ description: Record one refund decision. # for CALLERS (delegating model, MCP client); never the agent
48
+ requires: # variables a firing must supply (a caller's required params):
49
+ - order_id # a bare name is untyped (any value, null included)
50
+ - { name: due, type: date, description: The day it is due. } # typed: held at the boundary
51
+ returns: [result_file] # variables the run guarantees on completion (a caller's result)
52
+ # entry types: string integer number boolean date date-time object array
53
+ instructions: >-
54
+ Record the decision in scratchpad/refund.json, then archmax_advance to
55
+ review; never close the case yourself.
56
+ before: # gates entry: ok/veto only; a veto here rejects the run
57
+ - script: hooks/check-requester.js
58
+ after: # gates the archmax_advance out (or completion, when terminal)
59
+ - script: hooks/check-policy.js
60
+ max_iterations: 2 # how many `correct`s before a hard veto (per hook; default 0)
61
+ - rubric: # LLM grader, inline; the agent never sees it
62
+ instructions: |- # the only required key: criteria + verdict (ok/correct/veto + reason)
63
+ Judge the tone of the last assistant message …
64
+ max_iterations: 1
65
+ model: gpt-4.1-mini # optional override
66
+ tools:
67
+ allow: # adds to allow_always; entry = name | { tool, args: {<arg>: [globs]} } | { tool, paths }
68
+ - lookup_customer
69
+ - { tool: archmax_run, paths: ["skills/refund-policy/scripts/**"] } # narrows an always-on tool, within an enabled skill
70
+ - { tool: write_file, args: { file_path: ["reports/${{case_id}}/*.json"] } } # variable-bound guard
71
+ - archmax_workflow_enrich-order # runs a sibling workflow
72
+ forbid: [archmax_wait] # denies here; beats every grant incl. the always-on surface
73
+ skills:
74
+ allow: [refund-policy] # adds to allow_always (bundle readable + scripts runnable, no path entries)
75
+ forbid: [order-data] # subtracts, even from allow_always ([] never denies)
76
+ requires: [case_id] # variables that must be set before this state may be left
77
+ model: claude-opus-5 # MODEL ID ONLY — this state's turns; beats settings.model
78
+ budget: { maxTurns: 12, timeoutMs: 60000, maxParks: 5 } # bounds the state's segment
79
+ on_error: closed # where a terminal failure of this segment routes
80
+ metadata: { x: 40, y: 180 }
81
+ transitions: # none => terminal state
82
+ - to: review # slug; must exist
83
+ description: The decision is recorded and needs a person's approval. # REQUIRED, non-empty:
84
+ # the ONLY thing the agent knows about this edge (on a human
85
+ # state, also the button label). Say WHEN, never where.
86
+ type: none # approve | reject | refine | none — semantic on human states
87
+
88
+ review: # human state: parks with a decision record; a PERSON picks the edge
89
+ type: human
90
+ title: Review refund
91
+ summary: A person approves the recorded decision or sends it back. # label only
92
+ instructions: >- # required on human states: the decision to present
93
+ Confirm scratchpad/refund.json matches policy.
94
+ approvers: [finance@acme.com]
95
+ evidence: [scratchpad/refund.json] # plus whatever archmax_advance attached via `evidence:`
96
+ transitions: # required; no self-edge; one target per labeled type
97
+ - { to: closed, type: approve, description: Approve and close. }
98
+ - { to: triage, type: refine, description: Send back with a comment. }
99
+
100
+ closed: # bare key = terminal state with no declarations
101
+ ```
102
+
103
+ ## Machine rules
104
+
105
+ - **Movement.** The agent moves only by `archmax_advance({ to, reason, evidence? })`
106
+ — one per message, edge validated, then the leaving state's `after` hooks,
107
+ the target's `before` hooks, then the target's tools unlock. A state with no
108
+ transitions is terminal; the run ends when the agent finishes there. Human
109
+ decisions and `on_error` are the other two movers. `archmax_reset({ reason })`
110
+ returns to the entry state (forbid it via `forbid_always` when a run must
111
+ never restart). No fan-out: sequence states; concurrency lives in
112
+ sub-workflow calls.
113
+ - **Entry.** At least one state declares `triggers`; one state per trigger id;
114
+ `manual` is the entry the CLI, a host firing and a caller all use. A
115
+ re-invoked finished session is a **new turn in the state it ended in**, not
116
+ at the entry — give a state re-entry transitions if a conversation must move
117
+ on, or park instead of finishing.
118
+ - **Variables.** One flat checkpointed `name → value` store, never declared.
119
+ Host seeds (`--variables`, `createAgent({ variables })`, a delivery) are
120
+ **locked**; the agent uses `archmax_get_variables`/`archmax_set_variables`
121
+ (`lock: true` to freeze). Reserved: `trigger` (current trigger id, always
122
+ locked) and `title` (the run's one-line task label, agent-owned). A
123
+ `${{name}}` / `${{a.b.-1.c}}` reference works in `tools.allow` globs
124
+ (guard) and inside any text tool argument the model writes (exact
125
+ substitution, no retyping) — but not in scripts, which read
126
+ `args.variables`. Name a guarded variable in an earlier state's `requires`
127
+ or seed it; an unresolvable guard reference fails the run, an unresolvable
128
+ argument reference is refused and retried. An `archmax_set_variables` typo
129
+ creates a new variable; `requires` is what surfaces it.
130
+ - **Models.** `settings.model` names the id every state runs on and a state's
131
+ `model` names its own — an **id only**, over the endpoint the assembly is
132
+ already configured with (a block with `temperature` is a load error;
133
+ sampling is env/host configuration). Precedence: state → workflow →
134
+ `ARCHMAX_MODEL`. Author it where the work differs in kind: small models for
135
+ classification, extraction and other mechanical states; leave drafting,
136
+ judgement and any state with a big tool surface or several transitions on
137
+ the default, since a small model that routes badly costs more in retries
138
+ than it saves. Repeating the workflow's id on a state is a validate warning.
139
+ A rubric keeps its own `model`, a sub-workflow uses its own spec's, and a
140
+ host that passes an explicit `model` instance to `createAgent` outranks
141
+ every declared id (it must pass `modelFactory` to honour them).
142
+ - **Parking.** A state that needs an event or a time calls
143
+ `archmax_wait({ reason, until? })` (`until`: `30m`/`2h`/`1d` or ISO instant →
144
+ `resumeAt` for the host's scheduler; the runtime holds no timer). Any
145
+ delivered trigger resumes **that same state** with the payload as locked
146
+ variables and an `[event]` runtime note; the state then picks its own edge.
147
+ Bound a polling loop with `budget.maxParks` + `on_error`. `archmax_wait` is
148
+ in every state, terminal ones included. Use a **human state** when a person
149
+ picks the edge, `archmax_wait` when an event or time resumes the work.
150
+ - **Human states** park with a record built from `instructions`, `evidence`
151
+ and transition `type`s. The handoff spends one reply-only model call so the
152
+ park carries a message — do **not** instruct the agent to announce the
153
+ handoff (two messages). While parked, `archmax reply` is answered on a
154
+ tool-free turn and re-parks unchanged; only `decide` moves the run. A
155
+ `refine` edge delivers the reviewer's comment as correction.
156
+ - **Hooks.** `before`: ok/veto (veto rejects entry; on a start state, the
157
+ run; routed through `on_error` if declared). `after`: ok/correct/veto on
158
+ the outgoing advance or on completion of a terminal state; `correct` sends
159
+ the agent back with the reason, bounded per hook by `max_iterations`
160
+ (default 0 → a correct is a hard veto). A list runs in order and stops at
161
+ the first non-ok. Errors and missing files veto (fail-closed). Hooks run
162
+ on runtime authority: the state's `allow`/`forbid`/`skills` do not bind
163
+ them; `forbid_always` and safety rules do. Human states may not declare
164
+ hooks.
165
+ - **Budgets and `on_error`.** `budget.maxTurns`/`timeoutMs`/`maxParks` bound
166
+ a state's segment. A terminal failure (hook execution error, exhausted
167
+ corrections, exhausted budget, exhausted parks) routes to `on_error`
168
+ instead of failing the run; recoverable rejections (invalid edge, veto,
169
+ in-budget correct) keep the agent in place. Declare `on_error` on states
170
+ with strict `after` hooks or bounded loops. A **tool that throws** is
171
+ neither: the call is answered with an error-status tool message carrying
172
+ the error, the agent stays in the state and can retry or switch tools, and
173
+ `on_error` never sees it (a park or a cancelled run still propagates).
174
+ - **Sub-workflows are tools.** Allow `archmax_workflow_<slug>` (slug verbatim)
175
+ and the sibling `workflows/<slug>/` is callable: its `manual` trigger's
176
+ `requires` are the parameters (typed where the entries are typed; a
177
+ mistyped argument is refused `invalid-param`, and `"${{count}}"` alone
178
+ passes the value with its own type), its `returns` the result
179
+ (`{ message, returns }`; a mistyped typed return fails `invalid-return`).
180
+ The trigger's `description` leads the tool description. Nothing captures the result — the caller records
181
+ what it needs with `archmax_set_variables`, made mandatory by `requires`.
182
+ The child is a separate session with a fresh transcript, sees only its
183
+ arguments (as locked variables), and shares no prose. One call = one
184
+ sub-run; fan out with parallel tool calls or `Promise.all` in a script
185
+ (bounds: depth 3, 4 concurrent). A child's human state parks the child;
186
+ a script's delegation cannot park and fails closed.
187
+ - **Choosing a mechanism.** Deterministic work with no model → an `archmax_run`
188
+ script. Delegated work that is itself a process or reused across workflows
189
+ → a sub-workflow. Judging work already done → a rubric hook (never
190
+ callable by the agent; `task` is disclosed nowhere and not grantable).
191
+ - **Prompt economy.** The static prompt + tool schemas are re-sent every
192
+ model call. The static part carries the workflow's `title` + `instructions`
193
+ and **no state of the graph**; a state's `instructions` and its **own**
194
+ outgoing edges are sent only while it is active. So: workflow-wide
195
+ conventions go in the root `instructions` (paid once, cached); everything
196
+ else goes in the state that needs it, and splitting a broad state into
197
+ several narrow ones costs nothing in the prefix. `title` and `summary` are
198
+ free — neither reaches a model call. `WORKFLOW.md` carries only what the spec
199
+ cannot say; reader diagrams go inside an HTML comment (stripped from the
200
+ prompt).
201
+
202
+ ## What the agent sees of the graph
203
+
204
+ Only the state it is in. Every model call is handed that state's outgoing edges
205
+ and nothing else of the machine — no other state's slug, `title`, `summary`,
206
+ hooks or transitions, and no count of the states:
207
+
208
+ ```
209
+ Transitions — choose one with `archmax_advance` when this state's work is done:
210
+ - to `review` (human decision node) — The decision is recorded and needs a person's approval.
211
+ - to `closed` (terminal) — Nothing to refund; the case is answered.
212
+ ```
213
+
214
+ Two rules follow, and they are the ones authors get wrong:
215
+
216
+ 1. **Write the `description` for the source state, not the target.** The agent
217
+ reading it knows nothing about where it is going. Say *when* the edge applies:
218
+ `The refund is over $50, or the customer has asked twice.` Not `Go to
219
+ refund-review.` — that tells a reader who already knows the graph something
220
+ they cannot use, and tells this reader nothing.
221
+ 2. **Routing conditions cannot live downstream.** A target's `summary` or
222
+ `instructions` is never disclosed to the state routing into it. If the
223
+ condition is "when the order is over 30 days old", it belongs on the edge.
224
+
225
+ A missing or blank `description` is a **load error**. There is no fallback, and
226
+ no way to see more of the graph.
227
+
228
+ ## Governance
229
+
230
+ Closed by default; **deny beats allow, and no narrower level widens a denial.**
231
+
232
+ - **Always on, never declared**: `ls`, `read_file`, `write_file`,
233
+ `edit_file`, `glob`, `grep`, `write_todos`, `archmax_eval`, `archmax_run`,
234
+ and the controls `archmax_advance`, `archmax_reset`, `archmax_wait`,
235
+ `archmax_get_variables`, `archmax_set_variables`. Any other tool needs an
236
+ `allow`/`allow_always` entry. An entry naming an always-on tool **narrows**
237
+ it. `task` is ungrantable (error).
238
+ - **Entries**: bare name; `{ tool, args: { <arg>: ["glob", …] } }` (matches
239
+ if any glob matches; `${{var}}` allowed); `{ tool, paths: [...] }`
240
+ (shorthand for `args.file_path`). `"*"` as the tool is legal only in a
241
+ forbid list. Globs match dot-segments (`secrets/**` covers `secrets/.env`).
242
+ - **Skills** are governed by **slug**, never path. Enabling a bundle makes
243
+ `skills/<slug>/**` readable and its scripts runnable with no `tools.allow`
244
+ entry; a disabled bundle is unreachable **and invisible** (not listed, not
245
+ in the prompt). `allow` adds, `forbid` subtracts, `allow: []` denies
246
+ nothing. A `tools.allow` entry may still narrow *within* an enabled bundle.
247
+ A state enabling no skill can run no script at all.
248
+ - **Scripts' `tools.*` calls** are governed per call: an `archmax_run` script
249
+ is bound like the model (grant every tool it calls); a hook is exempt from
250
+ the state's lists. Neither may read `workflows/**` or write a mount. A
251
+ refused call throws inside the script.