oh-my-workflow 0.2.0 → 0.4.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.
package/skill/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: oh-my-workflow
2
+ name: omw
3
3
  description: Use when a task decomposes into multiple coding-agent CLI calls (claude -p / codex exec) that should run as one structured, schema-gated, journaled workflow — fan-out search, verify-vote, pipeline, or loop-until-dry. Teaches you to author a plain-JS omw script, run it with `omw run`, read the JSONL journal, and repair your own script from structured failures.
4
4
  ---
5
5
 
@@ -9,11 +9,15 @@ You write a **plain-JS orchestration script**. Its nodes are whole coding-agent
9
9
  CLIs you already pay for (`claude -p`, `codex exec`). omw is the thin glue: it
10
10
  runs your script, schema-gates each node's output, and journals every step — so
11
11
  you can read your own failure and fix your own script. (What's "deterministic" is
12
- scoped below — the engine's guarantees and `--agent fake`, not your script.)
12
+ scoped below — the engine's guarantees and `--agent fake`, not your script unless
13
+ you pass `--strict`.)
13
14
 
14
- The runtime gives your script exactly **five hooks** (`agent` / `pipeline` /
15
- `parallel` / `phase` / `log`). That is the entire surface. There is no DSL to
16
- learn; everything else is ordinary JavaScript control flow.
15
+ omw is the **open twin of Claude Code's native dynamic Workflow**: the same
16
+ authoring shape and vocabulary (`agent` / `parallel` / `pipeline` / `workflow` /
17
+ `budget`), but the nodes are *external coding-agent CLIs*, it runs from any host,
18
+ and there is **no magic** — no source transform, no ambient globals, no
19
+ sandbox-by-default. Your script is ordinary JavaScript; the runtime hands it a
20
+ **hooks object** as the first argument. There is no DSL to learn.
17
21
 
18
22
  ## When to use this
19
23
 
@@ -24,63 +28,102 @@ benefits from structure you'd otherwise hand-roll:
24
28
  - **Verify / vote**: produce a finding, then have K independent agents judge it.
25
29
  - **Pipeline**: each item flows scope → search → verify → synthesize independently.
26
30
  - **Loop-until-dry**: keep spawning finders until a round returns nothing new.
31
+ - **Budget-bounded loop**: keep working until a token ceiling is reached.
27
32
 
28
33
  You want: bounded concurrency, schema-validated node output with automatic
29
34
  node-level retry, a replayable journal, and a `null`-on-failure contract so one
30
35
  bad node never crashes the run.
31
36
 
32
- **Don't** use omw for a single agent call, or for work that needs a sandbox
33
- (omw deliberately has none your script is trusted code), or where a node is a
34
- single raw LLM API call (that's LangGraph/Mastra territory; an omw node is a
35
- *whole coding agent*).
37
+ **Don't** use omw for a single agent call, or where a node is a single raw LLM
38
+ API call (that's LangGraph/Mastra territory; an omw node is a *whole coding
39
+ agent*). omw has no sandbox by default your script is trusted code — though you
40
+ can opt into a determinism sandbox with `--strict`.
36
41
 
37
- ## The 30-second free demo (no API key)
42
+ ## The 30-second free demo (no API key, nothing to clone)
43
+
44
+ omw is on npm, so you can run the whole thing in one line — no install step, no
45
+ key, no cost:
38
46
 
39
47
  ```sh
40
- git clone <repo-url> && cd oh-my-workflow # repo not yet public; fill in the URL
41
- bun install
42
- bun src/cli/omw.ts run examples/deep-research --agent fake
43
- # → {"confirmed":[…],"summary":{…}} exit 0 · no key · no cost · `--agent fake` is deterministic
48
+ bunx github:domuk-k/oh-my-workflow run examples/deep-research --agent fake
49
+ # → {"confirmed":[…],"summary":{…}} exit 0 · no key · no cost · deterministic
44
50
  ```
45
51
 
46
- `--agent fake` is a built-in deterministic adapter: it runs the full spine
47
- (fan-out + pipeline + a scripted schema-fail→self-repair + a scripted
48
- timeout→drop) and prints one result JSON. Add `--pretty` to see the phase/fan-out
49
- tree on stderr. Swap `--agent claude` once you've run `claude login`.
52
+ > Tip: the GitHub source keeps the skill and runtime aligned before a new npm
53
+ > release lands.
54
+
55
+ That single command runs the **whole spine** for you a fan-out search, a
56
+ pipeline, a scripted schema-fail→self-repair, and a scripted timeout→drop — and
57
+ prints one result JSON. Want to watch it happen? Add `--pretty` for the
58
+ phase/fan-out tree on stderr:
59
+
60
+ ```sh
61
+ bunx github:domuk-k/oh-my-workflow run examples/deep-research --agent fake --pretty
62
+ ```
50
63
 
