@workflow-manager/runner 0.7.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 +5 -1
- package/dist/acpExecutor.d.ts +2 -2
- package/dist/acpExecutor.js +9 -8
- package/dist/cliRunRenderer.js +17 -0
- package/dist/engine.js +201 -68
- package/dist/generated/bundledSkills.d.ts +5 -0
- package/dist/generated/bundledSkills.js +22 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +409 -55
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +44 -9
- package/dist/parser.js +10 -1
- package/dist/runnerApi.js +14 -0
- package/dist/runtimePreflight.js +3 -2
- package/dist/sessionFile.d.ts +11 -0
- package/dist/sessionFile.js +61 -0
- package/dist/tui/tuiRunRenderer.js +11 -0
- package/dist/types.d.ts +15 -8
- package/package.json +3 -2
- package/skills/workflow-author/README.md +18 -0
- package/skills/workflow-author/SKILL.md +308 -0
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
|
|
|
@@ -233,6 +233,10 @@ wfm remote info alice/remote-bunny
|
|
|
233
233
|
|
|
234
234
|
The published `@workflow-manager/runner` npm package ships the CLI runner and the bundled agent skills together. The primary skill is `skills/workflow-manager-cli/SKILL.md`, which teaches an agent how to configure, author, run, and publish workflows with `wfm`.
|
|
235
235
|
|
|
236
|
+
### Let your agent build workflows
|
|
237
|
+
|
|
238
|
+
For a task that should run the same repeatable way every time, install the `workflow-author` skill (`wfm skill install workflow-author`) and describe the task to your coding agent instead of writing the workflow file by hand. The agent elicits success criteria, decomposes the task into steps with an explicit quality gate each (agent-validated, human-approved, or external), scaffolds and validates the file, then runs it with `--session-file` and narrates progress and approval gates back to you. See [`doc/guide/authoring-with-an-agent.md`](doc/guide/authoring-with-an-agent.md) for the full loop.
|
|
239
|
+
|
|
236
240
|
Install skills with the CLI:
|
|
237
241
|
|
|
238
242
|
```bash
|
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);
|
package/dist/cliRunRenderer.js
CHANGED
|
@@ -89,6 +89,23 @@ export class CliRunRenderer {
|
|
|
89
89
|
this.write(` ${stepSummary} execution finished: ${status}${action}${feedbackReason}`);
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
|
+
if (event.type === "step.validation_started") {
|
|
93
|
+
const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId);
|
|
94
|
+
const adapter = typeof event.payload.adapter === "string" ? ` (${event.payload.adapter})` : "";
|
|
95
|
+
const criteria = typeof event.payload.criteria === "string" ? ` criteria=${event.payload.criteria}` : "";
|
|
96
|
+
this.write(` ${stepSummary} agent validation started${adapter}${criteria}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (event.type === "step.validation_finished") {
|
|
100
|
+
const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId);
|
|
101
|
+
const status = typeof event.payload.status === "string" ? event.payload.status : "unknown";
|
|
102
|
+
const action = typeof event.payload.action === "string" ? ` action=${event.payload.action}` : "";
|
|
103
|
+
const feedbackReason = typeof event.payload.feedbackReason === "string" && event.payload.feedbackReason.length > 0
|
|
104
|
+
? ` reason=${event.payload.feedbackReason}`
|
|
105
|
+
: "";
|
|
106
|
+
this.write(` ${stepSummary} agent validation: ${status}${action}${feedbackReason}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
92
109
|
if (event.type === "step.retried") {
|
|
93
110
|
const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId);
|
|
94
111
|
const attempt = typeof event.payload.attempt === "number" ? ` next attempt ${event.payload.attempt}` : " retry queued";
|
package/dist/engine.js
CHANGED
|
@@ -115,7 +115,9 @@ export function canUseInteractiveConfirmation(step) {
|
|
|
115
115
|
}
|
|
116
116
|
function canConfirm(step, options, output) {
|
|
117
117
|
const mode = requiresValidation(step);
|
|
118
|
-
|
|
118
|
+
// Agent validation is a QA gate handled by executeStep's validation pass below, not a
|
|
119
|
+
// human confirmer — it must never be short-circuited by autoConfirmAll/confirmations.
|
|
120
|
+
if ((mode === "none" || mode === "agent") && output.execution_status !== "YIELD_EXTERNAL")
|
|
119
121
|
return { ok: true };
|
|
120
122
|
if (options.autoConfirmAll)
|
|
121
123
|
return { ok: true };
|
|
@@ -501,6 +503,189 @@ export async function runWorkflow(definition, options) {
|
|
|
501
503
|
emitSnapshot();
|
|
502
504
|
return resolution;
|
|
503
505
|
};
|
|
506
|
+
// Applies a QA rejection's routing action (RETRY_CURRENT / ROLLBACK_PREVIOUS / RESTART_ALL /
|
|
507
|
+
// unknown) to `step`, mutating run/step state exactly the way a step's own self-reported
|
|
508
|
+
// QA_REJECTED output would. Shared by the direct QA_REJECTED path and by agent validation,
|
|
509
|
+
// which routes its verdict through the same machinery.
|
|
510
|
+
const applyQaRejection = (step, stepRun, index, qaAction, feedbackReason) => {
|
|
511
|
+
const retryMax = step.retryPolicy?.maxAttempts ?? definition.defaultRetryPolicy?.maxAttempts ?? 1;
|
|
512
|
+
if (qaAction === "RETRY_CURRENT") {
|
|
513
|
+
if (stepRun.attempt < retryMax) {
|
|
514
|
+
stepRun.status = "pending";
|
|
515
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
516
|
+
pushEvent("step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
|
|
517
|
+
emitSnapshot();
|
|
518
|
+
return { index };
|
|
519
|
+
}
|
|
520
|
+
stepRun.status = "failed";
|
|
521
|
+
runStatus = "failed";
|
|
522
|
+
currentStepKey = step.key;
|
|
523
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
524
|
+
touchRun(true);
|
|
525
|
+
pushEvent("run.failed", { stepKey: step.key, reason: feedbackReason ? `max retry exceeded: ${feedbackReason}` : "max retry exceeded" }, step.key);
|
|
526
|
+
emitSnapshot();
|
|
527
|
+
return { stop: true };
|
|
528
|
+
}
|
|
529
|
+
if (qaAction === "ROLLBACK_PREVIOUS") {
|
|
530
|
+
if (index === 0) {
|
|
531
|
+
stepRun.status = "failed";
|
|
532
|
+
runStatus = "failed";
|
|
533
|
+
currentStepKey = step.key;
|
|
534
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
535
|
+
touchRun(true);
|
|
536
|
+
pushEvent("run.failed", {
|
|
537
|
+
stepKey: step.key,
|
|
538
|
+
reason: feedbackReason
|
|
539
|
+
? `cannot rollback before first step: ${feedbackReason}`
|
|
540
|
+
: "cannot rollback before first step",
|
|
541
|
+
}, step.key);
|
|
542
|
+
emitSnapshot();
|
|
543
|
+
return { stop: true };
|
|
544
|
+
}
|
|
545
|
+
const prevStep = orderedSteps[index - 1];
|
|
546
|
+
const prevRun = stepRuns.get(prevStep.key);
|
|
547
|
+
prevRun.status = "pending";
|
|
548
|
+
prevRun.attempt = 0;
|
|
549
|
+
prevRun.confirmed = false;
|
|
550
|
+
delete prevRun.output;
|
|
551
|
+
delete globalState[prevStep.key];
|
|
552
|
+
delete stepRun.output;
|
|
553
|
+
delete globalState[step.key];
|
|
554
|
+
touchStep(prevStep.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
555
|
+
pushEvent("step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
|
|
556
|
+
stepRun.status = "pending";
|
|
557
|
+
stepRun.confirmed = false;
|
|
558
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
559
|
+
emitSnapshot();
|
|
560
|
+
return { index: index - 1 };
|
|
561
|
+
}
|
|
562
|
+
if (qaAction === "RESTART_ALL") {
|
|
563
|
+
for (const s of definition.steps) {
|
|
564
|
+
const sr = stepRuns.get(s.key);
|
|
565
|
+
sr.status = "pending";
|
|
566
|
+
sr.attempt = 0;
|
|
567
|
+
sr.confirmed = false;
|
|
568
|
+
delete sr.output;
|
|
569
|
+
delete globalState[s.key];
|
|
570
|
+
touchStep(s.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
571
|
+
}
|
|
572
|
+
currentStepKey = null;
|
|
573
|
+
pushEvent("step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
|
|
574
|
+
emitSnapshot();
|
|
575
|
+
return { index: 0 };
|
|
576
|
+
}
|
|
577
|
+
stepRun.status = "failed";
|
|
578
|
+
runStatus = "failed";
|
|
579
|
+
currentStepKey = step.key;
|
|
580
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
581
|
+
touchRun(true);
|
|
582
|
+
pushEvent("run.failed", { stepKey: step.key, reason: `Unknown QA action: ${qaAction}` }, step.key);
|
|
583
|
+
emitSnapshot();
|
|
584
|
+
return { stop: true };
|
|
585
|
+
};
|
|
586
|
+
// Runs the step's configured validator agent against its just-produced output and folds the
|
|
587
|
+
// verdict back into the SAME routing machinery a self-reported QA_REJECTED would use. Runs
|
|
588
|
+
// unconditionally for validation.mode "agent" — autoConfirmAll/confirmations never skip it,
|
|
589
|
+
// since it is a QA gate, not a human approval.
|
|
590
|
+
const runAgentValidation = async (step, stepRun, index, input, executionOutput) => {
|
|
591
|
+
const agentSpec = step.validation?.agent ?? {};
|
|
592
|
+
const validatorAdapter = agentSpec.adapterKey ?? resolveTaskAdapter(step.taskSpec?.adapterKey);
|
|
593
|
+
const criteria = agentSpec.criteria;
|
|
594
|
+
const objective = criteria
|
|
595
|
+
? `Validate the output of step "${step.key}": ${criteria}`
|
|
596
|
+
: `Validate the output of step "${step.key}"`;
|
|
597
|
+
const validatorStep = {
|
|
598
|
+
key: step.key,
|
|
599
|
+
kind: "task",
|
|
600
|
+
title: step.title,
|
|
601
|
+
objective,
|
|
602
|
+
dependsOn: step.dependsOn,
|
|
603
|
+
taskSpec: {
|
|
604
|
+
adapterKey: validatorAdapter,
|
|
605
|
+
init: agentSpec.init,
|
|
606
|
+
payload: agentSpec.payload ?? {},
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
const validatorInput = {
|
|
610
|
+
global_context: input.global_context,
|
|
611
|
+
step_context: {
|
|
612
|
+
step_id: step.key,
|
|
613
|
+
step_objective: objective,
|
|
614
|
+
previous_output: { [step.key]: executionOutput.mutated_payload },
|
|
615
|
+
assigned_node_type: "AGENT",
|
|
616
|
+
},
|
|
617
|
+
priming_configuration: {
|
|
618
|
+
required_skills: agentSpec.init?.skills ?? [],
|
|
619
|
+
mcp_endpoints: agentSpec.init?.mcps ?? [],
|
|
620
|
+
system_prompts: agentSpec.init?.systemPrompts ?? [],
|
|
621
|
+
context: agentSpec.init?.context,
|
|
622
|
+
adapter: validatorAdapter,
|
|
623
|
+
model: agentSpec.init?.model,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
pushEvent("step.validation_started", { adapter: validatorAdapter, criteria: criteria ?? null }, step.key);
|
|
627
|
+
emitSnapshot();
|
|
628
|
+
const validationHooks = {
|
|
629
|
+
onStarted: (payload) => {
|
|
630
|
+
pushEvent("agent.started", { attempt: stepRun.attempt, validator: true, ...(payload ?? {}) }, step.key);
|
|
631
|
+
},
|
|
632
|
+
onStdout: (chunk) => {
|
|
633
|
+
emitLog(step.key, "stdout", chunk);
|
|
634
|
+
pushEvent("agent.stdout", { stream: "stdout", text: chunk }, step.key);
|
|
635
|
+
},
|
|
636
|
+
onStderr: (chunk) => {
|
|
637
|
+
emitLog(step.key, "stderr", chunk);
|
|
638
|
+
pushEvent("agent.stderr", { stream: "stderr", text: chunk }, step.key);
|
|
639
|
+
},
|
|
640
|
+
onFinished: (payload) => {
|
|
641
|
+
pushEvent("agent.finished", { attempt: stepRun.attempt, validator: true, ...(payload ?? {}) }, step.key);
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
const validatorOutput = validatedExecutorOutput(validatorStep, validatorInput, stepRun.attempt, await executeStep(validatorStep, validatorInput, stepRun.attempt, definition, workflowFilePath, validationHooks));
|
|
645
|
+
pushEvent("step.validation_finished", {
|
|
646
|
+
status: validatorOutput.execution_status,
|
|
647
|
+
action: validatorOutput.qa_routing.action,
|
|
648
|
+
feedbackReason: validatorOutput.qa_routing.feedback_reason,
|
|
649
|
+
}, step.key);
|
|
650
|
+
emitSnapshot();
|
|
651
|
+
if (validatorOutput.execution_status === "SUCCESS" && validatorOutput.qa_routing.action === "PROCEED") {
|
|
652
|
+
return { type: "proceed" };
|
|
653
|
+
}
|
|
654
|
+
if (validatorOutput.execution_status === "FAILED" || validatorOutput.execution_status === "YIELD_EXTERNAL") {
|
|
655
|
+
const reason = `agent validation failed: ${validatorOutput.qa_routing.feedback_reason || validatorOutput.execution_status}`;
|
|
656
|
+
stepRun.status = "failed";
|
|
657
|
+
runStatus = "failed";
|
|
658
|
+
currentStepKey = step.key;
|
|
659
|
+
touchStep(step.key, {
|
|
660
|
+
finishedAt: new Date().toISOString(),
|
|
661
|
+
lastExecution: {
|
|
662
|
+
executionStatus: "FAILED",
|
|
663
|
+
qaAction: "PROCEED",
|
|
664
|
+
feedbackReason: reason,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
touchRun(true);
|
|
668
|
+
pushEvent("run.failed", { stepKey: step.key, reason }, step.key);
|
|
669
|
+
emitSnapshot();
|
|
670
|
+
return { type: "stop" };
|
|
671
|
+
}
|
|
672
|
+
// QA_REJECTED (any action), or SUCCESS with a non-PROCEED action: route the validator's
|
|
673
|
+
// verdict onto the validated step exactly like a self-reported QA rejection.
|
|
674
|
+
const qaAction = String(validatorOutput.qa_routing.action);
|
|
675
|
+
const feedbackReason = validatorOutput.qa_routing.feedback_reason || `agent validation rejected step ${step.key}`;
|
|
676
|
+
touchStep(step.key, {
|
|
677
|
+
lastExecution: {
|
|
678
|
+
executionStatus: "QA_REJECTED",
|
|
679
|
+
qaAction: isQaAction(qaAction) ? qaAction : "PROCEED",
|
|
680
|
+
feedbackReason,
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
const outcome = applyQaRejection(step, stepRun, index, qaAction, feedbackReason);
|
|
684
|
+
if ("stop" in outcome && outcome.stop) {
|
|
685
|
+
return { type: "stop" };
|
|
686
|
+
}
|
|
687
|
+
return { type: "reindex", index: outcome.index };
|
|
688
|
+
};
|
|
504
689
|
for (const step of definition.steps) {
|
|
505
690
|
stepRuns.set(step.key, {
|
|
506
691
|
stepKey: step.key,
|
|
@@ -711,78 +896,26 @@ export async function runWorkflow(definition, options) {
|
|
|
711
896
|
break;
|
|
712
897
|
}
|
|
713
898
|
if (output.execution_status === "QA_REJECTED") {
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
if (qaAction === "RETRY_CURRENT") {
|
|
717
|
-
if (stepRun.attempt < retryMax) {
|
|
718
|
-
stepRun.status = "pending";
|
|
719
|
-
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
720
|
-
pushEvent("step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
|
|
721
|
-
emitSnapshot();
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
stepRun.status = "failed";
|
|
725
|
-
runStatus = "failed";
|
|
726
|
-
currentStepKey = step.key;
|
|
727
|
-
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
728
|
-
touchRun(true);
|
|
729
|
-
pushEvent("run.failed", { stepKey: step.key, reason: "max retry exceeded" }, step.key);
|
|
730
|
-
emitSnapshot();
|
|
899
|
+
const outcome = applyQaRejection(step, stepRun, index, String(output.qa_routing.action), output.qa_routing.feedback_reason);
|
|
900
|
+
if ("stop" in outcome && outcome.stop) {
|
|
731
901
|
break;
|
|
732
902
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
const prevStep = orderedSteps[index - 1];
|
|
745
|
-
const prevRun = stepRuns.get(prevStep.key);
|
|
746
|
-
prevRun.status = "pending";
|
|
747
|
-
prevRun.attempt = 0;
|
|
748
|
-
prevRun.confirmed = false;
|
|
749
|
-
delete prevRun.output;
|
|
750
|
-
delete globalState[prevStep.key];
|
|
751
|
-
delete stepRun.output;
|
|
752
|
-
delete globalState[step.key];
|
|
753
|
-
touchStep(prevStep.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
754
|
-
pushEvent("step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
|
|
755
|
-
stepRun.status = "pending";
|
|
756
|
-
stepRun.confirmed = false;
|
|
757
|
-
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
758
|
-
emitSnapshot();
|
|
759
|
-
index -= 1;
|
|
760
|
-
continue;
|
|
903
|
+
index = outcome.index;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (step.kind === "task" &&
|
|
907
|
+
output.execution_status === "SUCCESS" &&
|
|
908
|
+
output.qa_routing.action === "PROCEED" &&
|
|
909
|
+
requiresValidation(step) === "agent") {
|
|
910
|
+
const validationOutcome = await runAgentValidation(step, stepRun, index, inputEnvelope, output);
|
|
911
|
+
if (validationOutcome.type === "stop") {
|
|
912
|
+
break;
|
|
761
913
|
}
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
const sr = stepRuns.get(s.key);
|
|
765
|
-
sr.status = "pending";
|
|
766
|
-
sr.attempt = 0;
|
|
767
|
-
sr.confirmed = false;
|
|
768
|
-
delete sr.output;
|
|
769
|
-
delete globalState[s.key];
|
|
770
|
-
touchStep(s.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
771
|
-
}
|
|
772
|
-
currentStepKey = null;
|
|
773
|
-
pushEvent("step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
|
|
774
|
-
emitSnapshot();
|
|
775
|
-
index = 0;
|
|
914
|
+
if (validationOutcome.type === "reindex") {
|
|
915
|
+
index = validationOutcome.index;
|
|
776
916
|
continue;
|
|
777
917
|
}
|
|
778
|
-
|
|
779
|
-
runStatus = "failed";
|
|
780
|
-
currentStepKey = step.key;
|
|
781
|
-
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
782
|
-
touchRun(true);
|
|
783
|
-
pushEvent("run.failed", { stepKey: step.key, reason: `Unknown QA action: ${qaAction}` }, step.key);
|
|
784
|
-
emitSnapshot();
|
|
785
|
-
break;
|
|
918
|
+
// "proceed": validator approved — fall through to the normal success path below.
|
|
786
919
|
}
|
|
787
920
|
stepRun.status = "succeeded";
|
|
788
921
|
stepRun.output = output.mutated_payload;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const BUNDLED_SKILLS = {
|
|
2
|
+
"commit-discipline": [
|
|
3
|
+
{ name: "SKILL.md", content: "---\nname: commit-discipline\ndescription: >\n Load this skill before staging commits or opening a pull request in\n workflow-manager. Enforces Conventional Commits, focused diffs, a clean\n rebase onto origin/main, and PR descriptions that link to the work.\ntype: core\napplies_to:\n - \".git/**\"\n - \"**/*\"\n---\n\n# Commit Discipline\n\nUse this skill at the moment you are about to `git add`, `git commit`, or `gh pr create`. It encodes how this repo expects commits and PRs to look so that history stays readable and release tooling (release-please, changelogs) keeps working.\n\n## When to use this skill\n\nLoad it when you are about to:\n\n- Stage changes for commit.\n- Author a commit message.\n- Rebase onto `origin/main` before opening a PR.\n- Open or update a pull request.\n\nSkip it for purely exploratory commands that do not touch git state.\n\n## Conventional Commits\n\nThis repo uses Conventional Commits. The release tooling (`release-please-config.json`) reads commit subjects to decide version bumps and changelog entries. Format:\n\n```\n<type>(<optional-scope>): <imperative summary>\n```\n\nAllowed `type` values:\n\n- `feat` — user-visible new functionality.\n- `fix` — user-visible bug fix.\n- `perf` — performance improvement with no behavior change.\n- `refactor` — internal restructuring, no behavior change, no new feature.\n- `docs` — documentation only.\n- `test` — tests only.\n- `build` — build system, packaging, dependencies.\n- `ci` — CI configuration only.\n- `chore` — repo plumbing that does not fit the above and has no user impact.\n- `revert` — reverts a previous commit; reference the reverted SHA.\n\nRules:\n\n- Subject in the imperative mood, no trailing period, max 72 chars.\n- Use a scope when the change is localized (e.g., `feat(cli): ...`, `fix(parser): ...`, `feat(remote-registry): ...`).\n- Use `!` for breaking changes (e.g., `feat(cli)!: rename --token to --auth-token`) AND include a `BREAKING CHANGE:` footer explaining the migration.\n- Body (optional) explains _why_, not _what_; reference issues/PRs by number.\n\n## Commit hygiene\n\n- One logical change per commit. Do not bundle unrelated edits.\n- Never commit secrets, `.env*` files, real tokens, generated `dist/` output (unless explicitly requested), `node_modules/`, or local Supabase data.\n- Never amend or force-push commits that already exist on `main`. Force-push to feature branches is allowed only with `--force-with-lease`.\n- If pre-commit / pre-push hooks fail, fix the underlying issue. Do not bypass with `--no-verify` unless the user explicitly asks.\n\n## Before opening a PR\n\nInside the feature worktree:\n\n```bash\ngit fetch origin\ngit rebase origin/main\nbun run lint\nbun test\nbun run build\n```\n\nPlus the conditional scripts from `repo-hygiene` / `doc-sync` when applicable.\n\nIf the rebase produces conflicts you cannot confidently resolve, stop and surface them to the user instead of force-pushing through them.\n\n## PR description template\n\nOpen PRs against `main` with a description that contains:\n\n1. **Summary** — 1–3 bullets describing the change in user-visible terms.\n2. **Why** — link to the issue, spec, or user request driving the change.\n3. **How** — short note on the approach; call out anything non-obvious.\n4. **Validation** — list the exact commands you ran and that they passed (lint, test, build, plus any conditional ones).\n5. **Docs** — link to the doc files updated, or state explicitly \"no user-visible behavior changed\".\n6. **Risk / rollout** — call out feature flags, migration steps, or follow-ups.\n\nExample title:\n\n```\nfeat(cli): add `wfm pull --output` flag for redirecting fetched workflows\n```\n\n## Anti-patterns to refuse\n\n- Subjects like `update stuff`, `wip`, `fix things`, `address review`.\n- Squashing unrelated commits together to \"clean up history\" right before merge.\n- Pushing to `main` directly.\n- Opening a PR with failing CI and asking reviewers to \"ignore the red\".\n- Mixing a refactor and a behavior change in the same commit.\n\n## Definition of done\n\n- Every commit subject is a valid Conventional Commit.\n- Branch is rebased on the latest `origin/main`.\n- All required scripts (lint, test, build, plus conditional ones) passed locally.\n- PR description follows the template and links updated docs.\n" },
|
|
4
|
+
],
|
|
5
|
+
"doc-sync": [
|
|
6
|
+
{ name: "SKILL.md", content: "---\nname: doc-sync\ndescription: >\n Load this skill whenever a change affects user-visible behavior in\n workflow-manager: CLI flags, command output, workflow schema, public\n TypeScript types, adapters, the remote registry surface, or environment\n variables. Ensures README, doc/, AGENTS.md, and in-CLI help stay in sync.\ntype: core\napplies_to:\n - \"src/index.ts\"\n - \"src/parser.ts\"\n - \"src/engine.ts\"\n - \"src/types.ts\"\n - \"src/remote/**\"\n - \"apps/remote-registry/**\"\n - \"doc/**\"\n - \"README.md\"\n - \"AGENTS.md\"\n---\n\n# Doc Sync\n\nUse this skill to keep documentation honest. In this repo, documentation is part of the product surface — the CLI is published, workflows are shared via the remote registry, and the docs site is built from `doc/`. Out-of-date docs are a bug.\n\n## When to use this skill\n\nLoad it whenever the change does any of the following:\n\n- Adds, removes, or renames a CLI command, flag, or argument in `src/index.ts`.\n- Changes workflow schema, defaults, or validation rules in `src/parser.ts` or `src/types.ts`.\n- Changes engine semantics (approvals, retries, rollback, restart, snapshot shape) in `src/engine.ts`.\n- Adds or changes an adapter (`mock`, `opencode`, `codex`, `claude-code`, future adapters).\n- Changes remote registry behavior in `src/remote/**` or `apps/remote-registry/**`.\n- Introduces or renames an environment variable or required local service.\n- Bumps a public type or contract referenced by docs or other packages.\n\nIf none of the above is true, this skill is not needed.\n\n## Required updates by change type\n\n| Change | Update |\n| --- | --- |\n| New / renamed CLI command or flag | Usage text in `src/index.ts`, `README.md` Quickstart and CLI section, the matching `doc/` page, and the man page source if behavior is reflected there. |\n| Workflow schema change | `README.md` schema section, the relevant page under `doc/guide/` or `doc/reference/`, plus the `workflow-manager-cli` skill checklist if authoring rules shift. |\n| Engine semantics change | The corresponding `doc/` engine page and any example workflows in `example-workflow.{md,json}` that no longer demonstrate the new behavior accurately. |\n| Adapter added or changed | List of supported adapters in `AGENTS.md`, `README.md`, and `doc/`. The `workflow-manager-cli` skill must also list it. |\n| Remote registry change | `apps/remote-registry/DESIGN.md` if UI/UX shifts, `README.md` remote section, `doc/` remote pages, and any onboarding snippets. |\n| New env var or required service | `README.md` setup section, `AGENTS.md` Install And Setup, and any `.env.example` files. Never commit real secrets. |\n\n## Workflow\n\n1. Diff your change and list every user-visible surface it touches.\n2. For each surface, open the matching doc file and update it in the same PR. Do not defer to a follow-up.\n3. Rebuild docs to catch broken links or examples:\n ```bash\n bun run docs:build\n ```\n4. If you changed the remote-registry app, also run:\n ```bash\n bun run remote-registry:build\n ```\n5. Re-read the README quickstart end-to-end and confirm the commands you ship still work as written.\n\n## Style rules\n\n- Match the existing tone of the surrounding doc. Do not introduce a new voice.\n- Prefer runnable examples over prose. Examples should match the actual current CLI output.\n- Keep command examples copy-pasteable. No invented flags or fictitious env vars.\n- When you remove or rename a flag, document the migration in the same section, not in a separate changelog-only entry.\n- Cross-link related docs sparingly and only when they meaningfully help the reader.\n\n## Anti-patterns to refuse\n\n- Updating code without updating docs \"because the docs are out of date anyway\".\n- Adding a doc page that is not linked from the docs nav or another page.\n- Copying README content into `doc/` instead of linking, creating two sources of truth.\n- Leaving `TODO: update docs` markers in a finished PR.\n\n## Definition of done\n\n- Every user-visible behavioral change is reflected in `README.md`, the relevant `doc/` pages, in-CLI `--help`, and `AGENTS.md` where applicable.\n- `bun run docs:build` passes.\n- Examples in updated docs were actually executed (or visually verified) against the new behavior.\n" },
|
|
7
|
+
],
|
|
8
|
+
"repo-hygiene": [
|
|
9
|
+
{ name: "SKILL.md", content: "---\nname: repo-hygiene\ndescription: >\n Load this skill on any code change in workflow-manager. It enforces the\n \"leave the repo cleaner than you found it\" rules: no dead code, no orphan\n files, no stray comments, no unrelated formatting churn, and validated\n scripts before declaring done.\ntype: core\napplies_to:\n - \"src/**\"\n - \"apps/**\"\n - \"tests/**\"\n - \"scripts/**\"\n---\n\n# Repo Hygiene\n\nUse this skill whenever you edit, add, or delete code in this repository. It encodes the cleanliness rules already implied by `AGENTS.md` and `biome.json`, made explicit so they are followed every time.\n\n## When to use this skill\n\nLoad it for any change under `src/`, `apps/`, `tests/`, or `scripts/`. Skip only for pure docs work in `doc/` and pure markdown edits at the repo root (those load `doc-sync` instead).\n\n## Core rules\n\n1. Smallest viable change. Edit only what the task requires. Do not refactor unrelated code, do not reformat unrelated files, do not rename unrelated symbols.\n2. No dead code. Remove unused imports, unused exports, unused locals, unreachable branches, and obsolete helpers in the files you touch. Do not leave `TODO`/`FIXME` without an associated issue link or a short, dated reason.\n3. No stray comments. Per `AGENTS.md` code style, do not add explanatory comments unless the user explicitly asks for them. Keep existing comments only if they are still accurate; delete misleading ones.\n4. File placement matters. Source goes in `src/`. Tests go in `tests/`. Remote-registry UI code stays inside `apps/remote-registry/`. Scripts stay in `scripts/`. Do not create new top-level files unless the task explicitly calls for them.\n5. Import discipline. ESM only. Node built-ins use the `node:` prefix. In `src/`, local imports use `.js` extensions; in `tests/`, local imports use `.ts` paths. Keep value imports first, then `import type` imports.\n6. Strict typing. Prefer `unknown` over `any` for untrusted input and narrow it before use. Reuse shared types from `src/types.ts` instead of inventing parallel shapes. Keep payloads JSON-serializable where possible.\n7. Error handling. CLI-facing code exits with clear messages and non-zero codes for failure. Validation errors are returned as strings for ordinary user-input problems, not thrown. Executor flows return structured `OutputEnvelope` failures rather than ad hoc exceptions.\n8. No silent swallowing. Never `catch` and ignore. If an error is intentionally non-fatal, log it with enough context to debug.\n9. No secrets. Never log tokens, env values, or credentials. Never commit `.env*` files or any local Supabase keys.\n\n## Pre-completion checklist\n\nBefore declaring a task done, run inside the worktree:\n\n```bash\nbun run lint\nbun test # narrowest relevant subset first; then full suite\nbun run build\n```\n\nPlus, when applicable:\n\n```bash\nbun run remote-registry:test && bun run remote-registry:build # apps/remote-registry/**\nbun run docs:build # doc/** or user-facing CLI/schema\n```\n\nIf any script fails, fix it or stop and report it. Do not declare completion with a red script.\n\n## Anti-patterns to refuse\n\n- Adding a new dependency without checking `package.json` first.\n- Introducing a new top-level config file when an existing one can be extended.\n- Reformatting whole files because the editor auto-formatted them.\n- Leaving commented-out code \"in case we need it later\" — git history is the archive.\n- Mixing an unrelated refactor into a feature PR.\n\n## Definition of clean\n\nA change is clean when:\n\n- The diff contains only lines required by the task.\n- `git status` shows no unintended new files.\n- `bun run lint`, `bun test`, and `bun run build` are all green.\n- A future reader can understand the change without an accompanying explanation message.\n" },
|
|
10
|
+
],
|
|
11
|
+
"spec-driven-development": [
|
|
12
|
+
{ name: "SKILL.md", content: "# Spec-Driven Development\n\nWrite a structured specification before writing any code. The spec is the shared source of truth between you and the engineer - it defines what we're building, why, and how we'll know it's done.\n\n## When to apply\n\nUse spec-driven development when starting projects, handling ambiguous requirements, making architectural decisions, or tackling changes requiring 30+ minutes of work. Skip it for single-line fixes or unambiguous, self-contained changes.\n\n## Six-section spec format\n\nEvery spec must cover these six sections in this order:\n\n1. **Objective** - One paragraph: what we are building, why, and the success criteria. Surface assumptions explicitly.\n2. **Commands** - The exact CLI / API surface. Flags, exit codes, examples.\n3. **Project Structure** - File layout with one-line responsibilities per file.\n4. **Code Style** - Language version, modules, error handling pattern, no-classes / no-comments rules, max function length.\n5. **Testing Strategy** - Unit vs integration, what gets mocked vs real, coverage gates, naming conventions.\n6. **Boundaries** - In scope, out of scope, and any non-negotiable constraints.\n\n## Practices\n\n- List assumptions before writing spec content. If they are wrong, the spec is wrong.\n- Reframe vague requirements as measurable success criteria.\n- Treat specs as living documents - update when decisions change.\n- Reference the spec from pull requests so the audit trail is visible.\n\n## Output rules\n\n- Plain markdown only. No preamble.\n- Every spec must contain all six sections, even if a section is \"N/A\".\n- Code style and testing strategy must include concrete rules, not vague aspirations.\n\n15 minutes of specification prevents hours of rework.\n" },
|
|
13
|
+
],
|
|
14
|
+
"workflow-author": [
|
|
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\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
|
+
],
|
|
18
|
+
"workflow-manager-cli": [
|
|
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" },
|
|
20
|
+
{ name: "SKILL.md", content: "---\nname: workflow-manager-cli\ndescription: >\n Load this skill when working with the wfm CLI from @workflow-manager/runner:\n authoring, validating, or running workflow definitions, configuring adapters\n and step skills, controlling runs through the attach API, or publishing\n workflows to the remote registry. Covers doctor, skill, scaffold, validate,\n run, approve, resume, cancel, auth, publish, pull, search, and remote info.\ntype: core\nlibrary: \"@workflow-manager/runner\"\nsources:\n - \"navio/workflow-manager:README.md\"\n - \"navio/workflow-manager:src/index.ts\"\n - \"navio/workflow-manager:src/parser.ts\"\n - \"navio/workflow-manager:src/engine.ts\"\n - \"navio/workflow-manager:src/runtimePreflight.ts\"\n - \"navio/workflow-manager:src/skillResolver.ts\"\n - \"navio/workflow-manager:src/remote/commands.ts\"\n - \"navio/workflow-manager:doc/guide/runner-api.md\"\n---\n\n# wfm CLI\n\n`wfm` (also installed as `workflow-manager`) parses a workflow definition file (Markdown frontmatter or JSON), validates it, and executes its steps through pluggable agent adapters with deterministic dependency ordering, approvals, retries, and rollback routing.\n\n## Core flow\n\nFollow this sequence unless the user asks for a narrower task:\n\n1. `wfm doctor` — inspect host adapter binaries and provider API keys.\n2. `wfm scaffold ./workflow.md` (or `--format json`) — generate a starter definition.\n3. Edit step keys, objectives, `dependsOn`, validation modes, and `taskSpec.init`.\n4. `wfm validate ./workflow.md` — always validate before running or publishing.\n5. `wfm doctor ./workflow.md` — preflight the specific workflow before real adapter runs.\n6. `wfm run ./workflow.md` — execute with live progress; add `--verbose` for agent output.\n7. `wfm publish ./workflow.md` — only after validation succeeds.\n\n## Command reference\n\n```bash\nwfm doctor [workflow] [--json] # host + per-workflow preflight checks\nwfm skill list # list skills bundled with the npm package\nwfm skill install [name ...] # install bundled skills for an agent (see below)\nwfm scaffold [path] [--format markdown|json]\nwfm validate <workflow>\nwfm run <workflow> [--input input.json] [--objective \"text\"] [--confirm stepA,stepB:human] [--auto-confirm-all] [--port 43121] [--verbose] [--json]\nwfm approve|resume|cancel [--url ...] [--token ...] [--run-id ...] [--step ...] [--actor ...] [--note ...]\nwfm auth <login|whoami|logout> [--token <token>]\nwfm publish <workflow> [--slug s] [--title t] [--description d] [--visibility public|private] [--version v] [--tag a,b] [--draft]\nwfm pull <owner/slug> [--version v] [--output path]\nwfm search [query]\nwfm remote info <owner/slug>\nwfm man\n```\n\nExit codes: `0` success, `1` validation or runtime error, `2` run finished in a non-success terminal status.\n\n## Host configuration\n\nAdapters (set per step via `taskSpec.adapterKey`; omit for the default):\n\n| Adapter | Kind | Notes |\n| --- | --- | --- |\n| `pi-agent` | real (default) | Drives the host `pi` coding agent CLI in print mode. `pi` must be on `PATH`; its auth lives in `~/.pi`, not env vars. |\n| `acp` | real (opt-in) | Connects to any Agent Client Protocol agent over JSON-RPC/stdio. Needs `payload.useRealAdapter: true` and an agent via `payload.acpCommand`/`acpArgs`, `payload.acpAgent` preset, or `WFM_ACP_COMMAND`. Permissions via `payload.acpPermissions` (`allow`/`deny`/`reads-only`). |\n| `claude-code` | real (opt-in) | ACP preset (`claude-code-acp`). Routes through `acp` when `useRealAdapter` is true. Legacy `claude` CLI path via `payload.legacyExecutor: true`. |\n| `opencode` | real (opt-in) | ACP preset (`opencode acp`). Routes through `acp` when `useRealAdapter` is true. Legacy `opencode` CLI path via `payload.legacyExecutor: true`. |\n| `codex` | mock / opt-in | Mock by default; routes through `acp` when `useRealAdapter` and an `acpCommand`/`acpAgent` are set. |\n| `mock` | mock | Deterministic simulation for tests and examples. |\n\nProvider API keys are inferred from `taskSpec.init.model` and checked by `wfm doctor` / run preflight:\n\n- `openrouter/...` → `OPENROUTER_API_KEY`\n- `openai/...`, `gpt-...` → `OPENAI_API_KEY`\n- `anthropic/...`, `claude-...` → `ANTHROPIC_API_KEY`\n\nSteps can also declare extra required env vars in `taskSpec.payload.requiredEnv`.\n\nFor custom `pi-agent` commands, the run directory exposes `input.json` / `output.json` envelopes through the `WFM_PI_INPUT_FILE` and `WFM_PI_OUTPUT_FILE` env vars.\n\n## Workflow anatomy\n\nBoth formats describe the same schema; Markdown holds it in YAML frontmatter (body text is free-form notes), JSON holds it directly. Required top-level fields: `key`, `title`, `steps`. Optional: `description`, `objectives`, `inputSchema`, `outputSchema`, `defaultRetryPolicy`, `skills`.\n\n```yaml\n---\nkey: my-workflow # stable external identifier — change cautiously\ntitle: My Workflow\nobjectives: [deliver a working implementation]\ndefaultRetryPolicy:\n maxAttempts: 2\nsteps:\n - key: discover # stable step key\n kind: task # task | approval\n objective: Understand requirements and constraints\n dependsOn: []\n validation: # mode: none | human | external\n mode: human\n required: true\n autoConfirm: false\n taskSpec:\n # adapterKey omitted -> pi-agent\n init:\n context: { repo: example/repo }\n skills: [architecture, planning] # resolved step skills, see below\n mcps: [mcp://github]\n systemPrompts: [Focus on architecture trade-offs]\n model: openrouter/anthropic/claude-sonnet-4\n payload: {}\n - key: qa_gate\n kind: approval # human checkpoint, no adapter\n dependsOn: [discover]\n approvalSpec:\n autoApprove: false\n validation: { mode: human, required: true, autoConfirm: false }\n---\n```\n\nAuthoring rules:\n\n- Workflow keys, step keys, and status strings are stable external identifiers; prefer additive, backward-compatible changes.\n- Make `dependsOn` explicit; the engine resolves dependencies deterministically and rejects cycles.\n- Keep validation modes explicit per step. `human` pauses for approval (inline terminal prompt or attach API); `external` expects an outside system to resume the run.\n- Use `adapterKey: mock` for deterministic examples and tests; use Markdown when humans will review notes, JSON for machine-generated definitions.\n\nSteps communicate via ATEP-like envelopes: an `InputEnvelope` (global/step context plus priming config) goes in, an `OutputEnvelope` comes out carrying execution status and a QA routing action — `PROCEED`, `RETRY_CURRENT`, `ROLLBACK_PREVIOUS`, or `RESTART_ALL` — which the engine uses to advance, retry, roll back, or restart the run.\n\n## Step skills\n\n`taskSpec.init.skills` names are resolved per step in this order:\n\n1. Embedded `skills.<name>.content` in the workflow file (how published workflows ship skills).\n2. `skills.<name>.source` — a relative path that must match `skills/**/SKILL.md` under the workflow directory.\n3. `<workflow-dir>/skills/<name>/SKILL.md` (project-local).\n4. `~/.workflow-manager/skills/<name>/SKILL.md` (user-global).\n5. The npm-packaged skills under `node_modules`.\n\n`wfm publish` inlines resolved skill markdown into `skills[*].content` with a `contentSha256` integrity hash; pulled workflows are rejected when declared skill content is missing or tampered.\n\n## Running and controlling runs\n\n`wfm run` shows live progress on stderr and starts a local attach API on `127.0.0.1` for the lifetime of the run (OS-assigned port, or `--port`). The base URL and a per-run bearer token are printed to stderr before execution starts; every endpoint except `/health` requires `Authorization: Bearer <token>`. The API serves the session (`/session`), run snapshots, per-step detail, logs, an SSE event stream, and approve/resume/cancel endpoints (contract: `doc/guide/runner-api.md`).\n\n- Interactive human approvals show an inline terminal prompt; non-interactive waits are resolved via `wfm approve` / `wfm resume` / `wfm cancel` using the printed URL and token.\n- `--confirm stepA,stepB:human` pre-supplies confirmations for specific steps.\n- Never use `--auto-confirm-all` unless the workflow is intentionally non-interactive — it bypasses every approval gate.\n- `--json` prints the final result (including a `session` object) on stdout while progress stays on stderr.\n- `--input input.json` merges a JSON file into global input state; `--objective` overrides the run objective.\n\n## Remote registry\n\n```bash\nwfm auth login --token <token> # token created in the registry web app\nwfm auth whoami\nwfm search <query>\nwfm remote info <owner/slug>\nwfm publish ./workflow.md --visibility public --tag automation\nwfm pull <owner/slug> --output ./pulled-workflow.json\n```\n\nIf `publish` fails, check login state with `wfm auth whoami`. Preserve the author's source format when publishing.\n\n## Installing these skills for agents\n\n`wfm skill install` copies bundled skills into an agent's skill directory so the agent loads this guidance on demand:\n\n```bash\nwfm skill list # see what ships with the package\nwfm skill install # workflow-manager-cli -> ./.claude/skills/\nwfm skill install --global # -> ~/.claude/skills/\nwfm skill install --agent opencode # -> ./.opencode/skill/\nwfm skill install --all --force # every bundled skill, overwrite existing\nwfm skill install doc-sync --dir ./my/skills # any other destination\n```\n\n## Troubleshooting\n\n- Validation failure: fix the reported schema or dependency-cycle error before re-running; ordinary input errors are reported as messages, not stack traces.\n- `doctor` failure for a real adapter: install the missing CLI (`pi`, `opencode`, `claude`) or export the missing provider API key.\n- Run stuck `waiting_for_approval`: approve inline in the terminal, or use `wfm approve` with the attach URL/token printed at run start.\n- Non-zero exit `2`: the run completed but not successfully — re-run with `--json` or `--verbose` to inspect step output.\n- Publish/pull failures: verify `wfm auth whoami`; for skill integrity errors, confirm the declared skill files exist and match their `contentSha256`.\n" },
|
|
21
|
+
],
|
|
22
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import type { BundledSkillFile } from "./generated/bundledSkills.js";
|
|
3
|
+
type PackagedSkillSource = {
|
|
4
|
+
kind: "disk";
|
|
5
|
+
dir: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "bundled";
|
|
8
|
+
files: BundledSkillFile[];
|
|
9
|
+
};
|
|
10
|
+
export interface PackagedSkill {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
source: PackagedSkillSource;
|
|
14
|
+
}
|
|
15
|
+
export declare function packagedSkillsDir(): string;
|
|
16
|
+
export declare function parseSkillDescription(skillMarkdown: string): string;
|
|
17
|
+
export declare function listPackagedSkillsFromDisk(root: string): PackagedSkill[];
|
|
18
|
+
export declare function listPackagedSkillsFromBundle(bundle?: Record<string, BundledSkillFile[]>): PackagedSkill[];
|
|
19
|
+
export declare function listPackagedSkills(root?: string): PackagedSkill[];
|
|
20
|
+
export declare function materializeSkillFiles(skill: PackagedSkill, destDir: string): void;
|
|
2
21
|
export {};
|