@workflow-manager/runner 0.7.0 → 0.8.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/dist/manPage.js CHANGED
@@ -21,25 +21,36 @@ List the agent skills bundled with the npm package.
21
21
  .B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]
22
22
  Install bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills are not overwritten unless --force is passed.
23
23
  .TP
24
- .B scaffold [path] [--format markdown|json]
24
+ .B scaffold [path] [--format markdown|json] [--template default|agent-validated]
25
25
  Create a starter workflow file. Format defaults to markdown unless the output
26
- path ends in .json.
26
+ path ends in .json. Template defaults to default (the general multi-step example);
27
+ agent-validated scaffolds a compact pipeline demonstrating first-class agent
28
+ validation (validation.mode: agent with a validator agent and criteria).
27
29
  .TP
28
30
  .B validate <workflow.md|workflow.json>
29
31
  Validate workflow structure and report schema errors.
30
32
  .TP
31
- .B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json] [--ui]
33
+ .B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--session-file path] [--verbose] [--json] [--ui]
32
34
  Run the workflow with live CLI progress and optional JSON output.
33
35
  .TP
34
- .B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
36
+ .B approve [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
35
37
  Approve the current waiting runner step through the local attach API.
36
38
  .TP
37
- .B resume [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
39
+ .B resume [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
38
40
  Alias for approve, intended for external resume flows.
39
41
  .TP
40
- .B cancel [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
42
+ .B cancel [--url value] [--token value] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]
41
43
  Cancel the current waiting runner step through the local attach API.
42
44
  .TP
45
+ .B status [--url value] [--token value] [--session-file path] [--run-id value] [--step key]
46
+ Print the current run snapshot (or one step detail with --step) as JSON on stdout.
47
+ .TP
48
+ .B logs [--url value] [--token value] [--session-file path] [--run-id value] [--step key] [--limit number] [--cursor value]
49
+ Print buffered agent stdout/stderr chunks as JSON on stdout.
50
+ .TP
51
+ .B events [--url value] [--token value] [--session-file path] [--run-id value] [--since sequence] [--include-logs]
52
+ Print run events as JSON on stdout in a single poll (no streaming). Log events are excluded unless --include-logs is passed.
53
+ .TP
43
54
  .B auth <login|whoami|logout> [--token value]
44
55
  Manage remote registry authentication for CLI publish and pull flows.
45
56
  .TP
@@ -77,6 +88,9 @@ Bypass confirmation gating for all steps.
77
88
  .B --port <number>
78
89
  Bind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.
79
90
  .TP
91
+ .B --session-file <path>
92
+ Write attach connection details (base URL, bearer token, run id, pid, timestamps) to a JSON file with mode 0600 when the run starts, and rewrite it with endedAt and the final status when the run finishes. Attach commands (approve, resume, cancel, status, logs, events) accept the same flag to read those details back.
93
+ .TP
80
94
  .B --verbose
81
95
  Stream per-step agent output and execution updates to stderr while the workflow runs.
82
96
  .TP
@@ -89,16 +103,28 @@ Full-screen terminal UI (requires a TTY; falls back to standard output).
89
103
  Human approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.
90
104
  .TP
91
105
  .B --url <value>
92
- Runner attach API base URL for approve, resume, or cancel commands.
106
+ Runner attach API base URL for approve, resume, cancel, status, logs, or events commands.
93
107
  .TP
94
108
  .B --token <value>
95
- Runner attach API bearer token for approve, resume, or cancel commands.
109
+ Runner attach API bearer token for approve, resume, cancel, status, logs, or events commands.
96
110
  .TP
97
111
  .B --run-id <value>
98
112
  Runner id to control. If omitted, the CLI reads it from /session.
99
113
  .TP
100
114
  .B --step <value>
101
- Optional step key when controlling a specific waiting step.
115
+ Optional step key when controlling a specific waiting step, or when scoping status and logs output.
116
+ .TP
117
+ .B --limit <number>
118
+ Maximum number of log chunks returned by the logs command. Defaults to 200.
119
+ .TP
120
+ .B --cursor <value>
121
+ Pagination cursor for the logs command, taken from a previous nextCursor value.
122
+ .TP
123
+ .B --since <sequence>
124
+ Only return events with a sequence greater than this value in the events command.
125
+ .TP
126
+ .B --include-logs
127
+ Include agent.stdout and agent.stderr events in the events command output.
102
128
  .TP
103
129
  .B --actor <value>
104
130
  Actor name recorded in approval audit events.
@@ -116,6 +142,9 @@ Validate json workflow:
116
142
  Scaffold json workflow file:
117
143
  .B wfm scaffold ./new-workflow.json --format json
118
144
  .TP
145
+ Scaffold an agent-validated pipeline example:
146
+ .B wfm scaffold ./agent-validated.md --template agent-validated
147
+ .TP
119
148
  Authenticate with a CLI token:
120
149
  .B wfm auth login --token wm_exampletoken
121
150
  .TP
@@ -131,6 +160,12 @@ Search the remote registry:
131
160
  Run with explicit confirmations:
132
161
  .B wfm run ./example-workflow.json --confirm discover:human,qa_gate:human
133
162
  .TP
163
+ Run with a session file for attach clients:
164
+ .B wfm run ./example-workflow.json --session-file ./run-session.json
165
+ .TP
166
+ Observe and control the run through the session file:
167
+ .B wfm status --session-file ./run-session.json && wfm approve --session-file ./run-session.json --step qa_gate
168
+ .TP
134
169
  Inspect host setup:
135
170
  .B wfm doctor
136
171
  .TP
package/dist/parser.js CHANGED
@@ -185,9 +185,18 @@ export function validateWorkflow(def) {
185
185
  errors.push(`Unsupported adapter for ${step.key}: ${adapter}`);
186
186
  }
187
187
  const mode = step.validation?.mode ?? step.approvalSpec?.validation?.mode;
188
- if (mode && !["none", "human", "external"].includes(mode)) {
188
+ if (mode && !["none", "human", "external", "agent"].includes(mode)) {
189
189
  errors.push(`Invalid validation mode for ${step.key}: ${mode}`);
190
190
  }
191
+ if (step.approvalSpec?.validation?.mode === "agent") {
192
+ errors.push(`Approval step ${step.key} cannot use agent validation`);
193
+ }
194
+ if (step.validation?.mode === "agent" && step.validation.agent?.adapterKey) {
195
+ const validatorAdapter = step.validation.agent.adapterKey;
196
+ if (!SUPPORTED_ADAPTERS.includes(validatorAdapter)) {
197
+ errors.push(`Unsupported validator adapter for ${step.key}: ${validatorAdapter}`);
198
+ }
199
+ }
191
200
  }
192
201
  const cycle = findDependencyCycle(def);
193
202
  if (cycle) {
package/dist/runnerApi.js CHANGED
@@ -136,6 +136,20 @@ export async function startRunnerApiServer(store, requestedPort) {
136
136
  });
137
137
  return;
138
138
  }
139
+ const eventsListMatch = pathname.match(/^\/runs\/([^/]+)\/events\/list$/);
140
+ if (method === "GET" && eventsListMatch) {
141
+ const runId = decodeURIComponent(eventsListMatch[1] ?? "");
142
+ if (!store.isKnownRun(runId)) {
143
+ errorResponse(res, 404, "not_found", `Unknown run: ${runId}`);
144
+ return;
145
+ }
146
+ const sinceSequence = Number.parseInt(url.searchParams.get("sinceSequence") ?? "0", 10) || undefined;
147
+ const includeLogs = parseBoolean(url.searchParams.get("includeLogs"), true);
148
+ const items = store.events(sinceSequence, includeLogs);
149
+ const nextSequence = items.reduce((max, event) => Math.max(max, event.sequence), sinceSequence ?? 0);
150
+ jsonResponse(res, 200, { items, nextSequence });
151
+ return;
152
+ }
139
153
  const eventsMatch = pathname.match(/^\/runs\/([^/]+)\/events$/);
140
154
  if (method === "GET" && eventsMatch) {
141
155
  const runId = decodeURIComponent(eventsMatch[1] ?? "");
@@ -0,0 +1,11 @@
1
+ export interface RunnerSessionFile {
2
+ baseUrl: string;
3
+ attachToken: string;
4
+ runId: string;
5
+ pid: number;
6
+ startedAt: string;
7
+ endedAt?: string;
8
+ status?: string;
9
+ }
10
+ export declare function writeSessionFile(filePath: string, session: RunnerSessionFile): void;
11
+ export declare function readSessionFile(filePath: string): RunnerSessionFile | string;
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function writeSessionFile(filePath, session) {
4
+ const resolvedPath = path.resolve(filePath);
5
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
6
+ fs.writeFileSync(resolvedPath, `${JSON.stringify(session, null, 2)}\n`, {
7
+ encoding: "utf-8",
8
+ mode: 0o600,
9
+ });
10
+ // writeFileSync only applies the mode on creation; enforce it on rewrites too.
11
+ fs.chmodSync(resolvedPath, 0o600);
12
+ }
13
+ export function readSessionFile(filePath) {
14
+ const resolvedPath = path.resolve(filePath);
15
+ let raw;
16
+ try {
17
+ raw = fs.readFileSync(resolvedPath, "utf-8");
18
+ }
19
+ catch (error) {
20
+ return `Could not read session file ${resolvedPath}: ${error.message}`;
21
+ }
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return `Session file ${resolvedPath} is not valid JSON`;
28
+ }
29
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
30
+ return `Session file ${resolvedPath} must contain a JSON object`;
31
+ }
32
+ const record = parsed;
33
+ for (const field of ["baseUrl", "attachToken", "runId", "startedAt"]) {
34
+ if (typeof record[field] !== "string" || record[field] === "") {
35
+ return `Session file ${resolvedPath} is missing required field: ${field}`;
36
+ }
37
+ }
38
+ if (typeof record.pid !== "number" || !Number.isInteger(record.pid)) {
39
+ return `Session file ${resolvedPath} is missing required field: pid`;
40
+ }
41
+ if (record.endedAt !== undefined && typeof record.endedAt !== "string") {
42
+ return `Session file ${resolvedPath} has an invalid endedAt field`;
43
+ }
44
+ if (record.status !== undefined && typeof record.status !== "string") {
45
+ return `Session file ${resolvedPath} has an invalid status field`;
46
+ }
47
+ const session = {
48
+ baseUrl: record.baseUrl,
49
+ attachToken: record.attachToken,
50
+ runId: record.runId,
51
+ pid: record.pid,
52
+ startedAt: record.startedAt,
53
+ };
54
+ if (typeof record.endedAt === "string") {
55
+ session.endedAt = record.endedAt;
56
+ }
57
+ if (typeof record.status === "string") {
58
+ session.status = record.status;
59
+ }
60
+ return session;
61
+ }
@@ -38,6 +38,17 @@ function buildEventMetaText(event) {
38
38
  const feedbackReason = readString(payload, "feedbackReason");
39
39
  return `execution finished: ${status}${action ? ` action=${action}` : ""}${feedbackReason ? ` reason=${feedbackReason}` : ""}`;
40
40
  }
41
+ if (event.type === "step.validation_started") {
42
+ const adapter = readString(payload, "adapter");
43
+ const criteria = readString(payload, "criteria");
44
+ return `agent validation started${adapter ? ` (${adapter})` : ""}${criteria ? ` criteria=${criteria}` : ""}`;
45
+ }
46
+ if (event.type === "step.validation_finished") {
47
+ const status = readString(payload, "status") ?? "unknown";
48
+ const action = readString(payload, "action");
49
+ const feedbackReason = readString(payload, "feedbackReason");
50
+ return `agent validation: ${status}${action ? ` action=${action}` : ""}${feedbackReason ? ` reason=${feedbackReason}` : ""}`;
51
+ }
41
52
  if (event.type === "step.retried") {
42
53
  const attempt = typeof payload.attempt === "number" ? payload.attempt : undefined;
43
54
  return attempt !== undefined ? `retry scheduled (attempt ${attempt})` : "retry scheduled";
package/dist/types.d.ts CHANGED
@@ -4,17 +4,11 @@ export type StepKind = "task" | "approval" | "system";
4
4
  export type NodeType = "AGENT" | "HUMAN" | "SYSTEM";
5
5
  export type ExecutionStatus = "SUCCESS" | "QA_REJECTED" | "YIELD_EXTERNAL" | "FAILED";
6
6
  export type QaAction = "PROCEED" | "RETRY_CURRENT" | "ROLLBACK_PREVIOUS" | "RESTART_ALL";
7
- export type ValidationMode = "none" | "human" | "external";
7
+ export type ValidationMode = "none" | "human" | "external" | "agent";
8
8
  export type AdapterKey = "pi-agent" | "mock" | "opencode" | "codex" | "claude-code" | "acp";
9
9
  export interface RetryPolicy {
10
10
  maxAttempts?: number;
11
11
  }
12
- export interface ValidationSpec {
13
- mode?: ValidationMode;
14
- required?: boolean;
15
- autoConfirm?: boolean;
16
- confirmerPolicy?: string;
17
- }
18
12
  export interface TaskInitConfig {
19
13
  context?: Record<string, unknown> | string;
20
14
  skills?: string[];
@@ -22,6 +16,19 @@ export interface TaskInitConfig {
22
16
  systemPrompts?: string[];
23
17
  model?: string;
24
18
  }
19
+ export interface AgentValidationSpec {
20
+ adapterKey?: AdapterKey;
21
+ init?: TaskInitConfig;
22
+ criteria?: string;
23
+ payload?: Record<string, unknown>;
24
+ }
25
+ export interface ValidationSpec {
26
+ mode?: ValidationMode;
27
+ required?: boolean;
28
+ autoConfirm?: boolean;
29
+ confirmerPolicy?: string;
30
+ agent?: AgentValidationSpec;
31
+ }
25
32
  export interface StepDefinition {
26
33
  key: string;
27
34
  kind: StepKind;
@@ -260,7 +267,7 @@ export interface RunEvent {
260
267
  id: string;
261
268
  runId: string;
262
269
  stepRunId?: string;
263
- type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "agent.started" | "agent.stdout" | "agent.stderr" | "agent.finished" | "run.completed" | "run.failed" | "run.cancelled";
270
+ type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.validation_started" | "step.validation_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "agent.started" | "agent.stdout" | "agent.stderr" | "agent.finished" | "run.completed" | "run.failed" | "run.cancelled";
264
271
  sequenceNumber: number;
265
272
  occurredAt: string;
266
273
  actor: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-manager/runner",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,6 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.json",
26
+ "generate:skills": "node ./scripts/generate-bundled-skills.mjs",
26
27
  "build:bin": "node ./scripts/build-binary.mjs --outfile ./dist/wfm",
27
28
  "build:bin:macos": "node ./scripts/build-binary.mjs --target bun-darwin-arm64 --outfile ./dist/wfm-macos-arm64",
28
29
  "build:bin:linux": "node ./scripts/build-binary.mjs --target bun-linux-x64 --outfile ./dist/wfm-linux-x64",
@@ -0,0 +1,18 @@
1
+ # workflow-author skill
2
+
3
+ Teaches a host coding agent (Claude Code, opencode, pi) how to turn a natural-language
4
+ task description into a repeatable `wfm` workflow, validate it, run it, and narrate
5
+ progress — including approval gates — back to the user.
6
+
7
+ This skill ships inside the root `@workflow-manager/runner` npm package alongside
8
+ `workflow-manager-cli`. Install it into an agent's skill directory with:
9
+
10
+ ```bash
11
+ wfm skill install workflow-author # -> ./.claude/skills/
12
+ wfm skill install workflow-author --agent opencode # -> ./.opencode/skill/
13
+ wfm skill install workflow-author --global # -> ~/.claude/skills/
14
+ ```
15
+
16
+ See `SKILL.md` for the full operating manual, and `doc/guide/workflow-schema.md` /
17
+ `doc/guide/runner-api.md` for the underlying schema and attach-API contract this
18
+ skill is built on.
@@ -0,0 +1,306 @@
1
+ ---
2
+ name: workflow-author
3
+ description: >
4
+ Author repeatable wfm workflows from a task description, validate them, run
5
+ them, and narrate progress as the workflow executes. Load this skill when a
6
+ user describes a multi-step task that should run the same way every time —
7
+ turn it into a wfm workflow file, validate it, and then drive `wfm run`
8
+ while translating status polls and approval gates into plain-language
9
+ updates for the user.
10
+ type: core
11
+ library: "@workflow-manager/runner"
12
+ sources:
13
+ - "navio/workflow-manager:src/parser.ts"
14
+ - "navio/workflow-manager:src/types.ts"
15
+ - "navio/workflow-manager:src/engine.ts"
16
+ - "navio/workflow-manager:src/index.ts"
17
+ - "navio/workflow-manager:src/sessionFile.ts"
18
+ - "navio/workflow-manager:doc/guide/workflow-schema.md"
19
+ - "navio/workflow-manager:doc/guide/runner-api.md"
20
+ ---
21
+
22
+ # workflow-author
23
+
24
+ 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*.
25
+
26
+ ## 1. When to use
27
+
28
+ Use this skill when the user has a task that:
29
+
30
+ - has more than one meaningful step, with a clear order or dependency between steps
31
+ - should produce the **same shape of result** every time it runs — not a bespoke plan improvised fresh each session
32
+ - benefits from an explicit quality gate (an objective check, a human sign-off, or both) before moving on
33
+ - is worth sharing: a teammate, a CI job, or a future session should be able to run it unchanged
34
+
35
+ 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."
36
+
37
+ ## 2. The authoring loop
38
+
39
+ Follow this sequence; do not skip validation.
40
+
41
+ 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.
42
+ 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.
43
+ 3. **Choose a quality gate per step.** For each step's `validation` (or an `approval` step's `approvalSpec.validation`), pick one:
44
+ - `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.
45
+ - 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.
46
+ - `mode: external` — an outside system resolves the step (a webhook, a deploy pipeline finishing).
47
+ - `mode: none` — mechanical steps with no interesting failure mode (formatting, a fixed notification).
48
+ 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:
49
+ ```bash
50
+ wfm scaffold --template agent-validated my-flow.md
51
+ ```
52
+ This drops a working three-step example (agent-validated task → approval → finalize) that you edit in place rather than writing from a blank file.
53
+ 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.
54
+ 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. This confirms dependency wiring and approval gating before spending a real adapter call.
55
+ 7. **Done.** The file *is* the reusable artifact — no further "session state" to preserve. Re-running it later reproduces the same step sequence.
56
+
57
+ ## 3. Schema cheat-sheet
58
+
59
+ Everything here matches `src/types.ts` on this branch — do not invent fields.
60
+
61
+ **Top level** (required: `key`, `title`, `steps`):
62
+
63
+ ```yaml
64
+ key: my-workflow # stable external identifier
65
+ title: My Workflow # default run objective
66
+ description: optional summary
67
+ objectives: [optional, run-level, objectives]
68
+ defaultRetryPolicy: { maxAttempts: 2 }
69
+ skills: # named skills resolvable by taskSpec.init.skills
70
+ my-skill:
71
+ source: ./skills/my-skill/SKILL.md # must match skills/**/SKILL.md under the workflow dir
72
+ steps: [ ... ]
73
+ ```
74
+
75
+ **Step** (required: `key`, `kind`):
76
+
77
+ ```yaml
78
+ - key: my-step # stable, kebab-case
79
+ kind: task # task | approval | system
80
+ title: optional display title
81
+ objective: optional step-level objective
82
+ dependsOn: [other-step-key]
83
+ timeoutSec: optional
84
+ retryPolicy: { maxAttempts: 2 }
85
+ validation: # gates confirmation for THIS step's own record
86
+ mode: none | human | external | agent
87
+ required: true
88
+ autoConfirm: false
89
+ agent: # only when mode: agent
90
+ adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # default: this step's adapter
91
+ criteria: "plain-language acceptance criteria the validator checks against"
92
+ init: { model, skills, mcps, systemPrompts, context }
93
+ payload: { mockResult: success } # lets mock drive the validator in tests
94
+ taskSpec: # required when kind: task
95
+ adapterKey: pi-agent | mock | opencode | codex | claude-code | acp # omit -> pi-agent
96
+ init:
97
+ model: openrouter/anthropic/claude-sonnet-4
98
+ skills: [skill-name]
99
+ mcps: [mcp://endpoint]
100
+ systemPrompts: [Focus on X]
101
+ context: { any: json }
102
+ payload:
103
+ mockResult: success | retry | rollback | restart | yield | fail # mock adapter only
104
+ approvalSpec: # required when kind: approval
105
+ autoApprove: false
106
+ validation: { mode: human, required: true, autoConfirm: false }
107
+ ```
108
+
109
+ **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.
110
+
111
+ **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`).
112
+
113
+ **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`.
114
+
115
+ ## 4. Running and narrating (the agent-as-UI protocol)
116
+
117
+ 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.
118
+
119
+ **Start it detached, with a session file:**
120
+
121
+ ```bash
122
+ wfm run my-flow.md --session-file .wfm/session.json &
123
+ ```
124
+
125
+ 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`.
126
+
127
+ **Poll on a cadence and narrate transitions**, not raw payloads:
128
+
129
+ ```bash
130
+ wfm status --session-file .wfm/session.json
131
+ ```
132
+
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
+
135
+ **Use `events` for incremental detail** between polls instead of re-reading the whole snapshot:
136
+
137
+ ```bash
138
+ wfm events --session-file .wfm/session.json --since 4
139
+ ```
140
+
141
+ 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.
142
+
143
+ **Use `logs` when the user asks what a step is doing right now:**
144
+
145
+ ```bash
146
+ wfm logs --session-file .wfm/session.json --step implement-fix --limit 50
147
+ ```
148
+
149
+ **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:
150
+
151
+ - relay the decision the user gives you, or
152
+ - if the user has explicitly delegated authority for this gate ("auto-approve the review steps," "you decide"), decide yourself and act:
153
+
154
+ ```bash
155
+ wfm approve --session-file .wfm/session.json --step review-gate --note "why you approved"
156
+ wfm cancel --session-file .wfm/session.json --step review-gate --note "why you're stopping the run"
157
+ ```
158
+
159
+ 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.
160
+
161
+ **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):
162
+
163
+ ```bash
164
+ cat .wfm/session.json # { ..., "endedAt": "...", "status": "succeeded" }
165
+ ```
166
+
167
+ **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.
168
+
169
+ ## 5. Repeatability rules
170
+
171
+ - 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.
172
+ - `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.
173
+ - 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.
174
+ - 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.
175
+
176
+ ## 6. Install/share
177
+
178
+ ```bash
179
+ wfm skill install workflow-author # this skill -> ./.claude/skills/
180
+ wfm skill install workflow-author --agent opencode # -> ./.opencode/skill/
181
+ wfm skill install workflow-author --global # -> ~/.claude/skills/
182
+ ```
183
+
184
+ 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.
185
+
186
+ ## Worked example
187
+
188
+ 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.
189
+
190
+ ```yaml
191
+ ---
192
+ key: fix-flaky-login-test
193
+ title: Fix Flaky Login Test
194
+ description: Diagnose and fix an intermittently failing login test, with a human sign-off before landing the fix
195
+ objectives:
196
+ - the login test passes reliably and the fix is reviewed before merge
197
+ defaultRetryPolicy:
198
+ maxAttempts: 2
199
+ steps:
200
+ - key: implement-fix
201
+ kind: task
202
+ objective: Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it
203
+ dependsOn: []
204
+ retryPolicy:
205
+ maxAttempts: 2
206
+ validation:
207
+ mode: agent
208
+ required: true
209
+ autoConfirm: false
210
+ agent:
211
+ criteria: >-
212
+ tests/login.test.ts passes 20 consecutive local runs, the fix
213
+ addresses a root cause (not a retry/sleep workaround), and no
214
+ unrelated files changed.
215
+ init:
216
+ model: openrouter/anthropic/claude-sonnet-4
217
+ systemPrompts:
218
+ - Check the diff against the criteria; call out any retry/sleep workaround explicitly
219
+ taskSpec:
220
+ adapterKey: mock
221
+ init:
222
+ context:
223
+ repo: example/webapp
224
+ skills: [debugging, testing]
225
+ systemPrompts: [Find the root cause before writing a fix; add a regression test]
226
+ payload:
227
+ mockResult: success
228
+ - key: review-gate
229
+ kind: approval
230
+ objective: Human sign-off on the fix before it merges
231
+ dependsOn: [implement-fix]
232
+ # validation must be set here too, not just under approvalSpec — an unset
233
+ # step.validation defaults to autoConfirm: true, which would silently skip
234
+ # this gate. See the schema cheat-sheet above.
235
+ validation:
236
+ mode: human
237
+ required: true
238
+ autoConfirm: false
239
+ approvalSpec:
240
+ autoApprove: false
241
+ validation:
242
+ mode: human
243
+ required: true
244
+ autoConfirm: false
245
+ - key: finalize
246
+ kind: task
247
+ objective: Open a PR with the fix, the regression test, and a summary of the root cause
248
+ dependsOn: [review-gate]
249
+ validation:
250
+ mode: none
251
+ required: false
252
+ autoConfirm: true
253
+ taskSpec:
254
+ adapterKey: mock
255
+ init:
256
+ systemPrompts: [Open a PR summarizing the root cause, the fix, and the new regression test]
257
+ payload:
258
+ mockResult: success
259
+ ---
260
+
261
+ # Fix Flaky Login Test
262
+
263
+ Repeatable workflow for chasing down a flaky test: implement a fix (agent-validated
264
+ against explicit, checkable criteria), get a human sign-off, then finalize.
265
+ ```
266
+
267
+ ### Simulated narration transcript
268
+
269
+ ```
270
+ $ wfm run fix-flaky-login-test.md --session-file .wfm/session.json &
271
+ Attach API: http://127.0.0.1:43121 (token b354...)
272
+
273
+ [agent] Started the workflow in the background — I'll check in as it progresses.
274
+
275
+ $ wfm status --session-file .wfm/session.json
276
+ {"status":"running","currentStepKey":"implement-fix", ...}
277
+
278
+ [agent] step 1/3 (implement-fix) is running, attempt 1 — reproducing the flake and writing a fix.
279
+
280
+ $ wfm status --session-file .wfm/session.json
281
+ {"status":"waiting_for_approval","currentStepKey":"review-gate",
282
+ "waitingForApproval":{"stepKey":"review-gate",
283
+ "reason":"confirmation required for review-gate",
284
+ "preview":{"summary":"Approve this gate to continue after implement-fix.",
285
+ "items":[{"title":"Dependency implement-fix","status":"succeeded",
286
+ "summary":"Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it"}]}}}
287
+
288
+ [agent] implement-fix passed agent validation (tests green, real root-cause fix,
289
+ no unrelated files touched) and is now waiting on you: review-gate wants a
290
+ human sign-off before finalize runs. Want me to look at the diff summary, or
291
+ are you approving as-is?
292
+
293
+ [user] approve it
294
+
295
+ $ wfm approve --session-file .wfm/session.json --step review-gate --note "Fix looks correct, root cause addressed"
296
+ approved review-gate
297
+
298
+ [agent] Approved review-gate. finalize is running now (opening the PR)...
299
+
300
+ $ cat .wfm/session.json
301
+ { ..., "endedAt": "2026-07-11T13:35:40.756Z", "status": "succeeded" }
302
+
303
+ [agent] Done — the workflow succeeded. implement-fix and finalize both ran
304
+ clean, and your sign-off on review-gate is recorded in the run's approval
305
+ audit trail.
306
+ ```