51
- > Once published this is `bunx oh-my-workflow run …`; today run the bin directly
52
- > from a clone as shown above.
64
+ `--agent fake` is a built-in, deterministic adapter it's the no-key demo engine
65
+ and the test double. For real work, write the workflow and run `omw run <file>`;
66
+ the CLI defaults to `--agent auto`, choosing the current/installed coding-agent
67
+ CLI. Set `OMW_AGENT=claude|codex|hermes` only when you need to pin it.
68
+
69
+ > **Reading this as a skill?** You already have it. To install/update it for a
70
+ > coding agent: `bunx github:domuk-k/oh-my-workflow skill install` (→ `~/.claude/skills/`;
71
+ > `--codex` → `~/.codex/skills/`; `--opencode` → `~/.config/opencode/skills/`;
72
+ > `--project` for one repo). `omw skill path` prints the bundled copy for other
73
+ > hosts. Re-run `skill install` anytime to refresh.
53
74
 
54
75
  ---
55
76
 
56
- ## The 5 hooks (the entire API)
77
+ ## The hooks (the entire API)
57
78
 
58
- Your script is a module that **default-exports** `async (rt, args) => result`.
59
- `rt` is the runtime; `args` is whatever `--args '{…}'` passed (parsed JSON).
60
- The returned value is serialized to stdout as the run's single result JSON.
79
+ Your script is a module that **default-exports** a function taking the **hooks**
80
+ as a destructured first argument and your `args` second:
61
81
 
62
82
  ```ts
63
- export default async function (rt, args) {
64
- // rt.agent / rt.pipeline / rt.parallel / rt.phase / rt.log
83
+ export default async function ({ agent, parallel, pipeline, phase, log, workflow, budget }, args) {
84
+ // destructure only the hooks you use
65
85
  return { /* whatever you want on stdout */ };
66
86
  }
67
87
  ```
68
88
 
69
- ### `rt.agent(prompt, opts?) => Promise<result | null>`
89
+ `args` is whatever `--args '{…}'` passed (parsed JSON). The returned value is
90
+ serialized to stdout as the run's single result JSON. (Legacy `(rt, args)` scripts
91
+ that call `rt.agent(…)` still run — the same object is passed — but they're
92
+ deprecated; run `omw codemod <file>` to migrate. The bridge is removed in 0.5.)
70
93
 
71
- Runs one coding-agent CLI node. **Never throws.** A terminal failure resolves to
72
- `null` (and is journaled with a failure `kind`). This is the load-bearing
73
- **null-contract** — build on it with `filter(Boolean)` and abstain quorums.
94
+ Optionally declare a `meta` block (a pure literal, like native):
74
95
 
75
96
  ```ts
76
- const out = await rt.agent("SCOPE the question into topics", {
97
+ export const meta = {
98
+ name: "deep-research",
99
+ description: "fan-out research with verify",
100
+ phases: [{ title: "Search", model: "smart" }, { title: "Verify" }],
101
+ };
102
+ ```
103
+
104
+ `meta.phases[].model` and `meta.model` set a default model per phase / for the
105
+ run; the effective model resolves along **`opts.model > phase model > meta.model`**.
106
+
107
+ ### `agent(prompt, opts?) => Promise<result | null>`
108
+
109
+ Runs one coding-agent CLI node. **Never throws** (the one exception is `budget`
110
+ exhaustion — see below). A terminal failure resolves to `null` (and is journaled
111
+ with a failure `kind`). This is the load-bearing **null-contract** — build on it
112
+ with `filter(Boolean)` and abstain quorums.
113
+
114
+ ```ts
115
+ const out = await agent("SCOPE the question into topics", {
77
116
  schema: { type: "object", required: ["topics"], properties: { topics: { type: "array" } } },
78
- label: "scope", // shows in the journal / --pretty tree
79
- phase: "Scope", // overrides the ambient phase() for this call
80
- model: "smart", // tier alias or raw model string, passed to the adapter
81
- timeoutMs: 120_000, // kill the subprocess after this; failure kind = "timeout"
82
- cwd: "/path/to/repo", // run the agent in this directory
83
- maxRetries: 2, // schema-gate retries (default 2 up to 3 attempts)
117
+ label: "scope", // shows in the journal / --pretty tree (cosmetic; not in resume key)
118
+ phase: "Scope", // overrides the ambient phase() for this call (cosmetic)
119
+ model: "smart", // tier alias or raw model string, passed to the adapter
120
+ effort: "high", // reasoning-effort hint: low|medium|high|xhigh|max (adapter maps it where supported)
121
+ agentType: "Explore", // cross-vendor node profile (named agent persona)
122
+ isolation: "worktree", // run this node in a fresh ephemeral git worktree (cwd = the worktree)
123
+ timeoutMs: 120_000, // kill the subprocess after this; failure kind = "timeout"
124
+ cwd: "/path/to/repo", // run the agent in this directory
125
+ maxRetries: 2, // schema-gate retries (default 2 → up to 3 attempts)
126
+ inheritMcp: false, // default: isolate from host MCP servers (fast). true = inherit (claude only; codex ignores)
84
127
  });
85
128
  ```
86
129
 
