@workflow-manager/runner 0.8.0 → 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.
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ Task steps run with the `pi-agent` adapter by default when `taskSpec.adapterKey`
|
|
|
74
74
|
|
|
75
75
|
Before execution starts, `wfm run` validates host runtime access for real adapters. Default `pi-agent` steps require the configured `pi` command to be installed and executable; pi manages provider credentials in its own auth store, so no API key environment variables are inferred for pi steps. Real `opencode` steps require the `opencode` CLI, and real `claude-code` steps require the `claude` CLI. For those adapters, if `taskSpec.init.model` or `taskSpec.payload.model` identifies a known provider, the matching key must also be present: `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY`. For custom clients, set `taskSpec.payload.requiredEnv` to the environment variable names that must exist before the run can start.
|
|
76
76
|
|
|
77
|
-
Use `wfm doctor` to inspect host adapter setup and `wfm doctor <workflow>` to validate a workflow's runtime requirements before running it. Today, `pi-agent` is the real default host adapter (driving the `pi` CLI), `mock` is a deterministic simulator, `opencode
|
|
77
|
+
Use `wfm doctor` to inspect host adapter setup and `wfm doctor <workflow>` to validate a workflow's runtime requirements before running it. Today, `pi-agent` is the real default host adapter (driving the `pi` CLI), `mock` is a deterministic simulator, and `opencode`, `claude-code`, and `codex` have opt-in real paths through ACP (`useRealAdapter: true`). Codex routes through the `codex-acp` bridge — install it with `npm install -g @agentclientprotocol/codex-acp`; it reuses the codex CLI's own auth, so no API key environment variable is needed.
|
|
78
78
|
|
|
79
79
|
During `wfm run`, the CLI starts a local attach API on `127.0.0.1`. Use `--port <n>` to bind a fixed port or omit it to let the OS choose one. The CLI prints the attach base URL and bearer token before execution starts.
|
|
80
80
|
|
package/dist/acpExecutor.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export declare function normalizeTimeout(value: unknown, fallbackMs?: number): n
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolves which ACP agent command to launch for a step. Precedence:
|
|
9
9
|
* payload.acpCommand → WFM_ACP_COMMAND env → preset for payload.acpAgent → preset
|
|
10
|
-
* for the adapter key (claude-code / opencode). Returns null when nothing
|
|
11
|
-
* (e.g. bare `acp`
|
|
10
|
+
* for the adapter key (claude-code / opencode / codex). Returns null when nothing
|
|
11
|
+
* resolves (e.g. bare `acp` without configuration), which keeps the step on mock.
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveAcpCommand(step: StepDefinition, env: NodeJS.ProcessEnv): ResolvedAcpCommand | null;
|
|
14
14
|
export declare function shouldUseRealAcp(step: StepDefinition): boolean;
|
package/dist/acpExecutor.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream, } from "@
|
|
4
|
+
import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
5
5
|
import { resolveTaskAdapter } from "./adapters.js";
|
|
6
6
|
import { resolveSkill } from "./skillResolver.js";
|
|
7
|
-
// Presets so claude-code / opencode / a named agent route through ACP without
|
|
8
|
-
// explicit command. Verified invocations (gemini --experimental-acp and opencode acp
|
|
7
|
+
// Presets so claude-code / opencode / codex / a named agent route through ACP without
|
|
8
|
+
// an explicit command. Verified invocations (gemini --experimental-acp and opencode acp
|
|
9
9
|
// confirmed against gemini 0.31 / opencode 1.2; claude-code-acp is the @zed-industries
|
|
10
|
-
// bridge — `claude` itself has no native ACP
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// bridge — `claude` itself has no native ACP; codex-acp is the @agentclientprotocol
|
|
11
|
+
// bridge — `codex` itself has no native ACP, handshake confirmed against codex-acp 1.1).
|
|
12
|
+
// All overridable via payload.acpCommand / acpArgs or WFM_ACP_COMMAND.
|
|
13
13
|
const ACP_COMMAND_PRESETS = {
|
|
14
14
|
"claude-code": { command: "claude-code-acp", args: [] },
|
|
15
15
|
opencode: { command: "opencode", args: ["acp"] },
|
|
16
16
|
gemini: { command: "gemini", args: ["--experimental-acp"] },
|
|
17
|
+
codex: { command: "codex-acp", args: [] },
|
|
17
18
|
};
|
|
18
19
|
const READ_ONLY_TOOL_KINDS = new Set(["read", "search", "fetch", "think"]);
|
|
19
20
|
function asRecord(value) {
|
|
@@ -69,8 +70,8 @@ function stringArg(value) {
|
|
|
69
70
|
/**
|
|
70
71
|
* Resolves which ACP agent command to launch for a step. Precedence:
|
|
71
72
|
* payload.acpCommand → WFM_ACP_COMMAND env → preset for payload.acpAgent → preset
|
|
72
|
-
* for the adapter key (claude-code / opencode). Returns null when nothing
|
|
73
|
-
* (e.g. bare `acp`
|
|
73
|
+
* for the adapter key (claude-code / opencode / codex). Returns null when nothing
|
|
74
|
+
* resolves (e.g. bare `acp` without configuration), which keeps the step on mock.
|
|
74
75
|
*/
|
|
75
76
|
export function resolveAcpCommand(step, env) {
|
|
76
77
|
const payload = asRecord(step.taskSpec?.payload);
|
|
@@ -13,7 +13,7 @@ export const BUNDLED_SKILLS = {
|
|
|
13
13
|
],
|
|
14
14
|
"workflow-author": [
|
|
15
15
|
{ name: "README.md", content: "# workflow-author skill\n\nTeaches a host coding agent (Claude Code, opencode, pi) how to turn a natural-language\ntask description into a repeatable `wfm` workflow, validate it, run it, and narrate\nprogress — including approval gates — back to the user.\n\nThis skill ships inside the root `@workflow-manager/runner` npm package alongside\n`workflow-manager-cli`. Install it into an agent's skill directory with:\n\n```bash\nwfm skill install workflow-author # -> ./.claude/skills/\nwfm skill install workflow-author --agent opencode # -> ./.opencode/skill/\nwfm skill install workflow-author --global # -> ~/.claude/skills/\n```\n\nSee `SKILL.md` for the full operating manual, and `doc/guide/workflow-schema.md` /\n`doc/guide/runner-api.md` for the underlying schema and attach-API contract this\nskill is built on.\n" },
|
|
16
|
-
{ name: "SKILL.md", content: "---\nname: workflow-author\ndescription: >\n Author repeatable wfm workflows from a task description, validate them, run\n them, and narrate progress as the workflow executes. Load this skill when a\n user describes a multi-step task that should run the same way every time —\n turn it into a wfm workflow file, validate it, and then drive `wfm run`\n while translating status polls and approval gates into plain-language\n updates for the user.\ntype: core\nlibrary: \"@workflow-manager/runner\"\nsources:\n - \"navio/workflow-manager:src/parser.ts\"\n - \"navio/workflow-manager:src/types.ts\"\n - \"navio/workflow-manager:src/engine.ts\"\n - \"navio/workflow-manager:src/index.ts\"\n - \"navio/workflow-manager:src/sessionFile.ts\"\n - \"navio/workflow-manager:doc/guide/workflow-schema.md\"\n - \"navio/workflow-manager:doc/guide/runner-api.md\"\n---\n\n# workflow-author\n\nTurn a natural-language task description into a `wfm` workflow file: a static, reviewable, re-runnable artifact instead of a one-off chat session. This skill is the authoring + operating counterpart to `workflow-manager-cli` — read that skill for the general command reference; this one is about *deciding what to build* and *narrating a run to a human*.\n\n## 1. When to use\n\nUse this skill when the user has a task that:\n\n- has more than one meaningful step, with a clear order or dependency between steps\n- should produce the **same shape of result** every time it runs — not a bespoke plan improvised fresh each session\n- benefits from an explicit quality gate (an objective check, a human sign-off, or both) before moving on\n- is worth sharing: a teammate, a CI job, or a future session should be able to run it unchanged\n\nDo **not** reach for a workflow file for a single ad-hoc question, a one-shot investigation, or a task whose steps genuinely can't be known in advance. Ad-hoc orchestration (just doing the work directly) is cheaper than authoring a workflow when there is nothing to repeat. The signal to watch for: the user says some version of \"every time we do X\" or \"we need a repeatable way to Y.\"\n\n## 2. The authoring loop\n\nFollow this sequence; do not skip validation.\n\n1. **Elicit the task and success criteria.** Ask (or infer from context) what \"done\" looks like for the overall task, and for each step you expect to need. Concrete, checkable criteria beat vague ones — \"the change satisfies the objective and includes tests\" is checkable; \"does a good job\" is not.\n2. **Decompose into chronological steps.** Every step gets a stable, kebab-case `key` (`implement-fix`, not `Step 1`). Wire `dependsOn` to express strict order — the engine resolves dependencies deterministically and rejects cycles, so an explicit `dependsOn: [previous-step]` is safer than relying on array order.\n3. **Choose a quality gate per step.** For each step's `validation` (or an `approval` step's `approvalSpec.validation`), pick one:\n - `mode: agent` with concrete `criteria` — an objective, checkable condition a second agent call can verify against the step's output (tests pass, files changed match scope, no TODOs left). Prefer this whenever the check can be phrased as a fact about the artifact.\n - a dedicated `kind: approval` step — a human judgment call (does this look right, is this the right tradeoff, are we comfortable shipping this). Use for genuinely subjective or high-stakes decisions.\n - `mode: external` — an outside system resolves the step (a webhook, a deploy pipeline finishing).\n - `mode: none` — mechanical steps with no interesting failure mode (formatting, a fixed notification).\n4. **Write the workflow.** Prefer the Markdown frontmatter format — humans (and you, later) can read the body notes alongside the machine-readable frontmatter; use JSON only for machine-generated definitions. Start from a scaffold:\n ```bash\n wfm scaffold --template agent-validated my-flow.md\n ```\n This drops a working three-step example (agent-validated task → approval → finalize) that you edit in place rather than writing from a blank file.\n5. **Validate, and keep validating.** `wfm validate my-flow.md` — fix every reported line, one at a time, until it prints `Validation OK`. Never hand a workflow to the user (or run it) with unresolved validation errors.\n6. **Dry-run adapter-heavy workflows with mocks.** If steps use real adapters (`pi-agent`, `claude-code`, `opencode`, `acp`), set `taskSpec.payload.mockResult: success` (or `retry` / `rollback` / `fail` to test routing) and `taskSpec.adapterKey: mock` temporarily, or `wfm doctor my-flow.md` to check host requirements without executing. This confirms dependency wiring and approval gating before spending a real adapter call.\n7. **Done.** The file *is* the reusable artifact — no further \"session state\" to preserve. Re-running it later reproduces the same step sequence.\n\n## 3. Schema cheat-sheet\n\nEverything here matches `src/types.ts` on this branch — do not invent fields.\n\n**Top level** (required: `key`, `title`, `steps`):\n\n```yaml\nkey: my-workflow # stable external identifier\ntitle: My Workflow # default run objective\ndescription: optional summary\nobjectives: [optional, run-level, objectives]\ndefaultRetryPolicy: { maxAttempts: 2 }\nskills: # named skills resolvable by taskSpec.init.skills\n my-skill:\n source: ./skills/my-skill/SKILL.md # must match skills/**/SKILL.md under the workflow dir\nsteps: [ ... ]\n```\n\n**Step** (required: `key`, `kind`):\n\n```yaml\n- key: my-step # stable, kebab-case\n kind: task # task | approval | system\n title: optional display title\n objective: optional step-level objective\n dependsOn: [other-step-key]\n timeoutSec: optional\n retryPolicy: { maxAttempts: 2 }\n validation: # gates confirmation for THIS step's own record\n mode: none | human | external | agent\n required: true\n autoConfirm: false\n agent: # only when mode: agent\n adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # default: this step's adapter\n criteria: \"plain-language acceptance criteria the validator checks against\"\n init: { model, skills, mcps, systemPrompts, context }\n payload: { mockResult: success } # lets mock drive the validator in tests\n taskSpec: # required when kind: task\n adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # omit -> pi-agent\n init:\n model: openrouter/anthropic/claude-sonnet-4\n skills: [skill-name]\n mcps: [mcp://endpoint]\n systemPrompts: [Focus on X]\n context: { any: json }\n payload:\n mockResult: success | retry | rollback | restart | yield | fail # mock adapter only\n approvalSpec: # required when kind: approval\n autoApprove: false\n validation: { mode: human, required: true, autoConfirm: false }\n```\n\n**Critical gotcha** (verified on this branch, `src/parser.ts`): the parser fills an *unset* step-level `validation` with `{ mode: \"none\", required: false, autoConfirm: true }` by default — **even on `approval` steps**. `canConfirm` in `src/engine.ts` checks `step.validation?.autoConfirm` *before* `step.approvalSpec?.validation?.autoConfirm`. If you only set `approvalSpec.validation` and leave the step's own top-level `validation` unset, the default `autoConfirm: true` wins and the gate **silently auto-approves** instead of waiting for a human. Always set both `validation` and `approvalSpec.validation` on an approval step with matching `mode`/`required`/`autoConfirm` — see the worked example below.\n\n**Validation rules the CLI enforces:** unique step keys; every `dependsOn` references an existing step; no dependency cycles; `kind` is `task | approval | system`; `taskSpec.adapterKey` (if set) is one of the supported adapters; `validation.mode` is `none | human | external | agent`; `mode: agent` is **not allowed** on approval steps (`approvalSpec.validation`).\n\n**Agent validation routing:** a validator agent's verdict becomes a QA action — `PROCEED` (continue), `RETRY_CURRENT` (rerun this step with feedback), `ROLLBACK_PREVIOUS` (rerun an earlier step), or `RESTART_ALL` (restart the run) — bounded by the step's `retryPolicy.maxAttempts`.\n\n## 4. Running and narrating (the agent-as-UI protocol)\n\nOnce a workflow validates, you are the UI for the run: start it detached, poll its state, and turn each transition into a short update instead of dumping raw JSON at the user.\n\n**Start it detached, with a session file:**\n\n```bash\nwfm run my-flow.md --session-file .wfm/session.json &\n```\n\nThe session file is written the moment the attach API is listening — `{ baseUrl, attachToken, runId, pid, startedAt }` — and rewritten with `endedAt` + `status` when the run finishes. It is never deleted, so it doubles as your \"is this still running\" signal. Every attach command below accepts `--session-file .wfm/session.json` instead of separate `--url`/`--token`.\n\n**Poll on a cadence and narrate transitions**, not raw payloads:\n\n```bash\nwfm status --session-file .wfm/session.json\n```\n\nRead `status` and `currentStepKey` off the JSON, and translate: `\"running\"` + `currentStepKey: \"implement-fix\"` becomes something like *\"step 1/3 (implement-fix) is running, attempt 1...\"*. Don't re-poll faster than the work can plausibly progress — a few seconds between polls is usually plenty; back off further once a step has been running a while.\n\n**Use `events` for incremental detail** between polls instead of re-reading the whole snapshot:\n\n```bash\nwfm events --session-file .wfm/session.json --since 4\n```\n\nTrack the last `nextSequence` you saw and pass it back as `--since` next time. Add `--include-logs` only when you actually want `agent.stdout`/`agent.stderr` chunks inline.\n\n**Use `logs` when the user asks what a step is doing right now:**\n\n```bash\nwfm logs --session-file .wfm/session.json --step implement-fix --limit 50\n```\n\n**Detect and handle `waitingForApproval`.** When `status`'s top-level `status` is `\"waiting_for_approval\"`, the JSON includes a `waitingForApproval` object with `stepKey`, `reason`, and a `preview` (`summary` plus `items` describing what's being reviewed, including dependency outputs). Summarize that preview for the user in plain language. Then either:\n\n- relay the decision the user gives you, or\n- if the user has explicitly delegated authority for this gate (\"auto-approve the review steps,\" \"you decide\"), decide yourself and act:\n\n```bash\nwfm approve --session-file .wfm/session.json --step review-gate --note \"why you approved\"\nwfm cancel --session-file .wfm/session.json --step review-gate --note \"why you're stopping the run\"\n```\n\nNever approve on the user's behalf without either their live input or a standing delegation they gave you for that specific gate — it is a QA checkpoint, not decoration.\n\n**On terminal status, report the outcome** by reading `endedAt` and `status` back from the session file (the run process has exited by then, so the attach API is gone — the session file is the only source left):\n\n```bash\ncat .wfm/session.json # { ..., \"endedAt\": \"...\", \"status\": \"succeeded\" }\n```\n\n**Exit codes** (for the `wfm run` process itself, if you're waiting on it directly rather than polling): `0` — run succeeded; `2` — run finished but not successfully (failed, cancelled, or ended waiting); `1` — validation or runtime error before/during execution, not a normal terminal status.\n\n## 5. Repeatability rules\n\n- Never rename or remove a published workflow's step keys — other automation and history may reference them. Add new steps or a new workflow `key`/version instead of mutating shape in place.\n- `key` (workflow) and step `key`s are stable external identifiers; change them only deliberately, and treat it as a breaking change for anything that depends on them.\n- Keep `taskSpec.payload` deterministic — avoid embedding timestamps, random IDs, or environment-specific paths that would make two runs diverge for reasons unrelated to the actual task.\n- Prefer `validation.mode: agent` criteria that a validator can check as a fact about the output (tests pass, a file exists, a diff touches only expected paths) over criteria that require taste. Save taste calls for `approval` steps.\n\n## 6. Install/share\n\n```bash\nwfm skill install workflow-author # this skill -> ./.claude/skills/\nwfm skill install workflow-author --agent opencode # -> ./.opencode/skill/\nwfm skill install workflow-author --global # -> ~/.claude/skills/\n```\n\nTo share the workflow *file* itself (not this skill) with teammates, use the remote registry — `wfm publish my-flow.md` / `wfm pull owner/slug`; see the `workflow-manager-cli` skill and `doc/guide/` for the full registry contract.\n\n## Worked example\n\nA repeatable \"fix a flaky test\" workflow: an agent-validated implementation step, a human sign-off gate, then a finalize step. Validated on this branch with `wfm validate` (`Validation OK`) and executed end-to-end with the mock adapter.\n\n```yaml\n---\nkey: fix-flaky-login-test\ntitle: Fix Flaky Login Test\ndescription: Diagnose and fix an intermittently failing login test, with a human sign-off before landing the fix\nobjectives:\n - the login test passes reliably and the fix is reviewed before merge\ndefaultRetryPolicy:\n maxAttempts: 2\nsteps:\n - key: implement-fix\n kind: task\n objective: Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it\n dependsOn: []\n retryPolicy:\n maxAttempts: 2\n validation:\n mode: agent\n required: true\n autoConfirm: false\n agent:\n criteria: >-\n tests/login.test.ts passes 20 consecutive local runs, the fix\n addresses a root cause (not a retry/sleep workaround), and no\n unrelated files changed.\n init:\n model: openrouter/anthropic/claude-sonnet-4\n systemPrompts:\n - Check the diff against the criteria; call out any retry/sleep workaround explicitly\n taskSpec:\n adapterKey: mock\n init:\n context:\n repo: example/webapp\n skills: [debugging, testing]\n systemPrompts: [Find the root cause before writing a fix; add a regression test]\n payload:\n mockResult: success\n - key: review-gate\n kind: approval\n objective: Human sign-off on the fix before it merges\n dependsOn: [implement-fix]\n # validation must be set here too, not just under approvalSpec — an unset\n # step.validation defaults to autoConfirm: true, which would silently skip\n # this gate. See the schema cheat-sheet above.\n validation:\n mode: human\n required: true\n autoConfirm: false\n approvalSpec:\n autoApprove: false\n validation:\n mode: human\n required: true\n autoConfirm: false\n - key: finalize\n kind: task\n objective: Open a PR with the fix, the regression test, and a summary of the root cause\n dependsOn: [review-gate]\n validation:\n mode: none\n required: false\n autoConfirm: true\n taskSpec:\n adapterKey: mock\n init:\n systemPrompts: [Open a PR summarizing the root cause, the fix, and the new regression test]\n payload:\n mockResult: success\n---\n\n# Fix Flaky Login Test\n\nRepeatable workflow for chasing down a flaky test: implement a fix (agent-validated\nagainst explicit, checkable criteria), get a human sign-off, then finalize.\n```\n\n### Simulated narration transcript\n\n```\n$ wfm run fix-flaky-login-test.md --session-file .wfm/session.json &\nAttach API: http://127.0.0.1:43121 (token b354...)\n\n[agent] Started the workflow in the background — I'll check in as it progresses.\n\n$ wfm status --session-file .wfm/session.json\n{\"status\":\"running\",\"currentStepKey\":\"implement-fix\", ...}\n\n[agent] step 1/3 (implement-fix) is running, attempt 1 — reproducing the flake and writing a fix.\n\n$ wfm status --session-file .wfm/session.json\n{\"status\":\"waiting_for_approval\",\"currentStepKey\":\"review-gate\",\n \"waitingForApproval\":{\"stepKey\":\"review-gate\",\n \"reason\":\"confirmation required for review-gate\",\n \"preview\":{\"summary\":\"Approve this gate to continue after implement-fix.\",\n \"items\":[{\"title\":\"Dependency implement-fix\",\"status\":\"succeeded\",\n \"summary\":\"Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it\"}]}}}\n\n[agent] implement-fix passed agent validation (tests green, real root-cause fix,\nno unrelated files touched) and is now waiting on you: review-gate wants a\nhuman sign-off before finalize runs. Want me to look at the diff summary, or\nare you approving as-is?\n\n[user] approve it\n\n$ wfm approve --session-file .wfm/session.json --step review-gate --note \"Fix looks correct, root cause addressed\"\napproved review-gate\n\n[agent] Approved review-gate. finalize is running now (opening the PR)...\n\n$ cat .wfm/session.json\n{ ..., \"endedAt\": \"2026-07-11T13:35:40.756Z\", \"status\": \"succeeded\" }\n\n[agent] Done — the workflow succeeded. implement-fix and finalize both ran\nclean, and your sign-off on review-gate is recorded in the run's approval\naudit trail.\n```\n" },
|
|
16
|
+
{ name: "SKILL.md", content: "---\nname: workflow-author\ndescription: >\n Author repeatable wfm workflows from a task description, validate them, run\n them, and narrate progress as the workflow executes. Load this skill when a\n user describes a multi-step task that should run the same way every time —\n turn it into a wfm workflow file, validate it, and then drive `wfm run`\n while translating status polls and approval gates into plain-language\n updates for the user.\ntype: core\nlibrary: \"@workflow-manager/runner\"\nsources:\n - \"navio/workflow-manager:src/parser.ts\"\n - \"navio/workflow-manager:src/types.ts\"\n - \"navio/workflow-manager:src/engine.ts\"\n - \"navio/workflow-manager:src/index.ts\"\n - \"navio/workflow-manager:src/sessionFile.ts\"\n - \"navio/workflow-manager:doc/guide/workflow-schema.md\"\n - \"navio/workflow-manager:doc/guide/runner-api.md\"\n---\n\n# workflow-author\n\nTurn a natural-language task description into a `wfm` workflow file: a static, reviewable, re-runnable artifact instead of a one-off chat session. This skill is the authoring + operating counterpart to `workflow-manager-cli` — read that skill for the general command reference; this one is about *deciding what to build* and *narrating a run to a human*.\n\n## 1. When to use\n\nUse this skill when the user has a task that:\n\n- has more than one meaningful step, with a clear order or dependency between steps\n- should produce the **same shape of result** every time it runs — not a bespoke plan improvised fresh each session\n- benefits from an explicit quality gate (an objective check, a human sign-off, or both) before moving on\n- is worth sharing: a teammate, a CI job, or a future session should be able to run it unchanged\n\nDo **not** reach for a workflow file for a single ad-hoc question, a one-shot investigation, or a task whose steps genuinely can't be known in advance. Ad-hoc orchestration (just doing the work directly) is cheaper than authoring a workflow when there is nothing to repeat. The signal to watch for: the user says some version of \"every time we do X\" or \"we need a repeatable way to Y.\"\n\n## 2. The authoring loop\n\nFollow this sequence; do not skip validation.\n\n1. **Elicit the task and success criteria.** Ask (or infer from context) what \"done\" looks like for the overall task, and for each step you expect to need. Concrete, checkable criteria beat vague ones — \"the change satisfies the objective and includes tests\" is checkable; \"does a good job\" is not.\n2. **Decompose into chronological steps.** Every step gets a stable, kebab-case `key` (`implement-fix`, not `Step 1`). Wire `dependsOn` to express strict order — the engine resolves dependencies deterministically and rejects cycles, so an explicit `dependsOn: [previous-step]` is safer than relying on array order.\n3. **Choose a quality gate per step.** For each step's `validation` (or an `approval` step's `approvalSpec.validation`), pick one:\n - `mode: agent` with concrete `criteria` — an objective, checkable condition a second agent call can verify against the step's output (tests pass, files changed match scope, no TODOs left). Prefer this whenever the check can be phrased as a fact about the artifact.\n - a dedicated `kind: approval` step — a human judgment call (does this look right, is this the right tradeoff, are we comfortable shipping this). Use for genuinely subjective or high-stakes decisions.\n - `mode: external` — an outside system resolves the step (a webhook, a deploy pipeline finishing).\n - `mode: none` — mechanical steps with no interesting failure mode (formatting, a fixed notification).\n4. **Write the workflow.** Prefer the Markdown frontmatter format — humans (and you, later) can read the body notes alongside the machine-readable frontmatter; use JSON only for machine-generated definitions. Start from a scaffold:\n ```bash\n wfm scaffold --template agent-validated my-flow.md\n ```\n This drops a working three-step example (agent-validated task → approval → finalize) that you edit in place rather than writing from a blank file.\n5. **Validate, and keep validating.** `wfm validate my-flow.md` — fix every reported line, one at a time, until it prints `Validation OK`. Never hand a workflow to the user (or run it) with unresolved validation errors.\n6. **Dry-run adapter-heavy workflows with mocks.** If steps use real adapters (`pi-agent`, `claude-code`, `opencode`, `acp`), set `taskSpec.payload.mockResult: success` (or `retry` / `rollback` / `fail` to test routing) and `taskSpec.adapterKey: mock` temporarily, or `wfm doctor my-flow.md` to check host requirements without executing. This confirms dependency wiring and approval gating before spending a real adapter call.\n7. **Done.** The file *is* the reusable artifact — no further \"session state\" to preserve. Re-running it later reproduces the same step sequence.\n\n## 3. Schema cheat-sheet\n\nEverything here matches `src/types.ts` on this branch — do not invent fields.\n\n**Top level** (required: `key`, `title`, `steps`):\n\n```yaml\nkey: my-workflow # stable external identifier\ntitle: My Workflow # default run objective\ndescription: optional summary\nobjectives: [optional, run-level, objectives]\ndefaultRetryPolicy: { maxAttempts: 2 }\nskills: # named skills resolvable by taskSpec.init.skills\n my-skill:\n source: ./skills/my-skill/SKILL.md # must match skills/**/SKILL.md under the workflow dir\nsteps: [ ... ]\n```\n\n**Step** (required: `key`, `kind`):\n\n```yaml\n- key: my-step # stable, kebab-case\n kind: task # task | approval | system\n title: optional display title\n objective: optional step-level objective\n dependsOn: [other-step-key]\n timeoutSec: optional\n retryPolicy: { maxAttempts: 2 }\n validation: # gates confirmation for THIS step's own record\n mode: none | human | external | agent\n required: true\n autoConfirm: false\n agent: # only when mode: agent\n adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # default: this step's adapter\n criteria: \"plain-language acceptance criteria the validator checks against\"\n init: { model, skills, mcps, systemPrompts, context }\n payload: { mockResult: success } # lets mock drive the validator in tests\n taskSpec: # required when kind: task\n adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # omit -> pi-agent\n init:\n model: openrouter/anthropic/claude-sonnet-4\n skills: [skill-name]\n mcps: [mcp://endpoint]\n systemPrompts: [Focus on X]\n context: { any: json }\n payload:\n mockResult: success | retry | rollback | restart | yield | fail # mock adapter only\n approvalSpec: # required when kind: approval\n autoApprove: false\n validation: { mode: human, required: true, autoConfirm: false }\n```\n\n**Critical gotcha** (verified on this branch, `src/parser.ts`): the parser fills an *unset* step-level `validation` with `{ mode: \"none\", required: false, autoConfirm: true }` by default — **even on `approval` steps**. `canConfirm` in `src/engine.ts` checks `step.validation?.autoConfirm` *before* `step.approvalSpec?.validation?.autoConfirm`. If you only set `approvalSpec.validation` and leave the step's own top-level `validation` unset, the default `autoConfirm: true` wins and the gate **silently auto-approves** instead of waiting for a human. Always set both `validation` and `approvalSpec.validation` on an approval step with matching `mode`/`required`/`autoConfirm` — see the worked example below.\n\n**Validation rules the CLI enforces:** unique step keys; every `dependsOn` references an existing step; no dependency cycles; `kind` is `task | approval | system`; `taskSpec.adapterKey` (if set) is one of the supported adapters; `validation.mode` is `none | human | external | agent`; `mode: agent` is **not allowed** on approval steps (`approvalSpec.validation`).\n\n**Agent validation routing:** a validator agent's verdict becomes a QA action — `PROCEED` (continue), `RETRY_CURRENT` (rerun this step with feedback), `ROLLBACK_PREVIOUS` (rerun an earlier step), or `RESTART_ALL` (restart the run) — bounded by the step's `retryPolicy.maxAttempts`.\n\n## 4. Running and narrating (the agent-as-UI protocol)\n\nOnce a workflow validates, you are the UI for the run: start it detached, poll its state, and turn each transition into a short update instead of dumping raw JSON at the user.\n\n**Start it detached, with a session file:**\n\n```bash\nwfm run my-flow.md --session-file .wfm/session.json &\n```\n\nThe session file is written the moment the attach API is listening — `{ baseUrl, attachToken, runId, pid, startedAt }` — and rewritten with `endedAt` + `status` when the run finishes. It is never deleted, so it doubles as your \"is this still running\" signal. Every attach command below accepts `--session-file .wfm/session.json` instead of separate `--url`/`--token`.\n\n**Poll on a cadence and narrate transitions**, not raw payloads:\n\n```bash\nwfm status --session-file .wfm/session.json\n```\n\nRead `status` and `currentStepKey` off the JSON, and translate: `\"running\"` + `currentStepKey: \"implement-fix\"` becomes something like *\"step 1/3 (implement-fix) is running, attempt 1...\"*. Don't re-poll faster than the work can plausibly progress — a few seconds between polls is usually plenty; back off further once a step has been running a while.\n\nIf you're dry-running with `adapterKey: mock`, expect steps to resolve near-instantly: your very first `status` poll may already show several steps succeeded (or the run parked at an approval gate) with no intermediate \"running\" beat to narrate. That's expected — narrate what you actually observe rather than assuming a step-by-step cadence.\n\n**Use `events` for incremental detail** between polls instead of re-reading the whole snapshot:\n\n```bash\nwfm events --session-file .wfm/session.json --since 4\n```\n\nTrack the last `nextSequence` you saw and pass it back as `--since` next time. Add `--include-logs` only when you actually want `agent.stdout`/`agent.stderr` chunks inline.\n\n**Use `logs` when the user asks what a step is doing right now:**\n\n```bash\nwfm logs --session-file .wfm/session.json --step implement-fix --limit 50\n```\n\n**Detect and handle `waitingForApproval`.** When `status`'s top-level `status` is `\"waiting_for_approval\"`, the JSON includes a `waitingForApproval` object with `stepKey`, `reason`, and a `preview` (`summary` plus `items` describing what's being reviewed, including dependency outputs). Summarize that preview for the user in plain language. Then either:\n\n- relay the decision the user gives you, or\n- if the user has explicitly delegated authority for this gate (\"auto-approve the review steps,\" \"you decide\"), decide yourself and act:\n\n```bash\nwfm approve --session-file .wfm/session.json --step review-gate --note \"why you approved\"\nwfm cancel --session-file .wfm/session.json --step review-gate --note \"why you're stopping the run\"\n```\n\nNever approve on the user's behalf without either their live input or a standing delegation they gave you for that specific gate — it is a QA checkpoint, not decoration.\n\n**On terminal status, report the outcome** by reading `endedAt` and `status` back from the session file (the run process has exited by then, so the attach API is gone — the session file is the only source left):\n\n```bash\ncat .wfm/session.json # { ..., \"endedAt\": \"...\", \"status\": \"succeeded\" }\n```\n\n**Exit codes** (for the `wfm run` process itself, if you're waiting on it directly rather than polling): `0` — run succeeded; `2` — run finished but not successfully (failed, cancelled, or ended waiting); `1` — validation or runtime error before/during execution, not a normal terminal status.\n\n## 5. Repeatability rules\n\n- Never rename or remove a published workflow's step keys — other automation and history may reference them. Add new steps or a new workflow `key`/version instead of mutating shape in place.\n- `key` (workflow) and step `key`s are stable external identifiers; change them only deliberately, and treat it as a breaking change for anything that depends on them.\n- Keep `taskSpec.payload` deterministic — avoid embedding timestamps, random IDs, or environment-specific paths that would make two runs diverge for reasons unrelated to the actual task.\n- Prefer `validation.mode: agent` criteria that a validator can check as a fact about the output (tests pass, a file exists, a diff touches only expected paths) over criteria that require taste. Save taste calls for `approval` steps.\n\n## 6. Install/share\n\n```bash\nwfm skill install workflow-author # this skill -> ./.claude/skills/\nwfm skill install workflow-author --agent opencode # -> ./.opencode/skill/\nwfm skill install workflow-author --global # -> ~/.claude/skills/\n```\n\nTo share the workflow *file* itself (not this skill) with teammates, use the remote registry — `wfm publish my-flow.md` / `wfm pull owner/slug`; see the `workflow-manager-cli` skill and `doc/guide/` for the full registry contract.\n\n## Worked example\n\nA repeatable \"fix a flaky test\" workflow: an agent-validated implementation step, a human sign-off gate, then a finalize step. Validated on this branch with `wfm validate` (`Validation OK`) and executed end-to-end with the mock adapter.\n\n```yaml\n---\nkey: fix-flaky-login-test\ntitle: Fix Flaky Login Test\ndescription: Diagnose and fix an intermittently failing login test, with a human sign-off before landing the fix\nobjectives:\n - the login test passes reliably and the fix is reviewed before merge\ndefaultRetryPolicy:\n maxAttempts: 2\nsteps:\n - key: implement-fix\n kind: task\n objective: Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it\n dependsOn: []\n retryPolicy:\n maxAttempts: 2\n validation:\n mode: agent\n required: true\n autoConfirm: false\n agent:\n criteria: >-\n tests/login.test.ts passes 20 consecutive local runs, the fix\n addresses a root cause (not a retry/sleep workaround), and no\n unrelated files changed.\n init:\n model: openrouter/anthropic/claude-sonnet-4\n systemPrompts:\n - Check the diff against the criteria; call out any retry/sleep workaround explicitly\n taskSpec:\n adapterKey: mock\n init:\n context:\n repo: example/webapp\n skills: [debugging, testing]\n systemPrompts: [Find the root cause before writing a fix; add a regression test]\n payload:\n mockResult: success\n - key: review-gate\n kind: approval\n objective: Human sign-off on the fix before it merges\n dependsOn: [implement-fix]\n # validation must be set here too, not just under approvalSpec — an unset\n # step.validation defaults to autoConfirm: true, which would silently skip\n # this gate. See the schema cheat-sheet above.\n validation:\n mode: human\n required: true\n autoConfirm: false\n approvalSpec:\n autoApprove: false\n validation:\n mode: human\n required: true\n autoConfirm: false\n - key: finalize\n kind: task\n objective: Open a PR with the fix, the regression test, and a summary of the root cause\n dependsOn: [review-gate]\n validation:\n mode: none\n required: false\n autoConfirm: true\n taskSpec:\n adapterKey: mock\n init:\n systemPrompts: [Open a PR summarizing the root cause, the fix, and the new regression test]\n payload:\n mockResult: success\n---\n\n# Fix Flaky Login Test\n\nRepeatable workflow for chasing down a flaky test: implement a fix (agent-validated\nagainst explicit, checkable criteria), get a human sign-off, then finalize.\n```\n\n### Simulated narration transcript\n\n```\n$ wfm run fix-flaky-login-test.md --session-file .wfm/session.json &\nAttach API: http://127.0.0.1:43121 (token b354...)\n\n[agent] Started the workflow in the background — I'll check in as it progresses.\n\n$ wfm status --session-file .wfm/session.json\n{\"status\":\"running\",\"currentStepKey\":\"implement-fix\", ...}\n\n[agent] step 1/3 (implement-fix) is running, attempt 1 — reproducing the flake and writing a fix.\n\n$ wfm status --session-file .wfm/session.json\n{\"status\":\"waiting_for_approval\",\"currentStepKey\":\"review-gate\",\n \"waitingForApproval\":{\"stepKey\":\"review-gate\",\n \"reason\":\"confirmation required for review-gate\",\n \"preview\":{\"summary\":\"Approve this gate to continue after implement-fix.\",\n \"items\":[{\"title\":\"Dependency implement-fix\",\"status\":\"succeeded\",\n \"summary\":\"Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it\"}]}}}\n\n[agent] implement-fix passed agent validation (tests green, real root-cause fix,\nno unrelated files touched) and is now waiting on you: review-gate wants a\nhuman sign-off before finalize runs. Want me to look at the diff summary, or\nare you approving as-is?\n\n[user] approve it\n\n$ wfm approve --session-file .wfm/session.json --step review-gate --note \"Fix looks correct, root cause addressed\"\napproved review-gate\n\n[agent] Approved review-gate. finalize is running now (opening the PR)...\n\n$ cat .wfm/session.json\n{ ..., \"endedAt\": \"2026-07-11T13:35:40.756Z\", \"status\": \"succeeded\" }\n\n[agent] Done — the workflow succeeded. implement-fix and finalize both ran\nclean, and your sign-off on review-gate is recorded in the run's approval\naudit trail.\n```\n" },
|
|
17
17
|
],
|
|
18
18
|
"workflow-manager-cli": [
|
|
19
19
|
{ name: "README.md", content: "# workflow-manager-cli skill\n\nThis skill ships inside the root `@workflow-manager/runner` npm package.\n\nIt is designed for TanStack Intent discovery and should stay aligned with the CLI behavior documented in:\n\n- `README.md`\n- `src/index.ts`\n- `src/parser.ts`\n- `src/engine.ts`\n- `src/remote/commands.ts`\n" },
|
package/dist/runtimePreflight.js
CHANGED
|
@@ -178,6 +178,7 @@ export function runtimeDoctorChecks(env = process.env) {
|
|
|
178
178
|
return [
|
|
179
179
|
commandCheck("pi-agent", "Pi command", piAgentCommand(piAgentStep, env), true, env),
|
|
180
180
|
acpCheck,
|
|
181
|
+
commandCheck("codex-acp", "Codex ACP bridge", "codex-acp", false, env),
|
|
181
182
|
commandCheck("opencode", "OpenCode command (legacy)", "opencode", false, env),
|
|
182
183
|
commandCheck("claude", "Claude Code command (legacy)", "claude", false, env),
|
|
183
184
|
envCheck("openrouter-key", "OpenRouter API key", "OPENROUTER_API_KEY", env),
|
|
@@ -220,7 +221,7 @@ export function adapterMockFallbackReason(step) {
|
|
|
220
221
|
// command) but useRealAdapter is off, so the step still mocks.
|
|
221
222
|
return `adapterKey '${adapter}' is set, but useRealAdapter is not enabled, so the step runs as a mock. Set taskSpec.payload.useRealAdapter: true to run it through ACP.`;
|
|
222
223
|
}
|
|
223
|
-
// Bare acp
|
|
224
|
+
// Bare acp with no agent configured is treated as an intentional mock.
|
|
224
225
|
return null;
|
|
225
226
|
}
|
|
226
227
|
export function adapterMockFallbackWarnings(definition) {
|
|
@@ -258,7 +259,7 @@ export function adapterImplementationStatuses() {
|
|
|
258
259
|
{
|
|
259
260
|
adapter: "codex",
|
|
260
261
|
status: "partial",
|
|
261
|
-
detail: "routed through ACP
|
|
262
|
+
detail: "routed through ACP via the codex-acp bridge when useRealAdapter is true; otherwise mock",
|
|
262
263
|
},
|
|
263
264
|
{
|
|
264
265
|
adapter: "claude-code",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-manager/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
],
|
|
74
74
|
"license": "MIT",
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@
|
|
76
|
+
"@agentclientprotocol/sdk": "^1.2.1",
|
|
77
77
|
"gray-matter": "^4.0.3",
|
|
78
78
|
"picocolors": "^1.1.1"
|
|
79
79
|
},
|
|
@@ -132,6 +132,8 @@ wfm status --session-file .wfm/session.json
|
|
|
132
132
|
|
|
133
133
|
Read `status` and `currentStepKey` off the JSON, and translate: `"running"` + `currentStepKey: "implement-fix"` becomes something like *"step 1/3 (implement-fix) is running, attempt 1..."*. Don't re-poll faster than the work can plausibly progress — a few seconds between polls is usually plenty; back off further once a step has been running a while.
|
|
134
134
|
|
|
135
|
+
If you're dry-running with `adapterKey: mock`, expect steps to resolve near-instantly: your very first `status` poll may already show several steps succeeded (or the run parked at an approval gate) with no intermediate "running" beat to narrate. That's expected — narrate what you actually observe rather than assuming a step-by-step cadence.
|
|
136
|
+
|
|
135
137
|
**Use `events` for incremental detail** between polls instead of re-reading the whole snapshot:
|
|
136
138
|
|
|
137
139
|
```bash
|