@automatalabs/workflows 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -263,6 +263,47 @@ testable without a live agent — pass a stub runner.
263
263
 
264
264
  ---
265
265
 
266
+ ## Loading workflows from folders — `openWorkflowDir`
267
+
268
+ Integrators who keep a versioned folder of workflow scripts don't need to hand-roll
269
+ `readFileSync` plumbing or a `loadSavedWorkflow` resolver:
270
+
271
+ ```ts
272
+ import { openWorkflowDir, runDynamicWorkflow } from "@automatalabs/workflows";
273
+
274
+ const flows = openWorkflowDir("./workflows"); // or ["./workflows", teamDir] — first hit wins
275
+
276
+ flows.list(); // [{ name, file, meta }] — meta parsed per call, browsable by a UI
277
+ flows.read("review-pr"); // name → script string; throws with searched dirs + did-you-mean
278
+ flows.resolve; // (name) => string | undefined — IS a loadSavedWorkflow resolver
279
+
280
+ const run = await runDynamicWorkflow("review-pr", { // a NAME works when `workflows` is set
281
+ workflows: flows, // also accepts "./workflows" or [dir, dir]
282
+ args: { pr: 42 },
283
+ });
284
+ ```
285
+
286
+ Semantics worth knowing:
287
+
288
+ - **Construction does no I/O** — nothing is created, nothing is scanned or cached. Every method
289
+ reads the filesystem at call time, so a long-lived view always reflects the current working
290
+ tree (a cached scan would serve stale scripts after a `git checkout`/pull/save). Missing
291
+ directories simply contribute nothing.
292
+ - **The filename stem is the name** (`review-pr.workflow.js` or `review-pr.js` ⇒ `review-pr`),
293
+ mirroring the agentType registry convention; across dirs the first hit wins, within a dir
294
+ `.workflow.js` beats `.js`.
295
+ - **`workflows` also wires nested calls**: with the option set, `workflow("<name>")` inside the
296
+ script resolves from the same view. (Without it, `runDynamicWorkflow` has no saved-workflow
297
+ resolver and nested names cannot resolve.) A top-level string containing `export const meta`
298
+ is always treated as a verbatim script, never a name.
299
+ - **Versioning is git's job.** A run persists its script content, so `resume()` replays the exact
300
+ script that started the run even if the file changed since; an edited file simply cache-misses
301
+ from the first changed call on the next fresh run.
302
+ - **`resolve()` validates name shape strictly** (one flat path segment) — inline nested scripts
303
+ fall through to verbatim parsing, and path traversal out of the configured dirs is impossible.
304
+
305
+ ---
306
+
266
307
  ## Listening in on the live ACP stream (events)
267
308
 
268
309
  `createAcpRunner()` returns an `AcpAgentRunner` with a **typed event bus**. Subscribe with