@@ -92,8 +135,14 @@ const out = await rt.agent("SCOPE the question into topics", {
92
135
  structured outcome. The schema is plain JSON Schema.
93
136
  - **Without `schema`**: one shot; returns the raw text string, or `null` on
94
137
  adapter failure.
138
+ - `effort`/`agentType` are passed through to adapters that support them; the
139
+ `claude` adapter has no faithful CLI flag for them yet, so it **drops them with
140
+ a one-time warn** (honest-scope) rather than silently pretending.
141
+ - `isolation: "worktree"` gives the node its own ephemeral `git worktree` as cwd,
142
+ so parallel file-mutating nodes don't clobber each other; the worktree is
143
+ auto-removed if the node left it clean. A non-git cwd runs in place with a warn.
95
144
 
96
- ### `rt.parallel(thunks) => Promise<any[]>` — barrier
145
+ ### `parallel(thunks) => Promise<any[]>` — barrier
97
146
 
98
147
  Runs thunks concurrently, awaits **all** of them. A thunk that throws (or whose
99
148
  agent fails) becomes `null` in the result array — the call itself never rejects.
@@ -101,12 +150,12 @@ agent fails) becomes `null` in the result array — the call itself never reject
101
150
  together (dedup, count, cross-comparison).
102
151
 
103
152
  ```ts
104
- const results = (await rt.parallel(
105
- topics.map((t) => () => rt.agent(`SEARCH ${t}`, { schema: S, label: `search:${t}` })),
153
+ const results = (await parallel(
154
+ topics.map((t) => () => agent(`SEARCH ${t}`, { schema: S, label: `search:${t}` })),
106
155
  )).filter(Boolean);
107
156
  ```
108
157
 
109
- ### `rt.pipeline(items, ...stages) => Promise<any[]>` — no barrier (default)
158
+ ### `pipeline(items, ...stages) => Promise<any[]>` — no barrier (default)
110
159
 
111
160
  Runs each item through **all** stages independently. Item A can be in stage 3
112
161
  while item B is still in stage 1 — wall-clock is the slowest single chain, not
@@ -116,16 +165,49 @@ default for multi-stage work; only use `parallel` as a barrier when a stage
116
165
  genuinely needs the whole previous result set at once.
117
166
 
118
167
  ```ts
119
- const verified = (await rt.pipeline(
168
+ const verified = (await pipeline(
120
169
  found,
121
170
  async (f) => {
122
- const v = await rt.agent(`VERIFY ${JSON.stringify(f)}`, { schema: V });
171
+ const v = await agent(`VERIFY ${JSON.stringify(f)}`, { schema: V });
123
172
  return v ? { ...f, ...v } : null; // null → dropped by the filter below
124
173
  },
125
174
  )).filter(Boolean);
126
175
  ```
127
176
 
128
- ### `rt.phase(title)` and `rt.log(msg)`
177
+ ### `workflow(ref, args?) => Promise<result>` — nested sub-workflow
178
+
179
+ Runs another workflow inline as a sub-step, **one level deep**, sharing this run's
180
+ adapter, journal, and budget pool. `ref` is a path string or `{ scriptPath }`.
181
+
182
+ ```ts
183
+ const sub = await workflow({ scriptPath: "./refine.ts" }, { topic });
184
+ ```
185
+
186
+ A `workflow()` call **inside** a child throws (`"workflow() nesting is one level
187
+ only"`) — a runaway-recursion backstop.
188
+
189
+ ### `budget` — token ceiling
190
+
191
+ `budget` is `{ total, spent(), remaining() }`. Set a ceiling with `--budget N`;
192
+ `total` is `null` when unset and `remaining()` is then `Infinity`. Once spent
193
+ reaches `total`, `agent()` **throws `BudgetExceededError`** — the *one* documented
194
+ exception to the null-contract — so a bounded loop terminates instead of spinning.
195
+ A throw inside `parallel`/`pipeline` is still swallowed to `null` (matches native).
196
+
197
+ ```ts
198
+ const out = [];
199
+ while (budget.remaining() > 50_000) { // guard, or let agent() throw at the ceiling
200
+ const r = await agent("find the next bug");
201
+ if (r) out.push(r);
202
+ }
203
+ ```
204
+
205
+ > `budget` counts **output tokens the adapter reports** (success or a failure
206
+ > envelope that carries `usage`). A token-less failure (a killed timeout reports
207
+ > no usage) can't be counted — so a loop on a purely-timing-out node isn't bounded
208
+ > by `--budget` alone; pair it with your own iteration cap.
209
+
210
+ ### `phase(title)` and `log(msg)`
129
211
 
130
212
  `phase` groups subsequent `agent()` calls under a heading in the journal and the
131
213
  `--pretty` tree. `log` emits a narration line. Both are side-channel only — they
@@ -144,10 +226,10 @@ pass hundreds of items — only ~N agent subprocesses run at once; the rest queu
144
226
  ### Fan-out (barrier)
145
227
 
146
228
  ```ts
147
- export default async function (rt, args) {
148
- rt.phase("Search");
149
- const hits = (await rt.parallel(
150
- args.queries.map((q) => () => rt.agent(`SEARCH: ${q}`, { schema: HIT, label: `q:${q}` })),
229
+ export default async function ({ agent, parallel, phase }, args) {
230
+ phase("Search");
231
+ const hits = (await parallel(
232
+ args.queries.map((q) => () => agent(`SEARCH: ${q}`, { schema: HIT, label: `q:${q}` })),
151
233
  )).filter(Boolean);
152
234
  return { hits, count: hits.length };
153
235
  }
@@ -160,10 +242,10 @@ Count only real verdicts, and require a quorum of *cast* votes so an all-abstain
160
242
  finding doesn't silently survive.
161
243
 
162
244
  ```ts
163
- async function survives(rt, claim) {
164
- const votes = (await rt.parallel(
245
+ async function survives({ agent, parallel }, claim) {
246
+ const votes = (await parallel(
165
247
  [1, 2, 3].map(() => () =>
166
- rt.agent(`Try to REFUTE this claim. Default to refuted=true if unsure: ${claim}`, {
248
+ agent(`Try to REFUTE this claim. Default to refuted=true if unsure: ${claim}`, {
167
249
  schema: { type: "object", required: ["refuted"], properties: { refuted: { type: "boolean" } } },
168
250
  })),
169
251
  )).filter(Boolean); // drop abstainers (null)
@@ -172,11 +254,11 @@ async function survives(rt, claim) {
172
254
  }
173
255
  ```
174
256
 
175
- **Fresh context is the point — not self-critique.** Each `rt.agent()` call is a
176
- brand-new `claude -p` subprocess with no memory of the producer's turn, so a
177
- verify-vote node judges the claim cold. That is the structural form of Anthropic's
178
- own guidance for its most capable model: *"Separate, fresh-context verifier
179
- subagents tend to outperform self-critique"* ([Fable 5 prompting
257
+ **Fresh context is the point — not self-critique.** Each `agent()` call is a
258
+ brand-new subprocess with no memory of the producer's turn, so a verify-vote node
259
+ judges the claim cold. That is the structural form of Anthropic's own guidance for
260
+ its most capable model: *"Separate, fresh-context verifier subagents tend to
261
+ outperform self-critique"* ([Fable 5 prompting
180
262
  guide](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5)).
181
263
  omw gets it for free — **as long as you keep verification a separate `agent()`
182
264
  call.** Do **not** verify by feeding the result back into the producer's own
@@ -184,10 +266,9 @@ session: the schema-gate's in-session self-repair (the `--resume` / `followUp`
184
266
  path) deliberately *reuses* the producer's context to fix output **format**, which
185
267
  is the exact opposite of fresh-context verification. Use self-repair to make a
186
268
  node's JSON valid; use a new `agent()` to judge whether the *content* is true.
187
- (A **cross-CLI** verifier — a different agent CLI than the producer, so a shared
188
- memorized shortcut can't survive both — is a natural extension but **not a feature
189
- today**: omw binds one adapter per run, so per-node verifier selection is future
190
- work. Verify with fresh same-CLI nodes for now.)
269
+ (A **cross-CLI** verifier — a different agent CLI than the producer, via per-node
270
+ `agentType` / a different adapter — is a natural extension but **not a feature
271
+ today**: omw binds one adapter per run. Verify with fresh same-CLI nodes for now.)
191
272
 
192
273
  ### Gate on evidence, not intent
193
274
 
@@ -215,7 +296,7 @@ const strong = {
215
296
  output: { type: "string" }, // the observed tail, not a claim about it
216
297
  },
217
298
  };
218
- const built = await rt.agent("Run the build and the test suite. Report the command, its exit code, the number of passing tests, and the tail of the output.", { schema: strong });
299
+ const built = await agent("Run the build and the test suite. Report the command, its exit code, the number of passing tests, and the tail of the output.", { schema: strong });
219
300
  ```
220
301
 
221
302
  **Executable-evidence verify node** — combine this with fresh-context verification:
@@ -223,11 +304,11 @@ a separate node *runs what the producer built and observes the result* before th
223
304
  finding is accepted, rather than judging the producer's description of it.
224
305
 
225
306
  ```ts
226
- const verified = (await rt.pipeline(
307
+ const verified = (await pipeline(
227
308
  artifacts,
228
309
  async (a) => {
229
310
  // a.path was written by an upstream node; this fresh node runs it and reports facts.
230
- const v = await rt.agent(
311
+ const v = await agent(
231
312
  `Run \`${a.runCmd}\` in ${a.path}. Report exitCode and the output tail. Do not fix anything — only observe and report.`,
232
313
  { schema: { type: "object", required: ["exitCode", "output"], properties: { exitCode: { type: "number" }, output: { type: "string" } } } },
233
314
  );
@@ -239,10 +320,10 @@ const verified = (await rt.pipeline(
239
320
  ### Pipeline (no barrier)
240
321
 
241
322
  ```ts
242
- const out = (await rt.pipeline(
323
+ const out = (await pipeline(
243
324
  items,
244
- (item) => rt.agent(`ANALYZE ${item.id}`, { schema: A, label: `analyze:${item.id}` }),
245
- (analysis, item) => (analysis ? rt.agent(`SUMMARIZE ${item.id}: ${JSON.stringify(analysis)}`, { schema: S }) : null),
325
+ (item) => agent(`ANALYZE ${item.id}`, { schema: A, label: `analyze:${item.id}` }),
326
+ (analysis, item) => (analysis ? agent(`SUMMARIZE ${item.id}: ${JSON.stringify(analysis)}`, { schema: S }) : null),
246
327
  )).filter(Boolean);
247
328
  ```
248
329
 
@@ -253,8 +334,8 @@ For unknown-size discovery: keep going until K consecutive rounds find nothing n
253
334
  ```ts
254
335
  const seen = new Set(); const found = []; let dry = 0;
255
336
  while (dry < 2) {
256
- const round = (await rt.parallel(
257
- FINDERS.map((f) => () => rt.agent(f.prompt, { schema: BUG })),
337
+ const round = (await parallel(
338
+ FINDERS.map((f) => () => agent(f.prompt, { schema: BUG })),
258
339
  )).filter(Boolean);
259
340
  const fresh = round.filter((b) => !seen.has(b.key));
260
341
  if (fresh.length === 0) { dry++; continue; }
@@ -262,12 +343,25 @@ while (dry < 2) {
262
343
  }
263
344
  ```
264
345
 
346
+ ### Loop-until-budget
347
+
348
+ Scale depth to a token ceiling — guard on `budget.total` so an unset budget
349
+ (`remaining()` = `Infinity`) doesn't loop forever.
350
+
351
+ ```ts
352
+ const bugs = [];
353
+ while (budget.total && budget.remaining() > 50_000) {
354
+ const r = await agent("Find one more bug.", { schema: BUG });
355
+ if (r) bugs.push(r);
356
+ }
357
+ ```
358
+
265
359
  ---
266
360
 
267
361
  ## The run → journal → fix loop (this is the UX)
268
362
 
269
363
  ```sh
270
- bun src/cli/omw.ts run my-workflow.ts --agent claude --args '{"q":"…"}'
364
+ bunx github:domuk-k/oh-my-workflow run my-workflow.ts --args '{"q":"…"}' --pretty
271
365
  ```
272
366
 
273
367
  - **stdout** = the result JSON, one blob. Pipe it, parse it.
@@ -280,13 +374,15 @@ bun src/cli/omw.ts run my-workflow.ts --agent claude --args '{"q":"…"}'
280
374
  | code | meaning | where the detail is |
281
375
  |---|---|---|
282
376
  | `0` | run completed (node failures are absorbed by the null-contract) | stdout = result JSON |
283
- | `1` | **script error** — your JS threw, or syntax/load failure | stderr: `{"error":"script_error"\|"load_failed",…}` |
377
+ | `1` | **script error** — your JS threw (incl. `BudgetExceededError`), or syntax/load failure | stderr: `{"error":"script_error"\|"load_failed",…}` |
284
378
  | `2` | usage error (bad flags) | stderr: usage line |
285
379
  | `3` | adapter CLI not on PATH | stderr: `{"error":"adapter_missing","install_hint":…}` |
380
+ | `4` | completed, but a node hit `internal_error` (author bug, e.g. invalid schema) | stdout = partial result; stderr: `{"error":"internal_error_nodes",…}` |
286
381
 
287
382
  Exit `1` means **your script** threw (an `agent()` returning `null` does *not*
288
- throw — only your own code does). Exit `0` with fewer results than expected means
289
- nodes failed and were filtered — read the journal.
383
+ throw — only your own code, or an uncaught `BudgetExceededError`, does). Exit `0`
384
+ with fewer results than expected means nodes failed and were filtered — read the
385
+ journal.
290
386
 
291
387
  ### Reading a journal
292
388
 
@@ -326,33 +422,33 @@ Failure `kind`s on `agent_end`:
326
422
  `omw replay .omw/<runId>.jsonl [--json]` reconstructs the tree / a stats summary
327
423
  from a journal — a read-only **fixture replay** (reading back what a run
328
424
  recorded). For *live* resume (re-running nodes whose key changed, reusing the
329
- cached ones), use `omw run <wf> --resume <journal>` — see Scope below.
425
+ cached ones), use `omw run <wf> --resume <journal|runId>` — see Scope below.
330
426
 
331
427
  `omw validate <wf> [--json]` is a pre-flight that loads the module and lints a
332
428
  `fake` fixture for the silent-degradation traps (top-level `responses`, a string
333
429
  `match`, no rules+default) **without spawning agents** — exit 0 clean, 1 on a
334
- load/fixture problem. And a node that throws an `internal_error` (e.g. a JSON
335
- Schema that won't compile) no longer hides behind the null-contract: the run
336
- escalates to **exit 4** (the partial result still prints to stdout, and a
337
- `{"error":"internal_error_nodes","calls":[…]}` line goes to stderr), so an author
338
- bug reads differently from a flaky node abstaining.
430
+ load/fixture problem.
339
431
 
340
432
  ---
341
433
 
342
434
  ## Conventions (follow these)
343
435
 
344
- 1. **Build on the null-contract.** `agent()` returns `null`, never throws.
345
- `.filter(Boolean)` after every `parallel`/`pipeline`. For votes, require a
346
- quorum of *cast* (non-null) results so all-abstain can't pass.
436
+ 1. **Build on the null-contract.** `agent()` returns `null`, never throws (except
437
+ `BudgetExceededError` at the ceiling). `.filter(Boolean)` after every
438
+ `parallel`/`pipeline`. For votes, require a quorum of *cast* (non-null) results
439
+ so all-abstain can't pass.
347
440
  2. **Always pass a `schema` when you need structured data.** The gate's
348
441
  self-repair is the one genuine differentiator — use it instead of parsing
349
442
  prose yourself. Keep schemas tight (`required` + types).
350
443
  3. **Stay deterministic.** Don't branch the *shape* of the run on `Date.now()` /
351
- `Math.random()` / wall-clock. The resume key is `(callIndex, promptHash,
352
- optsHash)` (the journaled field is `call`); if a re-run's `agent()` call order shifts, every key shifts and
353
- resume breaks. Vary content by index, not by randomness. (omw can't *enforce*
354
- this no sandbox so it's a convention you keep; enforcement is v2.)
355
- 4. **stdout is for the machine.** Return your result; use `rt.log` / `--pretty`
444
+ `Math.random()` / wall-clock. The resume key is the **semantic** subset of
445
+ `(callIndex, promptHash, optsHash)` cosmetic `label`/`phase` changes don't
446
+ bust the cache, but `model`/`schema`/`effort`/`isolation` do. If a re-run's
447
+ `agent()` call order shifts, every key shifts and resume breaks; vary content
448
+ by index, not by randomness. omw can't enforce determinism by default (no
449
+ sandbox) — but pass **`--strict`** to freeze `Date`/`Math.random` to throw for
450
+ a reproducible run.
451
+ 4. **stdout is for the machine.** Return your result; use `log` / `--pretty`
356
452
  for humans. Never `console.log` to stdout from a workflow.
357
453
  5. **Ship a `fake` fixture for your example.** Export `const fake` alongside your
358
454
  default export so `--agent fake` runs deterministically with no key. The shape:
@@ -363,7 +459,8 @@ bug reads differently from a flaky node abstaining.
363
459
  // `responses` is a cursor that advances per invocation and sticks on the last —
364
460
  // so [invalidJSON, validJSON] models a schema self-repair, and a single
365
461
  // { fail } models a hard failure. A FakeResponse is { text } (a raw JSON
366
- // STRING the gate then extracts + validates) or { fail, stderr }.
462
+ // STRING the gate then extracts + validates) or { fail, stderr }. Either may
463
+ // carry { outputTokens } to drive budget tests.
367
464
  rules: [
368
465
  { match: (p) => p.includes("SCOPE"), responses: [{ text: '{"topics":["a","b"]}' }] },
369
466
  { match: (p) => p.includes("SEARCH a"),
@@ -377,7 +474,7 @@ bug reads differently from a flaky node abstaining.
377
474
  Common mistake: a top-level `responses` array (instead of `rules`) or a string
378
475
  `match` is silently ignored — every node then returns `default` and the demo
379
476
  degenerates to an empty result. See `examples/deep-research/workflow.ts` for a
380
- full working fixture.
477
+ full working fixture, and `conformance/*.ts` for native-shaped samples.
381
478
 
382
479
  ---
383
480
 
@@ -389,31 +486,41 @@ agents that expose such a CLI can be nodes.
389
486
  | adapter | status | invoke | structured out | in-session follow-up |
390
487
  |---|---|---|---|---|
391
488
  | **fake** | built-in, free, deterministic | in-process fixtures | as scripted | yes (fixture) |
392
- | **claude** | **full** (live-verified, claude 2.1.177) | `claude -p <p> --output-format json` | parse `.result` | `--resume` |
393
- | **codex** | **experimental** (live-verified, codex 0.137.0) | `codex exec --json -s workspace-write` | last `agent_message` from JSONL | `exec resume` |
489
+ | **claude** | **full** (live-verified, claude 2.1.x) | `claude -p <p> --output-format json --strict-mcp-config` | parse `.result` | `--resume` (same cwd) |
490
+ | **codex** | **experimental** (live-verified, codex 0.137.x) | `codex exec --json -s workspace-write` | last `agent_message` from JSONL | `exec resume` (same cwd) |
491
+ | **hermes** | **experimental** | `hermes -z <prompt> --yolo` | stdout IS the response (heuristic JSON extract) | — (fresh retries) |
394
492
  | **pi** | planned | `pi --print` | stdout | — |
395
493
  | **kiro** | **not a fit** | — | — | — |
396
494
 
397
495
  > The "in-session follow-up" column is the adapter flag the **schema gate** uses to
398
496
  > re-prompt a node in the same session — *not* run-level resume. Run-level resume
399
- > (skipping unchanged nodes across separate runs) is **v2**; see Honest scope below.
497
+ > (`--resume`, skipping unchanged nodes across runs) is a separate path.
400
498
 
401
499
  - **claude** renames its envelope onto omw's contract (`session_id→sessionId`,
402
- `total_cost_usd→costUsd`, `duration_ms→durationMs`; `is_error`/non-success
403
- `subtype` → `ok:false`).
500
+ `total_cost_usd→costUsd`, `duration_ms→durationMs`, `usage.output_tokens→
501
+ outputTokens`; `is_error`/non-success `subtype` → `ok:false`). By default a node
502
+ runs **isolated from the host's MCP servers** (`--strict-mcp-config`) — booting
503
+ figma/devtools/etc. on every node is the dominant fan-out latency, and a
504
+ coding-agent node rarely needs them. Opt back in per call with `{ inheritMcp:
505
+ true }`. `opts.effort`/`opts.agentType` have no faithful `claude -p` flag yet, so
506
+ they're **dropped with a one-time warn** rather than silently honored. The
507
+ schema-gate `--resume` runs in the **same cwd** as the original invoke and
508
+ **mirrors the same MCP choice**.
404
509
  - **codex** is experimental: it has **no cost field** (tokens only, so `costUsd`
405
510
  stays undefined), and its JSONL can include malformed lines under MCP
406
511
  (openai/codex#15451) — omw tolerates them line-by-line and fails *actionably*
407
- (surfacing the reason) rather than returning empty. Default sandbox is
408
- `workspace-write`.
409
- - **pi** isn't wired yet (not installed locally `--agent pi` returns exit 3
410
- with an install hint). It's a planned experimental adapter.
411
- - **kiro is excluded on purpose**: its CLI is a VS-Code-based IDE launcher (open
412
- files, diffs, extensions), with no headless promptresult interface so it
413
- can't be an omw node. The bar for an adapter is a real headless execution CLI.
512
+ rather than returning empty. Default sandbox is `workspace-write`.
513
+ - **hermes** is experimental: `-z/--oneshot` prints only the response text, so the
514
+ result is stdout (no JSON envelope; schema-gate extracts JSON heuristically).
515
+ `--yolo` runs it non-interactively. No in-session followUp (no session id on
516
+ stdout) schema retries use fresh invokes. No cost field.
517
+ - **pi** isn't wired yet (`--agent pi` exit 3 with an install hint).
518
+ - **kiro is excluded on purpose**: its CLI is a VS-Code-based IDE launcher, with
519
+ no headless prompt→result interface — so it can't be an omw node.
414
520
 
415
521
  Missing CLI → exit 3 with `install_hint`. Run `--agent fake` any time for the
416
- free path.
522
+ free path. `--agent auto` is the default: it honors `OMW_AGENT`, then host
523
+ environment hints, then installed CLIs (`claude`, `codex`, `hermes`).
417
524
 
418
525
  ---
419
526
 
@@ -438,54 +545,57 @@ self-repair loop, which is the one piece a "subprocess + for-loop" doesn't have.
438
545
 
439
546
  ### Resemblance ledger (vs the CC dynamic-workflow surface)
440
547
 
441
- **✅ Genuinely the same idea** — model-authored plain-JS orchestration; the
442
- 5-hook shape (`agent`/`pipeline`/`parallel`/`phase`/`log`); `null`-resolution +
443
- `filter(Boolean)`; schema-forced structured output; a step-by-step journal;
444
- resume key `(callIndex, promptHash, optsHash)` (frozen and **proven byte-stable**
445
- across re-runs); **live resume** via `omw run --resume <journal>` — a **per-node
446
- key match** (cached nodes skip the adapter, `agent_end{cached:true}`; nodes whose
447
- key changed re-run; verified end-to-end on `--agent fake`).
548
+ **✅ Genuinely the same idea** — model-authored plain-JS orchestration with the
549
+ destructured-DI shape; the native vocabulary `agent`/`parallel`/`pipeline`/
550
+ `phase`/`log`/`workflow`/`budget`; an optional `meta`/`phases` block with model
551
+ precedence; `null`-resolution + `filter(Boolean)`; schema-forced structured
552
+ output; `agent` opts `effort`/`agentType`/`isolation:'worktree'`; `budget` with a
553
+ shared spend pool and a `BudgetExceededError` ceiling; nested `workflow()` (one
554
+ level); a step-by-step journal; the resume key `(callIndex, promptHash,
555
+ optsHash)` (frozen, byte-stable, and keyed on the **semantic** opts subset);
556
+ **live resume** via `omw run --resume <journal|runId>`; and an opt-in `--strict`
557
+ determinism sandbox.
448
558
 
449
559
  > One honest altitude difference even here: a CC Workflow node is a single
450
560
  > in-harness subagent; an **omw node is a whole external coding-agent CLI**
451
- > subprocess. Same orchestration shape, heavier nodes.
561
+ > subprocess. Same orchestration shape, heavier nodes. And the no-magic stance is
562
+ > deliberate: omw runs your script as-is (no source transform), hands hooks as an
563
+ > argument (no ambient globals), and leaves determinism opt-in (`--strict`).
452
564
 
453
565
  **🟡 Designed-but-scoped** —
454
- - *Determinism enforcement*: CC throws on `Date.now`/`Math.random`; omw treats it
455
- as a **convention** (no sandbox), so live resume holds **only for workflows that
456
- keep it**. A guard that *enforces* it in resume mode is v2.
457
- - *Resume is per-node, not dependency-aware*: it matches `(callIndex, promptHash,
458
- optsHash)`, so an upstream edit invalidates a downstream node **only if** that
459
- output is threaded into the downstream prompt/opts. This is deliberate — it
460
- preserves **parallel/pipeline sibling cache** (independent fan-out nodes aren't
461
- forced live just because an earlier sibling changed). **The trap**: an omw node
462
- is a whole coding-agent CLI that works on the **filesystem**, so "node 1 writes
463
- files, node 2 reads them" is the *normal* coding-agent idiom not an exotic
464
- anti-pattern — and that channel is invisible to the key. Edit node 1 → on resume
465
- it re-runs and writes different files, but node 2 **hits its cache and serves a
566
+ - *Determinism enforcement*: native throws on `Date.now`/`Math.random` always;
567
+ omw makes it **opt-in** via `--strict` (the rest of the time it's a convention).
568
+ - *Resume is per-node, not dependency-aware*: it matches the semantic
569
+ `(callIndex, promptHash, optsHash)`, so an upstream edit invalidates a
570
+ downstream node **only if** that output is threaded into the downstream
571
+ prompt/opts. This is deliberate — it preserves **parallel/pipeline sibling
572
+ cache**. **The trap**: an omw node is a whole coding-agent CLI that works on the
573
+ **filesystem**, so "node 1 writes files, node 2 reads them" is the *normal*
574
+ idiom and that channel is invisible to the key. Edit node 1 → on resume it
575
+ re-runs and writes different files, but node 2 **hits its cache and serves a
466
576
  summary of the old files** (silently stale). Remedies: (a) re-run fresh (drop
467
- `--resume`) when an upstream's filesystem effects changed, or (b) thread a
468
- content digest of the changed files into the downstream prompt so its hash moves.
469
- An opt-in `--strict-resume` (prefix truncation: force every node after the first
470
- key MISS live correct cascade for *linear* workflows, but over-invalidates
471
- *parallel* siblings) and a dependency-aware cascade are both **v2** candidates;
472
- per-node stays the default precisely because it keeps the parallel cache.
473
-
474
- **❌ Not implemented (CC Workflow has these; omw v1 does not)** — `budget`
475
- (token-target loops), nested `workflow()` (running another workflow inline), a
476
- `meta`/`phases` declaration block, `opts.agentType` (custom subagent types),
477
- `opts.effort`, `run_in_background`, and `isolation: 'worktree'`. Don't write
478
- scripts that assume these.
577
+ `--resume`), or (b) thread a content digest of the changed files into the
578
+ downstream prompt so its hash moves. A dependency-aware cascade is v2.
579
+ - *`budget` counts reported output tokens only*: a token-less failure (a killed
580
+ timeout) can't be counted, so pair `--budget` with your own iteration cap when a
581
+ node may fail without producing tokens.
582
+
583
+ **❌ Not implemented** (native has these; omw does not) — `run_in_background`
584
+ (async node scheduling), and per-node verifier selection across *different*
585
+ adapters in one run (omw binds one adapter per run; `agentType` is passed through
586
+ but cross-CLI routing is future work). Don't write scripts that assume these.
479
587
 
480
588
  ---
481
589
 
482
590
  ## Quick reference
483
591
 
484
- - Module: `export default async (rt, args) => result` · optional `export const fake`.
592
+ - Module: `export default async ({ agent, parallel, pipeline, phase, log, workflow, budget }, args) => result` · optional `export const meta` / `export const fake`. (Legacy `(rt, args)` still runs; `omw codemod <file>` migrates it.)
485
593
  - Path resolves a directory to `workflow.ts` / `workflow.js` / `index.ts` / `index.js`.
486
- - `omw run <wf> --agent <fake|claude|codex|pi> [--args JSON] [--concurrency N] [--resume <journal.jsonl>] [--pretty]`
594
+ - `omw run <wf> [--agent <auto|fake|claude|codex|hermes|pi>] [--args JSON] [--concurrency N] [--budget N] [--resume <journal|runId>] [--strict] [--pretty]`
487
595
  - `omw replay <journal.jsonl> [--json]`
488
596
  - `omw validate <wf> [--json]` — pre-flight: load + fake-fixture lint, no agents spawned.
489
- - exit codes: `0` ok · `1` script/load error · `2` usage · `3` adapter missing · `4` completed but a node hit `internal_error` (author bug; result still on stdout).
597
+ - `omw codemod <file> [--to-di] [--write]` migrate a legacy `(rt, args)` workflow to destructured DI.
598
+ - `omw skill install [--codex|--opencode] [--project]` — install this skill for a coding agent.
599
+ - exit codes: `0` ok · `1` script/load error (incl. budget ceiling) · `2` usage · `3` adapter missing · `4` completed but a node hit `internal_error` (author bug; result still on stdout).
490
600
  - stdout = result JSON · journal = `.omw/<runId>.jsonl` · `--pretty` tree = stderr.
491
- - `agent()` never throws → `filter(Boolean)`; quorum of cast votes for verify-vote.
601
+ - `agent()` never throws (except `BudgetExceededError`) → `filter(Boolean)`; quorum of cast votes for verify-vote.