UNPKG

@workflow-manager/runner

Version:

CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes

311 lines (248 loc) 18.5 kB
--- name: workflow-author description: > Author repeatable wfm workflows from a task description, validate them, run them, and narrate progress as the workflow executes. Load this skill when a user describes a multi-step task that should run the same way every time — turn it into a wfm workflow file, validate it, and then drive `wfm run` while translating status polls and approval gates into plain-language updates for the user. type: core library: "@workflow-manager/runner" sources: - "navio/workflow-manager:src/parser.ts" - "navio/workflow-manager:src/types.ts" - "navio/workflow-manager:src/engine.ts" - "navio/workflow-manager:src/index.ts" - "navio/workflow-manager:src/sessionFile.ts" - "navio/workflow-manager:doc/guide/workflow-schema.md" - "navio/workflow-manager:doc/guide/runner-api.md" --- # workflow-author Turn 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*. ## 1. When to use Use this skill when the user has a task that: - has more than one meaningful step, with a clear order or dependency between steps - should produce the **same shape of result** every time it runs not a bespoke plan improvised fresh each session - benefits from an explicit quality gate (an objective check, a human sign-off, or both) before moving on - is worth sharing: a teammate, a CI job, or a future session should be able to run it unchanged Do **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." ## 2. The authoring loop Follow this sequence; do not skip validation. 1. **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. 2. **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. 3. **Choose a quality gate per step.** For each step's `validation` (or an `approval` step's `approvalSpec.validation`), pick one: - `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. - 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. - `mode: external` an outside system resolves the step (a webhook, a deploy pipeline finishing). - `mode: none` mechanical steps with no interesting failure mode (formatting, a fixed notification). 4. **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: ```bash wfm scaffold --template agent-validated my-flow.md ``` This drops a working three-step example (agent-validated task approval finalize) that you edit in place rather than writing from a blank file. 5. **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. 6. **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. `opencode` runs real by default, so to dry-run an `opencode` step specifically, either set `taskSpec.payload.useRealAdapter: false` (keep `adapterKey: opencode`) or swap `adapterKey: mock`. This confirms dependency wiring and approval gating before spending a real adapter call. 7. **Done.** The file *is* the reusable artifact no further "session state" to preserve. Re-running it later reproduces the same step sequence. ## 3. Schema cheat-sheet Everything here matches `src/types.ts` on this branch do not invent fields. **Top level** (required: `key`, `title`, `steps`): ```yaml key: my-workflow # stable external identifier title: My Workflow # default run objective description: optional summary objectives: [optional, run-level, objectives] defaultRetryPolicy: { maxAttempts: 2 } skills: # named skills resolvable by taskSpec.init.skills my-skill: source: ./skills/my-skill/SKILL.md # must match skills/**/SKILL.md under the workflow dir steps: [ ... ] ``` **Step** (required: `key`, `kind`): ```yaml - key: my-step # stable, kebab-case kind: task # task | approval | system title: optional display title objective: optional step-level objective dependsOn: [other-step-key] retryPolicy: { maxAttempts: 2 } validation: # gates confirmation for THIS step's own record mode: none | human | external | agent required: true autoConfirm: false agent: # only when mode: agent adapterKey: pi-agent | mock | opencode | codex | claude-code | kimi | gemini | qwen | acp # default: this step's adapter criteria: "plain-language acceptance criteria the validator checks against" init: { model, skills, mcps, systemPrompts, context } payload: { mockResult: success } # lets mock drive the validator in tests taskSpec: # required when kind: task adapterKey: pi-agent | mock | opencode | codex | claude-code | kimi | gemini | qwen | acp # omit -> pi-agent init: model: openrouter/anthropic/claude-sonnet-4 skills: [skill-name] mcps: [mcp://endpoint] systemPrompts: [Focus on X] context: { any: json } payload: mockResult: success | retry | rollback | restart | yield | fail # mock adapter only timeoutMs: 600000 # per-step subprocess timeout (pi-agent default); SIGTERM on expiry approvalSpec: # required when kind: approval autoApprove: false validation: { mode: human, required: true, autoConfirm: false } ``` **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. **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`). **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`. **Step timeouts:** there is no top-level `timeoutSec` (or similar) step field `StepDefinition` has no such field, and nothing in `src/engine.ts` reads one. The only supported timeout knob is adapter-specific: `taskSpec.payload.timeoutMs`, read by the subprocess-based executors (`pi-agent`, and the deprecated bespoke `claude-code`/`opencode` executors). It's milliseconds, defaults to `600000` (10 min) for `pi-agent`, and controls how long WFM waits before sending `SIGTERM` and failing the step with `timedOut: true` in `mutated_payload`. A long-running real step (an API-backed script, a slow build) should set this explicitly rather than relying on the default. ## 4. Running and narrating (the agent-as-UI protocol) Once 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. **Start it detached, with a session file:** ```bash wfm run my-flow.md --session-file .wfm/session.json & ``` The 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`. **Poll on a cadence and narrate transitions**, not raw payloads: ```bash wfm status --session-file .wfm/session.json ``` 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. 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. **Use `events` for incremental detail** between polls instead of re-reading the whole snapshot: ```bash wfm events --session-file .wfm/session.json --since 4 ``` Track 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. **Use `logs` when the user asks what a step is doing right now:** ```bash wfm logs --session-file .wfm/session.json --step implement-fix --limit 50 ``` **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: - relay the decision the user gives you, or - if the user has explicitly delegated authority for this gate ("auto-approve the review steps," "you decide"), decide yourself and act: ```bash wfm approve --session-file .wfm/session.json --step review-gate --note "why you approved" wfm cancel --session-file .wfm/session.json --step review-gate --note "why you're stopping the run" ``` Never 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. **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): ```bash cat .wfm/session.json # { ..., "endedAt": "...", "status": "succeeded" } ``` **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. ## 5. Repeatability rules - 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. - `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. - 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. - 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. ## 6. Install/share ```bash wfm skill install workflow-author # this skill -> ./.claude/skills/ wfm skill install workflow-author --agent opencode # -> ./.opencode/skill/ wfm skill install workflow-author --global # -> ~/.claude/skills/ ``` To 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. ## Worked example A 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. ```yaml --- key: fix-flaky-login-test title: Fix Flaky Login Test description: Diagnose and fix an intermittently failing login test, with a human sign-off before landing the fix objectives: - the login test passes reliably and the fix is reviewed before merge defaultRetryPolicy: maxAttempts: 2 steps: - key: implement-fix kind: task objective: Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it dependsOn: [] retryPolicy: maxAttempts: 2 validation: mode: agent required: true autoConfirm: false agent: criteria: >- tests/login.test.ts passes 20 consecutive local runs, the fix addresses a root cause (not a retry/sleep workaround), and no unrelated files changed. init: model: openrouter/anthropic/claude-sonnet-4 systemPrompts: - Check the diff against the criteria; call out any retry/sleep workaround explicitly taskSpec: adapterKey: mock init: context: repo: example/webapp skills: [debugging, testing] systemPrompts: [Find the root cause before writing a fix; add a regression test] payload: mockResult: success - key: review-gate kind: approval objective: Human sign-off on the fix before it merges dependsOn: [implement-fix] # validation must be set here too, not just under approvalSpec — an unset # step.validation defaults to autoConfirm: true, which would silently skip # this gate. See the schema cheat-sheet above. validation: mode: human required: true autoConfirm: false approvalSpec: autoApprove: false validation: mode: human required: true autoConfirm: false - key: finalize kind: task objective: Open a PR with the fix, the regression test, and a summary of the root cause dependsOn: [review-gate] validation: mode: none required: false autoConfirm: true taskSpec: adapterKey: mock init: systemPrompts: [Open a PR summarizing the root cause, the fix, and the new regression test] payload: mockResult: success --- # Fix Flaky Login Test Repeatable workflow for chasing down a flaky test: implement a fix (agent-validated against explicit, checkable criteria), get a human sign-off, then finalize. ``` ### Simulated narration transcript ``` $ wfm run fix-flaky-login-test.md --session-file .wfm/session.json & Attach API: http://127.0.0.1:43121 (token b354...) [agent] Started the workflow in the background I'll check in as it progresses. $ wfm status --session-file .wfm/session.json {"status":"running","currentStepKey":"implement-fix", ...} [agent] step 1/3 (implement-fix) is running, attempt 1 reproducing the flake and writing a fix. $ wfm status --session-file .wfm/session.json {"status":"waiting_for_approval","currentStepKey":"review-gate", "waitingForApproval":{"stepKey":"review-gate", "reason":"confirmation required for review-gate", "preview":{"summary":"Approve this gate to continue after implement-fix.", "items":[{"title":"Dependency implement-fix","status":"succeeded", "summary":"Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it"}]}}} [agent] implement-fix passed agent validation (tests green, real root-cause fix, no unrelated files touched) and is now waiting on you: review-gate wants a human sign-off before finalize runs. Want me to look at the diff summary, or are you approving as-is? [user] approve it $ wfm approve --session-file .wfm/session.json --step review-gate --note "Fix looks correct, root cause addressed" approved review-gate [agent] Approved review-gate. finalize is running now (opening the PR)... $ cat .wfm/session.json { ..., "endedAt": "2026-07-11T13:35:40.756Z", "status": "succeeded" } [agent] Done the workflow succeeded. implement-fix and finalize both ran clean, and your sign-off on review-gate is recorded in the run's approval audit trail. ```