@@ -353,6 +394,42 @@ promises — `parallel([() => agent("a"), () => agent("b")])`, not `parallel([ag
353
394
 
354
395
  ---
355
396
 
397
+ ## Validating scripts — `agentprism-workflows validate`
398
+
399
+ The package ships a bin that validates a workflow script **without spending tokens or spawning
400
+ any agent process** — no backend auth needed:
401
+
402
+ ```bash
403
+ npx @automatalabs/workflows validate my-workflow.js --args '{"target":"src/"}'
404
+ ```
405
+
406
+ Two passes: a **static parse** (the `meta` literal, syntax, the determinism blocklist), then a
407
+ **dry run** — the script executes in the real engine realm while every `agent()` call is served
408
+ by an in-process mock `AgentRunner` that fabricates schema-conforming results. The dry run
409
+ catches what a parse can't: thunk-vs-promise mistakes, reference errors, broken plumbing between
410
+ calls. Checkpoints take their headless defaults; script-declared `meta.backends` are treated as
411
+ approved (with a warning that real runs require approval). The report lists every agent call with
412
+ its backend attribution, every checkpoint, and warnings; exit codes are `0` valid, `1` parse
413
+ failure, `2` dry-run failure, `3` usage error.
414
+
415
+ Flags: `--args <json>` / `--args-file <path>`, `--workflows-dir <dir>` (repeatable — validate by
416
+ NAME and resolve nested `workflow("<name>")` calls from your folder), `--parse-only`,
417
+ `--cwd <dir>`, `--token-budget <n>` (exercise `budget`-guarded paths; the mock reports 1000
418
+ tokens per call), `--max-agents <n>`, `--timeout-ms <n>`, `--json`.
419
+
420
+ The same check is available programmatically — it never throws for an invalid script:
421
+
422
+ ```ts
423
+ import { validateWorkflowScript } from "@automatalabs/workflows";
424
+
425
+ const report = await validateWorkflowScript(script, { args: { target: "src/" } });
426
+ report.ok; // parse ok AND dry run completed
427
+ report.dryRun?.agentCalls; // [{ label, phase, model, backend, schema }, …]
428
+ report.warnings; // approval reminders, phase mismatches, headless-abort checkpoints, …
429
+ ```
430
+
431
+ ---
432
+
356
433
  ## Structured output
357
434
 
358
435
  Pass a JSON Schema to `agent({ schema })` (in a script) or `runner.run(prompt, { schema })` (direct)
@@ -422,6 +499,10 @@ Within a provider, the model spec selects the concrete model through ACP session
422
499
  runDynamicWorkflow, // (script, { args?, runner?, exec? }) => Promise<WorkflowRunResult>
423
500
  runWorkflow, // the bare engine run (no status trio)
424
501
  parseWorkflowScript, // parse a script's meta + body
502
+ validateWorkflowScript, // token-free parse + mock-runner dry run (the `validate` CLI's core)
503
+ fabricateFromSchema, // the dry run's JSON-Schema value fabricator
504
+ formatValidateReport, // render a ValidateWorkflowReport as CLI text
505
+ openWorkflowDir, // read-only view over folders of workflow scripts (name = filename stem)
425
506
  WorkflowManager, // stateful / resumable run manager
426
507
 
427
508
  // ── ACP backend ──
@@ -442,6 +523,7 @@ AGENTPRISM_PERSISTENCE_ROOT_ENV,
442
523
 
443
524
  // ── Types ──
444
525
  RunDynamicWorkflowOptions, WorkflowRunOptions, AgentOptions, ExecOptions,
526
+ ValidateWorkflowOptions, ValidateWorkflowReport, ValidatedAgentCall, ValidatedCheckpoint,
445
527
  WorkflowManagerOptions, CheckpointOptions, WorkflowRunResult, WorkflowSnapshot,
446
528
  WorkflowPathOptions, RunPersistence, RunPersistenceOptions,
447
529
  AcpPoolOptions, AgentRunner, RunOptions, AgentResult, AgentUsage, JournalEntry,
@@ -459,6 +541,25 @@ globals documented by the ambient `dsl.d.ts`.)
459
541
 
460
542
  ---
461
543
 
544
+ ## Skill for AI agents that write workflows
545
+
546
+ The repository publishes an **agent skill** — a self-contained, backend-agnostic authoring guide
547
+ in the standard `SKILL.md` format — at
548
+ [`skills/agentprism-workflow-authoring/`](https://github.com/VikashLoomba/agentprism-workflows/tree/main/skills/agentprism-workflow-authoring).
549
+ Install it into whatever coding agent you use (Claude Code, Codex, Cursor, OpenCode, …) with the
550
+ [skills](https://skills.sh) CLI:
551
+
552
+ ```bash
553
+ npx skills add VikashLoomba/agentprism-workflows
554
+ ```
555
+
556
+ It teaches the full script DSL: routing each `agent()` call to a different ACP backend inside one
557
+ script, structured outputs across all backends, `checkpoint()` gates, budgets, worktree
558
+ isolation, and the determinism rules that make runs resumable. `reference.md` alongside it holds
559
+ the exhaustive option tables.
560
+
561
+ ---
562
+
462
563
  ## See also
463
564
 
464
565
  - **[`@automatalabs/mcp-server`](https://www.npmjs.com/package/@automatalabs/mcp-server)** — the
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // The @automatalabs/workflows bin (`agentprism-workflows`). One subcommand:
3
+ //
4
+ // agentprism-workflows validate <workflow-file> [options]
5
+ //
6
+ // Validates a workflow script without spending tokens: static parse (meta literal,
7
+ // syntax, determinism blocklist), then a dry run over an in-process mock AgentRunner
8
+ // that fabricates schema-conforming results — no ACP process is spawned. See
9
+ // ./validate.ts for the programmatic API (`validateWorkflowScript`).
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { openWorkflowDir } from "@automatalabs/workflow-engine";
13
+ import { validateWorkflowScript, formatValidateReport } from "./validate.js";
14
+ const USAGE = `Usage: agentprism-workflows validate <workflow-file-or-name> [options]
15
+
16
+ Validates an AgentPrism workflow script without spending tokens:
17
+ 1. static parse — the meta literal, syntax, and the determinism blocklist
18
+ 2. dry run — the script executes against a mock agent backend that fabricates
19
+ schema-conforming results; no ACP process is spawned, no tokens are spent,
20
+ and checkpoints resolve to their headless defaults
21
+
22
+ Options:
23
+ --args <json> the script's \`args\` global for the dry run (a JSON value)
24
+ --args-file <path> read the args JSON from a file instead
25
+ --workflows-dir <dir> a directory of workflow scripts (repeatable; precedence in
26
+ the order given). Enables validating by NAME (filename stem)
27
+ and resolves nested workflow("<name>") calls in the dry run
28
+ --parse-only static parse only; skip the dry run
29
+ --cwd <dir> base cwd for the dry run (default: a throwaway temp dir;
30
+ point it at a real repo only if you want worktree isolation
31
+ to create — and clean up — real git worktrees)
32
+ --token-budget <n> set budget.total so budget-guarded paths execute
33
+ (the mock backend reports 1000 tokens per agent call)
34
+ --max-agents <n> cap dry-run agent calls
35
+ --timeout-ms <n> dry-run wall-clock limit (default 30000)
36
+ --json print the machine-readable report to stdout
37
+ -h, --help show this help
38
+
39
+ Notes:
40
+ - without --workflows-dir, nested workflow("<saved-name>") calls fail in the dry
41
+ run (no saved-workflow resolver); nested INLINE script strings always validate
42
+ - script-declared meta.backends are treated as approved for the dry run, but the
43
+ report reminds you that real runs require explicit approval
44
+
45
+ Exit codes: 0 valid · 1 parse/static failure · 2 dry-run failure · 3 usage error`;
46
+ function fail(message) {
47
+ process.stderr.write(`${message}\n\nRun \`agentprism-workflows validate --help\` for usage.\n`);
48
+ process.exit(3);
49
+ }
50
+ function parseIntFlag(name, raw) {
51
+ const value = Number(raw);
52
+ if (raw === undefined || !Number.isFinite(value) || value <= 0)
53
+ fail(`${name} expects a positive number`);
54
+ return Math.floor(value);
55
+ }
56
+ async function main(argv) {
57
+ const [command, ...rest] = argv;
58
+ if (command === undefined || command === "-h" || command === "--help") {
59
+ process.stdout.write(`${USAGE}\n`);
60
+ process.exit(command === undefined ? 3 : 0);
61
+ }
62
+ if (command !== "validate")
63
+ fail(`unknown command "${command}" — the only command is: validate`);
64
+ let file;
65
+ let json = false;
66
+ const workflowDirs = [];
67
+ const options = {};
68
+ for (let i = 0; i < rest.length; i++) {
69
+ const arg = rest[i];
70
+ switch (arg) {
71
+ case "-h":
72
+ case "--help":
73
+ process.stdout.write(`${USAGE}\n`);
74
+ process.exit(0);
75
+ break;
76
+ case "--json":
77
+ json = true;
78
+ break;
79
+ case "--parse-only":
80
+ options.dryRun = false;
81
+ break;
82
+ case "--args":
83
+ try {
84
+ options.args = JSON.parse(rest[++i] ?? "");
85
+ }
86
+ catch {
87
+ fail("--args expects a JSON value (quote it for your shell)");
88
+ }
89
+ break;
90
+ case "--args-file":
91
+ try {
92
+ options.args = JSON.parse(readFileSync(resolve(rest[++i] ?? ""), "utf8"));
93
+ }
94
+ catch (error) {
95
+ fail(`--args-file: ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ break;
98
+ case "--workflows-dir":
99
+ workflowDirs.push(resolve(rest[++i] ?? fail("--workflows-dir expects a directory")));
100
+ break;
101
+ case "--cwd":
102
+ options.cwd = resolve(rest[++i] ?? fail("--cwd expects a directory"));
103
+ break;
104
+ case "--token-budget":
105
+ options.tokenBudget = parseIntFlag("--token-budget", rest[++i]);
106
+ break;
107
+ case "--max-agents":
108
+ options.maxAgents = parseIntFlag("--max-agents", rest[++i]);
109
+ break;
110
+ case "--timeout-ms":
111
+ options.timeoutMs = parseIntFlag("--timeout-ms", rest[++i]);
112
+ break;
113
+ default:
114
+ if (arg.startsWith("-"))
115
+ fail(`unknown option "${arg}"`);
116
+ if (file !== undefined)
117
+ fail("exactly one workflow file expected");
118
+ file = arg;
119
+ }
120
+ }
121
+ if (file === undefined)
122
+ fail("missing <workflow-file-or-name>");
123
+ const flows = workflowDirs.length > 0 ? openWorkflowDir(workflowDirs) : undefined;
124
+ if (flows)
125
+ options.workflows = flows;
126
+ // The positional is a file path first; with --workflows-dir it may also be a NAME.
127
+ let script;
128
+ if (existsSync(resolve(file))) {
129
+ try {
130
+ script = readFileSync(resolve(file), "utf8");
131
+ }
132
+ catch (error) {
133
+ fail(`cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ }
136
+ else if (flows) {
137
+ try {
138
+ script = flows.read(file); // throws with searched dirs + closest matches
139
+ }
140
+ catch (error) {
141
+ fail(error instanceof Error ? error.message : String(error));
142
+ }
143
+ }
144
+ else {
145
+ fail(`cannot read ${file}: no such file (pass --workflows-dir to validate by name)`);
146
+ }
147
+ const report = await validateWorkflowScript(script, options);
148
+ process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : `${formatValidateReport(report)}\n`);
149
+ process.exit(report.exitCode);
150
+ }
151
+ main(process.argv.slice(2)).catch((error) => {
152
+ process.stderr.write(`validate crashed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
153
+ process.exit(3);
154
+ });
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { WorkflowManager as EngineWorkflowManager } from "@automatalabs/workflow-engine";
2
2
  import type { AcpEventName, AcpRunnerEventMap } from "@automatalabs/acp-agents";
3
- import type { ExecOptions, WorkflowManagerOptions } from "@automatalabs/workflow-engine";
3
+ import type { ExecOptions, WorkflowDir, WorkflowManagerOptions } from "@automatalabs/workflow-engine";
4
4
  import type { AgentRunner, WorkflowBackendConfig, WorkflowRunResult } from "@automatalabs/shared-types";
5
5
  export { runWorkflow, parseWorkflowScript } from "@automatalabs/workflow-engine";
6
+ export { openWorkflowDir, type WorkflowDir, type WorkflowDirEntry, type OpenWorkflowDirOptions, } from "@automatalabs/workflow-engine";
7
+ export { validateWorkflowScript, fabricateFromSchema, formatValidateReport, MOCK_TOKENS_PER_AGENT } from "./validate.js";
8
+ export type { ValidateWorkflowOptions, ValidateWorkflowReport, ValidatedAgentCall, ValidatedCheckpoint, } from "./validate.js";
6
9
  export type { WorkflowRunOptions, AgentOptions, ExecOptions, WorkflowManagerOptions, CheckpointOptions, WorkflowRunResult, WorkflowSnapshot, WorkflowPathOptions, RunPersistence, RunPersistenceOptions, } from "@automatalabs/workflow-engine";
7
10
  export { AGENTPRISM_PERSISTENCE_ROOT_ENV, WorkflowError, WorkflowErrorCode, isWorkflowError, isProviderUsageLimit, } from "@automatalabs/workflow-engine";
8
11
  export { createAcpRunner, AcpAgentRunner, InteractiveSession, selectBackend, ClaudeBackend, CodexBackend, CustomAcpBackend, AGENT_METHODS, CLIENT_METHODS, AGENT_METHOD_COVERAGE, CLIENT_METHOD_COVERAGE, ACP_AUTH_REQUIRED_ERROR_CODE, clientCapabilitiesFor, adaptPromptContent, resolveBackendRegistry, BACKENDS_ENV, toJsonSchema, toStrictJsonSchema, } from "@automatalabs/acp-agents";
@@ -89,6 +92,14 @@ export interface RunDynamicWorkflowOptions {
89
92
  exec?: ExecOptions;
90
93
  /** Approval policy for script-declared `meta.backends` (see {@link ScriptBackendApproval}). */
91
94
  allowScriptBackends?: ScriptBackendApproval;
95
+ /**
96
+ * A workflow directory view (or dir path(s) to open one over) serving saved workflows
97
+ * by name. When set, the first argument may be a workflow NAME instead of a script
98
+ * (resolver first, verbatim-script fallback — the engine's own nested-workflow rule),
99
+ * and nested `workflow("<name>")` calls resolve from the same view (it is wired into
100
+ * the run's `loadSavedWorkflow`).
101
+ */
102
+ workflows?: string | string[] | WorkflowDir;
92
103
  }
93
104
  /**
94
105
  * Run a dynamic workflow script to a TERMINAL result, with the AgentRunner seam
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -10,10 +10,19 @@
10
10
  // vm-realm globals, NOT importable symbols; they are documented for author IntelliSense
11
11
  // in ./dsl.d.ts (referenced above), not exported here.
12
12
  import { ACP_CROSS_CUTTING_EVENT_NAMES, createAcpRunner } from "@automatalabs/acp-agents";
13
- import { parseWorkflowScript, WorkflowError, WorkflowErrorCode, WorkflowManager as EngineWorkflowManager, } from "@automatalabs/workflow-engine";
13
+ import { openWorkflowDir, parseWorkflowScript, WorkflowError, WorkflowErrorCode, WorkflowManager as EngineWorkflowManager, } from "@automatalabs/workflow-engine";
14
14
  // ── Engine: run entry, script parsing, the managed-run lifecycle, and the
15
15
  // option/result + error types the host composes against. ──
16
16
  export { runWorkflow, parseWorkflowScript } from "@automatalabs/workflow-engine";
17
+ // ── Workflow directory view: openWorkflowDir("./workflows") binds a read-only,
18
+ // per-call-fresh view over folders of versioned workflow scripts (name = filename
19
+ // stem). `view.resolve` IS a loadSavedWorkflow resolver; runDynamicWorkflow accepts
20
+ // the view (or dir paths) via `workflows` to serve top-level names AND nested
21
+ // workflow("<name>") calls. ──
22
+ export { openWorkflowDir, } from "@automatalabs/workflow-engine";
23
+ // ── Token-free script validation: static parse + mock-runner dry run. Also the core of
24
+ // the `agentprism-workflows validate` CLI (./cli.ts). ──
25
+ export { validateWorkflowScript, fabricateFromSchema, formatValidateReport, MOCK_TOKENS_PER_AGENT } from "./validate.js";
17
26
  export { AGENTPRISM_PERSISTENCE_ROOT_ENV, WorkflowError, WorkflowErrorCode, isWorkflowError, isProviderUsageLimit, } from "@automatalabs/workflow-engine";
18
27
  // ── ACP backend: the default AgentRunner implementation, interactive sessions, backend
19
28
  // selection, the concrete backends (built-in + custom registry), the pool/runner options,
@@ -158,12 +167,22 @@ function toAgentEventPayload(name, event) {
158
167
  * for an ordinary pause/fail — so the caller can read `result.status` directly.
159
168
  */
160
169
  export async function runDynamicWorkflow(script, opts = {}) {
170
+ // Saved-workflow view: `script` may be a workflow NAME when `workflows` is set. A real
171
+ // script always contains the mandatory `export const meta` head, so anything without it
172
+ // is treated as a name and resolved via read() — which throws a diagnosable error
173
+ // (searched dirs + closest matches) instead of the engine's parse error on a bare name.
174
+ const flows = opts.workflows === undefined
175
+ ? undefined
176
+ : typeof opts.workflows === "string" || Array.isArray(opts.workflows)
177
+ ? openWorkflowDir(opts.workflows, { cwd: opts.cwd })
178
+ : opts.workflows;
179
+ const resolvedScript = flows !== undefined && !script.includes("export const meta") ? flows.read(script) : script;
161
180
  // Script-declared backends need explicit approval BEFORE the run. A malformed script is
162
181
  // deliberately not diagnosed here — runSync re-parses and throws the engine's own parse
163
182
  // error (its pre-existing contract), so the approval gate never masks a parse message.
164
183
  let declared;
165
184
  try {
166
- declared = parseWorkflowScript(script).meta.backends;
185
+ declared = parseWorkflowScript(resolvedScript).meta.backends;
167
186
  }
168
187
  catch {
169
188
  declared = undefined;
@@ -174,9 +193,9 @@ export async function runDynamicWorkflow(script, opts = {}) {
174
193
  }
175
194
  const owned = opts.runner === undefined;
176
195
  const runner = opts.runner ?? createAcpRunner();
177
- const manager = new WorkflowManager({ agent: runner, cwd: opts.cwd });
196
+ const manager = new WorkflowManager({ agent: runner, cwd: opts.cwd, loadSavedWorkflow: flows?.resolve });
178
197
  try {
179
- return await manager.runSync(script, opts.args, exec);
198
+ return await manager.runSync(resolvedScript, opts.args, exec);
180
199
  }
181
200
  finally {
182
201
  manager.dispose();
@@ -0,0 +1,84 @@
1
+ import type { WorkflowDir } from "@automatalabs/workflow-engine";
2
+ import type { WorkflowMeta } from "@automatalabs/shared-types";
3
+ export interface ValidateWorkflowOptions {
4
+ /** The `args` global handed to the script during the dry run. */
5
+ args?: unknown;
6
+ /** A workflow directory view (or dir path(s)) serving saved workflows by name, so
7
+ * nested `workflow("<name>")` calls resolve during the dry run instead of failing. */
8
+ workflows?: string | string[] | WorkflowDir;
9
+ /** Base cwd for the dry run. Default: a throwaway temp dir (so `isolation: "worktree"`
10
+ * degrades to a no-op instead of creating real worktrees in a repo). */
11
+ cwd?: string;
12
+ /** false => static parse only, no dry run. Default true. */
13
+ dryRun?: boolean;
14
+ /** Set budget.total for the dry run so budget-guarded paths execute. The mock runner
15
+ * reports 1000 tokens per agent call. */
16
+ tokenBudget?: number;
17
+ /** Cap on dry-run agent calls (defaults to the engine's own cap). */
18
+ maxAgents?: number;
19
+ /** Dry-run wall-clock limit. Default 30_000 ms. */
20
+ timeoutMs?: number;
21
+ }
22
+ /** One agent() call observed during the dry run, with its backend attribution. */
23
+ export interface ValidatedAgentCall {
24
+ label: string;
25
+ phase?: string;
26
+ /** The model spec the call requested (undefined = the run/session default). */
27
+ model?: string;
28
+ tier?: string;
29
+ mode?: string;
30
+ /** Which backend the spec routes to: "claude" | "codex" | "opencode" | a custom backend
31
+ * name (suffixed " (script-declared)" when it comes from meta.backends) | "default". */
32
+ backend: string;
33
+ /** True when the call requested structured output. */
34
+ schema: boolean;
35
+ }
36
+ export interface ValidatedCheckpoint {
37
+ prompt: string;
38
+ kind: string;
39
+ /** The reply the dry run took (the checkpoint's headless default). */
40
+ reply: unknown;
41
+ }
42
+ export interface ValidateWorkflowReport {
43
+ /** True when the parse succeeded AND the dry run (if performed) completed. */
44
+ ok: boolean;
45
+ /** 0 = valid; 1 = parse/static failure; 2 = dry-run failure. */
46
+ exitCode: 0 | 1 | 2;
47
+ parse: {
48
+ ok: boolean;
49
+ error?: string;
50
+ meta?: WorkflowMeta;
51
+ };
52
+ dryRun?: {
53
+ ok: boolean;
54
+ status: string;
55
+ reason?: string;
56
+ /** True when the run was cut off by ValidateWorkflowOptions.timeoutMs. */
57
+ timedOut: boolean;
58
+ agentCalls: ValidatedAgentCall[];
59
+ checkpoints: ValidatedCheckpoint[];
60
+ phasesVisited: string[];
61
+ logs: string[];
62
+ durationMs: number;
63
+ /** The script's return value, composed from fabricated agent results. */
64
+ result?: unknown;
65
+ };
66
+ warnings: string[];
67
+ }
68
+ /**
69
+ * Fabricate a value that structurally satisfies a JSON Schema — the dry run's stand-in
70
+ * for a real agent's structured output. Deterministic and intentionally simple: first
71
+ * enum/anyOf variant, `true` booleans (so ok-gates terminate), `mock-<field>` strings.
72
+ */
73
+ export declare function fabricateFromSchema(schema: unknown, hint?: string, depth?: number): unknown;
74
+ /** Tokens the mock runner reports per agent call, so `--token-budget` exercises
75
+ * budget-guarded script paths deterministically. */
76
+ export declare const MOCK_TOKENS_PER_AGENT = 1000;
77
+ /**
78
+ * Validate a workflow script: parse it, then (by default) dry-run it against a mock
79
+ * AgentRunner. Never throws for an invalid script — read `report.ok` / `report.exitCode`.
80
+ */
81
+ export declare function validateWorkflowScript(script: string, options?: ValidateWorkflowOptions): Promise<ValidateWorkflowReport>;
82
+ /** Render a ValidateWorkflowReport as the human-readable CLI output. */
83
+ export declare function formatValidateReport(report: ValidateWorkflowReport): string;
84
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAA2B,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAExF,MAAM,WAAW,uBAAuB;IACtC,iEAAiE;IACjE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;2FACuF;IACvF,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,WAAW,CAAC;IAC5C;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;8CAC0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;6FACyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,EAAE,EAAE,OAAO,CAAC;IACZ,gEAAgE;IAChE,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,KAAK,EAAE;QACL,EAAE,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,YAAY,CAAC;KACrB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,OAAO,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,0EAA0E;QAC1E,QAAQ,EAAE,OAAO,CAAC;QAClB,UAAU,EAAE,kBAAkB,EAAE,CAAC;QACjC,WAAW,EAAE,mBAAmB,EAAE,CAAC;QACnC,aAAa,EAAE,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,yEAAyE;QACzE,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,SAAU,EAAE,KAAK,SAAI,GAAG,OAAO,CA2DvF;AAcD;qDACqD;AACrD,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAqB1C;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,sBAAsB,CAAC,CAwLjC;AAMD,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAgC3E"}
@@ -0,0 +1,326 @@
1
+ // Token-free validation for workflow scripts: a static parse (meta literal, syntax,
2
+ // determinism blocklist) followed by an optional DRY RUN — the script executes for real
3
+ // in the engine's deterministic realm, but every agent() call is served by an in-process
4
+ // mock AgentRunner that fabricates schema-conforming results. No ACP process is spawned,
5
+ // no tokens are spent, checkpoints resolve to their headless defaults, and run state is
6
+ // journaled nowhere (journaling off + a throwaway persistence root for the run lease).
7
+ //
8
+ // This is the programmatic core behind `agentprism-workflows validate` (see ./cli.ts).
9
+ import { mkdtempSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { openWorkflowDir, WorkflowManager, parseWorkflowScript } from "@automatalabs/workflow-engine";
13
+ import { resolveBackendRegistry, selectBackend } from "@automatalabs/acp-agents";
14
+ /**
15
+ * Fabricate a value that structurally satisfies a JSON Schema — the dry run's stand-in
16
+ * for a real agent's structured output. Deterministic and intentionally simple: first
17
+ * enum/anyOf variant, `true` booleans (so ok-gates terminate), `mock-<field>` strings.
18
+ */
19
+ export function fabricateFromSchema(schema, hint = "value", depth = 0) {
20
+ if (depth > 16)
21
+ return null;
22
+ if (!schema || typeof schema !== "object")
23
+ return `mock-${hint}`;
24
+ const s = schema;
25
+ if (s.const !== undefined)
26
+ return s.const;
27
+ if (Array.isArray(s.enum) && s.enum.length > 0)
28
+ return s.enum[0];
29
+ if (s.default !== undefined)
30
+ return s.default;
31
+ const variants = s.anyOf ?? s.oneOf ?? s.allOf;
32
+ if (Array.isArray(variants) && variants.length > 0)
33
+ return fabricateFromSchema(variants[0], hint, depth + 1);
34
+ let type = s.type;
35
+ if (Array.isArray(type))
36
+ type = type[0];
37
+ if (type === undefined) {
38
+ if (s.properties)
39
+ type = "object";
40
+ else if (s.items)
41
+ type = "array";
42
+ else
43
+ return `mock-${hint}`;
44
+ }
45
+ switch (type) {
46
+ case "object": {
47
+ const out = {};
48
+ const props = (s.properties ?? {});
49
+ for (const [name, sub] of Object.entries(props))
50
+ out[name] = fabricateFromSchema(sub, name, depth + 1);
51
+ for (const name of Array.isArray(s.required) ? s.required : []) {
52
+ if (!(name in out))
53
+ out[name] = `mock-${name}`;
54
+ }
55
+ return out;
56
+ }
57
+ case "array": {
58
+ const min = typeof s.minItems === "number" ? s.minItems : 1;
59
+ const count = Math.min(Math.max(min, 1), 3);
60
+ return Array.from({ length: count }, (_x, i) => fabricateFromSchema(s.items, `${hint}-${i + 1}`, depth + 1));
61
+ }
62
+ case "string": {
63
+ if (s.format === "uri" || s.format === "url")
64
+ return "https://example.invalid/mock";
65
+ if (s.format === "date-time")
66
+ return "2024-01-01T00:00:00Z";
67
+ if (s.format === "date")
68
+ return "2024-01-01";
69
+ let value = `mock-${hint}`;
70
+ if (typeof s.minLength === "number" && value.length < s.minLength) {
71
+ value = value.padEnd(s.minLength, "x");
72
+ }
73
+ if (typeof s.maxLength === "number" && value.length > s.maxLength) {
74
+ value = value.slice(0, s.maxLength);
75
+ }
76
+ return value;
77
+ }
78
+ case "integer":
79
+ case "number": {
80
+ if (typeof s.minimum === "number")
81
+ return s.minimum;
82
+ if (typeof s.maximum === "number" && s.maximum < 1)
83
+ return s.maximum;
84
+ return 1;
85
+ }
86
+ case "boolean":
87
+ return true;
88
+ case "null":
89
+ return null;
90
+ default:
91
+ return `mock-${hint}`;
92
+ }
93
+ }
94
+ /** Tokens the mock runner reports per agent call, so `--token-budget` exercises
95
+ * budget-guarded script paths deterministically. */
96
+ export const MOCK_TOKENS_PER_AGENT = 1000;
97
+ function attributeBackend(model, tier, declared) {
98
+ const spec = model ?? tier;
99
+ if (!spec)
100
+ return "default";
101
+ const head = spec.split("/")[0].replace(/\[[^\]]*\]\s*$/, "").trim().toLowerCase();
102
+ if (declared && Object.keys(declared).some((name) => name.toLowerCase() === head)) {
103
+ return `${head} (script-declared)`;
104
+ }
105
+ try {
106
+ const registry = resolveBackendRegistry(declared);
107
+ return selectBackend({ model, tier }, registry).id;
108
+ }
109
+ catch {
110
+ return "default";
111
+ }
112
+ }
113
+ /**
114
+ * Validate a workflow script: parse it, then (by default) dry-run it against a mock
115
+ * AgentRunner. Never throws for an invalid script — read `report.ok` / `report.exitCode`.
116
+ */
117
+ export async function validateWorkflowScript(script, options = {}) {
118
+ const warnings = [];
119
+ let meta;
120
+ try {
121
+ meta = parseWorkflowScript(script).meta;
122
+ }
123
+ catch (error) {
124
+ return {
125
+ ok: false,
126
+ exitCode: 1,
127
+ parse: { ok: false, error: error instanceof Error ? error.message : String(error) },
128
+ warnings,
129
+ };
130
+ }
131
+ const declaredBackends = meta.backends && Object.keys(meta.backends).length > 0 ? meta.backends : undefined;
132
+ if (declaredBackends) {
133
+ warnings.push(`script declares custom backends (${Object.keys(declaredBackends).join(", ")}) — real runs must approve them ` +
134
+ `(allowScriptBackends / exec.scriptBackends / AGENTPRISM_ALLOW_SCRIPT_BACKENDS=1); the dry run treats them as approved`);
135
+ }
136
+ if (options.dryRun === false) {
137
+ return { ok: true, exitCode: 0, parse: { ok: true, meta }, warnings };
138
+ }
139
+ // Throwaway directories: the run cwd (unless the caller pins one) so worktree isolation
140
+ // no-ops, and a private persistence root so the run lease never touches the real store.
141
+ const ownedCwd = options.cwd === undefined;
142
+ const baseCwd = options.cwd ?? mkdtempSync(join(tmpdir(), "agentprism-validate-"));
143
+ const persistenceRoot = mkdtempSync(join(tmpdir(), "agentprism-validate-state-"));
144
+ const mockMeta = new Map();
145
+ const runner = {
146
+ async run(_prompt, runOptions = {}) {
147
+ const label = runOptions.label ?? "";
148
+ mockMeta.set(label, {
149
+ tier: runOptions.tier,
150
+ mode: runOptions.mode,
151
+ schema: runOptions.schema !== undefined,
152
+ });
153
+ runOptions.onUsage?.({
154
+ input: MOCK_TOKENS_PER_AGENT - 250,
155
+ output: 250,
156
+ cacheRead: 0,
157
+ cacheWrite: 0,
158
+ total: MOCK_TOKENS_PER_AGENT,
159
+ cost: 0,
160
+ });
161
+ if (runOptions.schema !== undefined)
162
+ return fabricateFromSchema(runOptions.schema);
163
+ return `[dry-run] mock output for ${runOptions.label ?? "agent"}`;
164
+ },
165
+ };
166
+ const agentCalls = [];
167
+ const checkpoints = [];
168
+ const controller = new AbortController();
169
+ let timedOut = false;
170
+ const timeoutMs = options.timeoutMs ?? 30_000;
171
+ const timer = setTimeout(() => {
172
+ timedOut = true;
173
+ controller.abort();
174
+ }, timeoutMs);
175
+ timer.unref?.();
176
+ const flows = options.workflows === undefined
177
+ ? undefined
178
+ : typeof options.workflows === "string" || Array.isArray(options.workflows)
179
+ ? openWorkflowDir(options.workflows)
180
+ : options.workflows;
181
+ const manager = new WorkflowManager({
182
+ agent: runner,
183
+ cwd: baseCwd,
184
+ journaling: false,
185
+ persistenceRoot,
186
+ loadSavedWorkflow: flows?.resolve,
187
+ });
188
+ manager.on("agentStart", (event) => {
189
+ const extra = mockMeta.get(event.label) ?? mockMeta.get("") ?? { schema: false };
190
+ agentCalls.push({
191
+ label: event.label,
192
+ phase: event.phase,
193
+ model: event.model,
194
+ tier: extra.tier,
195
+ mode: extra.mode,
196
+ backend: attributeBackend(event.model, extra.tier, declaredBackends),
197
+ schema: extra.schema,
198
+ });
199
+ });
200
+ try {
201
+ const run = await manager.runSync(script, options.args, {
202
+ journaling: false,
203
+ signal: controller.signal,
204
+ tokenBudget: options.tokenBudget,
205
+ maxAgents: options.maxAgents,
206
+ scriptBackends: declaredBackends,
207
+ confirm: async (promptText, checkpointOptions) => {
208
+ const opts = (checkpointOptions ?? {});
209
+ if (opts.headless === "abort") {
210
+ warnings.push(`checkpoint "${truncate(promptText, 60)}" sets headless: "abort" — unattended runs will fail at it`);
211
+ }
212
+ // Mirror the engine's headless resolution exactly: the declared default, else true.
213
+ const reply = opts.default ?? true;
214
+ checkpoints.push({ prompt: promptText, kind: opts.kind ?? "confirm", reply });
215
+ return reply;
216
+ },
217
+ });
218
+ // agentStart fires BEFORE the mock records its options, so backfill attribution for
219
+ // any call whose mock metadata arrived after the event (same tick ordering).
220
+ for (const call of agentCalls) {
221
+ const extra = mockMeta.get(call.label);
222
+ if (extra) {
223
+ call.tier = extra.tier;
224
+ call.mode = extra.mode;
225
+ call.schema = extra.schema;
226
+ call.backend = attributeBackend(call.model, extra.tier, declaredBackends);
227
+ }
228
+ }
229
+ const ok = run.status === "completed";
230
+ if (!ok && flows === undefined && run.reason?.includes("must be the first statement") && /\bworkflow\s*\(/.test(script)) {
231
+ warnings.push('the failure looks like a nested workflow("<name>") call on a bare name — provide workflow dirs ' +
232
+ "(ValidateWorkflowOptions.workflows / --workflows-dir) so names resolve during the dry run");
233
+ }
234
+ if (ok) {
235
+ if (agentCalls.length === 0 && checkpoints.length === 0) {
236
+ warnings.push("the script completed without a single agent() or checkpoint() call");
237
+ }
238
+ const declaredPhases = (meta.phases ?? []).map((p) => p.title);
239
+ // A phase counts as used via phase() OR via a per-call agent({ phase }) assignment.
240
+ const visited = new Set([...(run.phases ?? []), ...agentCalls.flatMap((c) => (c.phase ? [c.phase] : []))]);
241
+ for (const title of declaredPhases) {
242
+ if (!visited.has(title))
243
+ warnings.push(`meta.phases declares "${title}" but no phase("${title}") or agent({ phase }) used it`);
244
+ }
245
+ if (declaredPhases.length > 0) {
246
+ for (const title of visited) {
247
+ if (!declaredPhases.includes(title))
248
+ warnings.push(`phase "${title}" is used but meta.phases does not declare it`);
249
+ }
250
+ }
251
+ }
252
+ return {
253
+ ok,
254
+ exitCode: ok ? 0 : 2,
255
+ parse: { ok: true, meta },
256
+ dryRun: {
257
+ ok,
258
+ status: run.status,
259
+ reason: timedOut ? `dry run exceeded ${timeoutMs}ms and was aborted` : run.reason,
260
+ timedOut,
261
+ agentCalls,
262
+ checkpoints,
263
+ phasesVisited: run.phases ?? [],
264
+ logs: run.logs ?? [],
265
+ durationMs: run.durationMs,
266
+ result: run.result,
267
+ },
268
+ warnings,
269
+ };
270
+ }
271
+ finally {
272
+ clearTimeout(timer);
273
+ try {
274
+ rmSync(persistenceRoot, { recursive: true, force: true });
275
+ }
276
+ catch {
277
+ /* best-effort cleanup */
278
+ }
279
+ if (ownedCwd) {
280
+ try {
281
+ rmSync(baseCwd, { recursive: true, force: true });
282
+ }
283
+ catch {
284
+ /* best-effort cleanup */
285
+ }
286
+ }
287
+ }
288
+ }
289
+ function truncate(text, max) {
290
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
291
+ }
292
+ /** Render a ValidateWorkflowReport as the human-readable CLI output. */
293
+ export function formatValidateReport(report) {
294
+ const lines = [];
295
+ if (report.parse.ok) {
296
+ const meta = report.parse.meta;
297
+ const phases = meta?.phases?.length ? `${meta.phases.length} declared phase(s)` : "no declared phases";
298
+ const backends = meta?.backends ? `, ${Object.keys(meta.backends).length} script-declared backend(s)` : "";
299
+ lines.push(`✓ parse "${meta?.name}" — ${phases}${backends}`);
300
+ }
301
+ else {
302
+ lines.push(`✗ parse ${report.parse.error}`);
303
+ }
304
+ const dry = report.dryRun;
305
+ if (dry) {
306
+ const summary = `${dry.agentCalls.length} agent call(s), ${dry.checkpoints.length} checkpoint(s), ${dry.durationMs}ms`;
307
+ lines.push(dry.ok ? `✓ dry run completed — ${summary}` : `✗ dry run ${dry.status} — ${dry.reason ?? "unknown failure"} (${summary})`);
308
+ for (const call of dry.agentCalls) {
309
+ const spec = call.model ?? (call.tier ? `tier=${call.tier}` : "(default model)");
310
+ const bits = [call.phase ? `[${call.phase}]` : undefined, spec, `→ ${call.backend}`, call.schema ? "(schema)" : undefined, call.mode ? `mode=${call.mode}` : undefined]
311
+ .filter(Boolean)
312
+ .join(" ");
313
+ lines.push(` • ${call.label} ${bits}`);
314
+ }
315
+ for (const cp of dry.checkpoints) {
316
+ lines.push(` ◆ checkpoint [${cp.kind}] "${truncate(cp.prompt, 60)}" → ${JSON.stringify(cp.reply)}`);
317
+ }
318
+ }
319
+ else if (report.parse.ok) {
320
+ lines.push("- dry run skipped (--parse-only)");
321
+ }
322
+ for (const warning of report.warnings)
323
+ lines.push(` ! ${warning}`);
324
+ lines.push(report.ok ? "result: valid" : "result: INVALID");
325
+ return lines.join("\n");
326
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatalabs/workflows",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -13,6 +13,9 @@
13
13
  "type": "module",
14
14
  "main": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
+ "bin": {
17
+ "agentprism-workflows": "./dist/cli.js"
18
+ },
16
19
  "exports": {
17
20
  ".": {
18
21
  "types": "./dist/index.d.ts",
@@ -27,9 +30,9 @@
27
30
  "access": "public"
28
31
  },
29
32
  "dependencies": {
33
+ "@automatalabs/workflow-engine": "0.10.0",
30
34
  "@automatalabs/shared-types": "0.12.1",
31
- "@automatalabs/workflow-engine": "0.9.1",
32
- "@automatalabs/acp-agents": "0.20.2"
35
+ "@automatalabs/acp-agents": "0.20.4"
33
36
  },
34
37
  "scripts": {
35
38
  "build": "tsc -b",