Implement workflow orchestration engine (P0 + P1)#714
Conversation
…, native executor, akm workflow run Implements P0 + P1 of docs/technical/akm-workflows-orchestration-plan.md, plus the open P0 hygiene items: - IR: Workflow Plan Graph (src/workflows/ir/schema.ts, compile.ts). Linear workflows compile to one agent node + gate per step — no behavior change; fan-out steps compile to a map node with collect/vote reducers. - Extended Markdown grammar: ### Runner / Model / Timeout / Fan-out / Schema / Env / Depends On step subsections (parser-orchestration.ts), additive and backward-compatible, errors accumulated; Depends On ids validated in validator.ts. - Persistence: migration 004 workflow_run_units (+ failure_reason), unit CRUD on WorkflowRunsRepository, serialized writer queue (exec/unit-writer.ts) so N parallel unit completions never contend on SQLite's single writer. - Native executor (exec/native-executor.ts): fan-out via a scheduler built on concurrentMap (cap min(16, cores−2), lifetime unit cap, per-unit timeout default 10m), schema-validated structured output via the runStructured core + a bounded JSON-Schema-subset validator (core/json-schema.ts), unit preamble asset, env bindings through the akm env run machinery extracted to env-binding.ts (secret tokens, dangerous-key policy, keys-only audit), workflow_unit_* events. - Engine loop (exec/run-workflow.ts) + `akm workflow run` CLI: advances the gated spine strictly through completeWorkflowStep; gate rejections stop the engine and surface corrective feedback. Marked experimental in STABILITY.md. - Hygiene (check-in review C2/M1): plain-text formatters render the CONTINUE directive; every run-detail response evaluates the check-in; validator error message lists name/updated; removed the phantom `workflow step` alias from docs. - Model tiers: new `fable` built-in alias (claude-fable-5 / opencode/claude-fable-5); docs recommend fast/balanced/deep tiers with deep → fable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
Two independent adversarial reviews of a1b9637 confirmed 8 findings; all fixed with regression tests: - Prompt interpolation used string replaceAll, so item/param values containing GetSubstitution patterns ($&, $$, $', $`) silently corrupted unit prompts. All template replacements now use function callbacks. - `akm workflow run` on a failed/blocked run dispatched the terminal step's units (real cost) before completeWorkflowStep's preflight threw. The engine now refuses non-active runs before any dispatch. - The llm runner ignored per-unit timeout, abort signal, ### Model, and native structured output; chatCompletion now receives timeoutMs (### Timeout none → max 32-bit delay), signal, responseSchema, and a model override resolved through the global alias tiers. - The lifetime unit cap reset per invocation; it is now seeded from the run's workflow_run_units journal so it is truly per-run. - ### Env bindings were silently dropped on sdk/llm runners (audit event fired, nothing injected). Units now fail loudly with env_unsupported; docs updated (env requires the agent runner). - Fan-out concurrency defaulted to the engine cap, violating the repo's local-model-first LLM-defaults rule; it now defaults to 1 and the cap only clamps. - Resume re-dispatched and clobbered completed unit rows; completed units with a matching input_hash are now reused from the journal (durable-row resume at unit granularity). - ### Depends On was parsed and validated but never consulted; the engine now asserts dependencies are completed/skipped before dispatching a step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
There was a problem hiding this comment.
Pull request overview
Adds an engine-driven workflow orchestration layer to AKM: workflows can now declare per-step orchestration metadata (runner/model/timeout/fan-out/schema/env/depends-on), compile to a deterministic IR plan graph, and execute via a new akm workflow run path with per-unit persistence, concurrency controls, and structured-output validation.
Changes:
- Extend workflow Markdown parsing + schema to support orchestration subsections and validate
Depends Onreferences. - Introduce a Workflow Plan Graph IR compiler (P0) and a native executor + scheduler + unit journal/writer queue (P1), plus a new CLI subcommand
workflow run. - Add migration 004 and repository support for
workflow_run_units, plus substantial tests and docs for the new engine and related fixes (check-in surfacing, model alias).
Reviewed changes
Copilot reviewed 39 out of 39 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/workflows/run-units.test.ts | New tests for workflow_run_units persistence + serialized writer queue behavior. |
| tests/workflows/parser-orchestration.test.ts | New tests for orchestration subsection parsing and error cases. |
| tests/workflows/native-executor.test.ts | New tests for fan-out dispatch, schema validation/retry, durable-row reuse, and engine loop behavior. |
| tests/workflows/migrations.test.ts | Updates migration ledger expectations to include migration 004. |
| tests/workflows/ir-compile.test.ts | New tests for deterministic IR compilation and fan-out → map node lowering. |
| tests/workflows/checkin-surfacing.test.ts | New tests ensuring stalled-run check-in directives surface in status and plain text. |
| tests/storage/sqlite-migrations.characterization.test.ts | Updates workflow DB schema characterization to include workflow_run_units. |
| tests/storage/snapshots/sqlite-migrations.characterization.test.ts.snap | Snapshot update reflecting migration 004 schema additions. |
| tests/json-schema-subset.test.ts | New tests for the bounded JSON-Schema-subset validator. |
| tests/agent/agent-builders.test.ts | Adds tests for the new built-in fable model alias resolution. |
| STABILITY.md | Marks akm workflow run + orchestrated steps as experimental/stability caveat. |
| src/workflows/validator.ts | Adds semantic validation for ### Depends On references; fixes frontmatter allowed-keys messaging. |
| src/workflows/schema.ts | Adds orchestration types (runner, model, timeout, fanOut, schema, env, dependsOn). |
| src/workflows/runtime/workflow-asset-loader.ts | Retains full parsed workflow document for IR compilation while keeping the existing step projection. |
| src/workflows/runtime/runs.ts | Includes check-in evaluation in run-detail shaping (checkin surfaced when stalled). |
| src/workflows/parser.ts | Integrates orchestration parsing and improves unknown-subsection error messaging. |
| src/workflows/parser-orchestration.ts | New orchestration subsection parser (Runner/Model/Timeout/Fan-out/Schema/Env/Depends On). |
| src/workflows/ir/schema.ts | New backend-agnostic plan-graph IR schema for workflow execution nodes + gated step spine. |
| src/workflows/ir/compile.ts | New deterministic compiler from parsed workflows → plan graph IR (linear and fan-out). |
| src/workflows/exec/unit-writer.ts | New serialized write queue to avoid concurrent SQLite writer contention. |
| src/workflows/exec/scheduler.ts | New scheduler policy layer (global concurrency cap + lifetime unit cap + abort support). |
| src/workflows/exec/run-workflow.ts | New engine loop implementing akm workflow run, advancing steps strictly via completeWorkflowStep. |
| src/workflows/exec/native-executor.ts | New native executor for one step plan: dispatch fan-out units, validate structured output, persist unit rows. |
| src/workflows/db.ts | Adds migration 004 creating workflow_run_units table + index. |
| src/workflows/cli.ts | Registers run as a workflow subcommand. |
| src/storage/repositories/workflow-runs-repository.ts | Adds workflow_run_units row types and repo methods to insert/finish/query units. |
| src/output/text/workflow.ts | Registers a plain-text formatter for workflow-run output. |
| src/output/text/helpers.ts | Adds workflow run plain formatter; restores check-in directive in plain status output. |
| src/output/shapes/passthrough.ts | Adds workflow-run to passthrough output command list. |
| src/integrations/agent/model-aliases.ts | Adds built-in fable alias mapping for supported platforms. |
| src/core/json-schema.ts | New bounded JSON-Schema-subset validator used for structured output verification. |
| src/core/events.ts | Adds workflow_unit_started / workflow_unit_finished event types. |
| src/commands/workflow-cli.ts | Adds akm workflow run JSON command wiring and flag parsing (--params, --max-steps). |
| src/commands/env/env-cli.ts | Refactors env injection path to reuse extracted resolveEnvBinding. |
| src/commands/env/env-binding.ts | New shared env-ref resolution + safety enforcement used by both env run and workflow units. |
| src/assets/prompts/workflow-unit-preamble.md | New standardized per-unit preamble template injected into dispatched prompts. |
| docs/features/workflows.md | Documents orchestrated steps and the new akm workflow run behavior and semantics. |
| docs/features/agent-integration.md | Documents fable as a built-in model alias. |
| CHANGELOG.md | Adds release notes for orchestration engine, check-in surfacing fixes, and fable alias. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- resolveFanOutSource now uses own-property checks (Object.hasOwn) so a fan-out `over:` key can never resolve to an Object.prototype member (toString, constructor, ...) instead of real params/evidence. - `### Env` entries are validated with parseAssetRef instead of an "env:" substring probe: the `environment:` alias and origin-qualified refs are accepted, while non-env refs like "myenv:foo" or "secret:token" are rejected at parse time. - Corrected the run-workflow header comment: the plan is compiled once per invocation (the run's step snapshot is fixed at start), not per step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…surfacing, scheduler tests Final sweep against docs/technical/akm-workflows-orchestration-plan.md closed the remaining committed-scope gaps: - ### Route (the routing pattern, explicitly in P1 + the touch list): `input:` / `when: <value> => <step-id>` / `default:` grammar with accumulated parse errors; validator enforces targets exist, come after the routing step, and never self-route; compiles to IrRouteSpec on the step plan (IrRouterNode remains the node-level form for the imperative frontend). The engine evaluates the route from the step's structured result (schema field or vote winner), run params, or prior evidence — own-property lookups, primitives only — records the decision in step evidence, fails the step on unroutable values, and auto-skips unselected branch targets via completeWorkflowStep(status: skipped). - Conformance suite (tests/workflows/conformance/): golden linear, fan-out+schema+vote, and routed workflows pinned as explicit expected plans + executed unit graphs, structured so P3/P4 backends register in BACKENDS and must reproduce identical graphs (plan §Anti-drift). - akm show now surfaces each step's orchestration summary (runner/profile/model/timeout/fan-out/schema-presence/env/dependsOn/ route) via WorkflowStepOrchestrationSummary. - Direct scheduler tests: default concurrency 1, clamp to the engine cap, min(16, cores-2) derivation, pre-dispatch lifetime-cap error, abort stops claiming, sibling isolation on failure. Deliberately deferred per the plan's phasing: best-of-n reducer and looping gates (P4 judge machinery), phases/watch/budget/worktree (P3/P4), CC emitter + report (P3), per-harness extractors (P2), replay resume (P5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…identity, unit session ids
Orchestration plan P2, executed via multi-agent workflow with peer review:
- AkmHarness descriptor fields: pattern (in-harness|local-runner|
cloud-delegate), structuredOutput (native-schema|native-json|none),
resume { flag, takesSessionId }, identityEnv (session-id vars) split
from presenceEnv (presence-only flags), resultExtractor seam.
- Adapters (builder + result extractor + tests each): codex (exec,
--output-schema native schema, JSONL), copilot (-p --allow-all-tools,
json), pi (-p --mode json), gemini (-p --output-format json), aider
(-m --yes-always, prompt-injected schema), amazonq (q chat
--no-interactive, bare --resume flagged takesSessionId: false),
openhands (--headless -t --json). All registered in HARNESS_REGISTRY;
BUILTIN_BUILDERS derivation makes them dispatchable, and the
missing-builder ConfigError no longer fires for codex/gemini/aider.
- agent-identity.ts and the session-log provider list are now derived
from the registry; presence flags (CODEX_SANDBOX, GEMINI_CLI) infer
the harness but never persist as agent_session_id.
- Migration 005: workflow_run_units.session_id — harness-native session
ids journaled opportunistically via the extractor seam, threaded
through the executor (UnitOutcome → finishUnit → durable-row reuse).
- Builtin profiles for copilot/pi/amazonq/openhands (+ headless
variants); executor applies a harness's result extractor to agent
stdout before recording unit text.
P3+ intentionally NOT started: paused for a redesign review of the
remaining phases (Claude Code coupling concerns).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
… semantics Post-P2 owner review found the P1 execution semantics brittle and coupled to Claude Code. Root cause recorded in the addendum: P1 ported CC's features (fan-out, schemas, routes) without its foundation (immutable plan, explicit data flow, journaled replay, single driver). Owner decisions (2026-07-06), superseding the P1 grammar and the P3/P5 designs: - Orchestrated workflows become a YAML program format with a published JSON Schema and a closed, parsed expression language (params/steps.<id>.output/item) — no ambient evidence search, no string re-substitution. - Full replay semantics: plan frozen per run (plan_json/plan_hash, migration 006), content-derived unit identity, replay-divergence detection, run lease; orchestration decisions byte-reproducible. - P1 markdown orchestration subsections will be removed (linear markdown workflows stay stable/unchanged); DB stays additive. - Failure policy surface (on_error/retry on the failure_reason taxonomy), fail-fast default. - P3 CC-delegation compiler replaced by a harness-neutral driver protocol (workflow brief + report). Copilot cloud delegate and stash MCP server remain out of scope. Phases R1-R4 replace P3-P5. No code changes in this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…ded; compiler mid-flight
Paused snapshot of the R1 workflow run (wf_35e2cd58-5d6), resuming on a
timer. State:
- DONE (green, tested): src/workflows/program/ expression language
(closed ${{ }} grammar, parsed AST, single-pass resolution) +
program-expressions tests; schemas/akm-workflow.json + YAML program
parser/validator (src/workflows/program/{schema,parser}.ts) +
program-parser tests.
- IN FLIGHT (agent died mid-task; will re-run on resume):
src/workflows/ir/schema.ts is rewritten to IR v2, but ir/compile.ts
and the executor still expect v1 — THIS COMMIT DOES NOT TYPECHECK.
Migration 006 and asset plumbing not started.
Do not build on this commit; the resumed workflow completes the
compiler, frozen-plan, asset-plumbing, and engine-cutover phases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…en plans, asset plumbing in flight Mid-run durability checkpoint of the resumed R1 workflow (wf_35e2cd58-5d6). Landed since fa0267d (compiler + frozen-plan phases, both green): - ir/schema.ts + ir/compile.ts at IR v2: compileWorkflowProgram (YAML, full expression validation, defaults merged at compile) and compileWorkflowPlan (linear markdown golden) both lowering to one plan graph; ir/plan-hash.ts canonical JSON + sha256. - Migration 006: plan_json/plan_hash + engine lease columns; setRunPlan in the repository; startWorkflowRun compiles and freezes the plan in the same transaction as the run insert; run-workflow executes the frozen plan with hash integrity check + legacy fallback. - ir-compile/conformance/migration tests rewritten for v2. Asset-plumbing agent still running; engine cutover pending — tree may not fully typecheck. Do not build on this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…plete; paused before docs R1 workflow (wf_35e2cd58-5d6) paused at the docs phase per owner instruction. Landed since 24949aa (both phases green; tree typechecks, 596 workflow/agent tests pass): - Asset plumbing: workflow:<name> refs resolve .yaml/.yml; loader projects programs into WorkflowAsset (program field); indexer matcher + renderer for YAML programs (show carries the orchestration summary); workflow validate lints YAML via parse+compile; workflow template --yaml (src/workflows/authoring/workflow-program-template.yaml). - Engine cutover to IR v2 + expressions: executor resolves ${{ item }} / ${{ params.* }} / ${{ steps.<id>.output.* }} via the parsed-AST single-pass resolver (ambient evidence search DELETED); failure policy live (on_error continue + bounded retry on failure_reason, fail-fast default, retry attempts journaled as <unitId>~r<n>); route v2 (explicit input expression + when map); P1 markdown orchestration grammar REMOVED (parser-orchestration.ts deleted, schema/validator/ renderer trimmed; linear markdown unchanged); executor/conformance test suites rewritten against YAML program sources. Remaining on resume: r1-core adversarial review (its first attempt died mid-run and is un-journaled, so it re-runs BEFORE docs), docs phase, final verify. Script hardened: a dead review agent now halts the run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…written R1 run wf_35e2cd58-5d6: the r1-core adversarial review returned 6 findings (4 actionable); the fix agent resolved all four at root cause with regression tests — notably a route-dispatch defect in run-workflow.ts and a `templating: verbatim|expressions` discriminator on IR agent nodes so linear-markdown instructions are never expression-parsed. Tree is green (tsc clean, 603 workflow/agent tests). The docs agent wrote its doc updates (CHANGELOG/STABILITY/workflows.md/ plan annotations) but died before journaling a result; it re-runs on resume, followed by the final verify + final review phases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…s, cascaded routing, per-dispatch cap)
Closes R1 of the redesign addendum (run wf_35e2cd58-5d6). This commit
lands the docs phase and the final whole-diff review's three confirmed
fixes (7 regression tests):
- Step-output contract: engine steps journal a promoted artifact under
evidence.output (solo result/text; collect -> per-item array with
null slots for on_error:continue failures; vote -> winner), and the
exported projectStepOutput() is the single projection used by both
the unit-template scope and route evaluation — so the documented
${{ steps.<id>.output.<path> }} addressing resolves against real
results end-to-end. Manual/pre-R1 evidence is exposed as-is.
- Cascaded routing: a skipped router's own targets are marked
skip-on-reach (live path AND journaled decisions on resume), so an
unselected branch's downstream branch targets never dispatch and the
skipped router's input is never resolved.
- Lifetime unit cap is now consumed per ACTUAL journaled dispatch
(retries included, durable-row reuses free) via a DispatchBudget in
the executor; the pre-batch check that made large partially-completed
fan-outs unresumable is removed. Capped steps fail hard regardless of
on_error.
Docs: workflows.md rewritten for the YAML program format (expression
table, frozen-plan semantics, failure policy, step-output contract),
STABILITY + CHANGELOG updated, plan addendum annotated R1 SHIPPED.
Verified post-fix: biome clean (12 intentional template-literal
warnings in test fixtures), tsc clean, unit 23780 pass / integration
735 pass with only the documented pre-existing failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…ergence + run lease R2 run wf_98b198b1-d0e, first two phases green (tsc clean, 274 workflow tests pass): - Unit identity is content-derived (<node_id>:<sha256(item)[:12]>, :solo for single units; ~r<n> retries and ~l<loop> gate-loop rows stack on top) — identity survives item-list regeneration/reordering. Duplicate fan-out items fail the step deterministically. Journaled COMPLETED rows with matching identity but different input_hash raise a hard "replay divergence" failure, never a silent re-dispatch; pre-release positional-id rows are ignored cleanly. - Run lease enforced (columns from migration 006): atomic acquire before dispatch, renew per step, release on exit incl. failure paths; a second engine invocation on a live lease refuses up front; expired leases are claimable; manual `workflow complete` is refused while an engine holds the spine (leaseHolder threaded through CompleteWorkflowStepInput, engine-only). Run halted at the artifact-gates phase by the account monthly spend limit (must() guard, working as designed). Remaining on resume: artifact gates + max_loops, budget, watch, worktree+SDK, docs, verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…iew r2-a fixes applied Watchdog checkpoint (run wf_98b198b1-d0e in flight, Budget phase active). Landed since aa1dbc7, tsc clean + 287 workflow tests green: - Typed artifacts: outputSchema validated against the promoted artifact before completion; artifact-judging gates (judge receives the artifact, canonical JSON clipped, not engine prose); gate evaluator calls journaled as unit rows (<stepId>.gate:l<loop>); bounded gate loops (max_loops) with feedback threaded into re-run prompts, loop rows journaled as ~l<loop>. - Review r2-a: 5 findings, actionable ones fixed — notably the run-workflow !result.ok branch failing runs unconditionally, which broke on_error: continue semantics at the engine level. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
I would not merge this PR as-is. It is large — 107 files, 15,948 additions, 268 deletions — and the riskiest changes are in execution, persistence, replay, and validation paths. I did a static review from the GitHub PR/source. I did not execute the test suite locally. Blocking / high-risk correctness issues
The PR adds lease columns, but both docs and migration comments say enforcement is deferred. The runtime still loads the next step and frozen plan, then dispatches without acquiring any cross-process lease. The in-process unit writer queue does not solve this. It serializes writes only inside one Bun process; it does not prevent a second CLI process from dispatching the same unit. Impact: duplicated agent/LLM spend, duplicated side effects, corrupt run evidence, nondeterministic branch execution. Fix: implement a DB-backed lease before executing any step. Use an atomic UPDATE ... WHERE engine_lease_until IS NULL OR engine_lease_until < now pattern, store holder id, refresh while running, and release on completion/failure.
The input hash only includes prompt, runner, model, and schema. It excludes resolved profile, resolved runner kind, resolved model after alias lookup, environment bindings, timeout, isolation, config defaults, tool policy, and harness adapter behavior. Worse, environment bindings are resolved before checking completed unit rows, so a fully reusable completed step can fail just because a now-unused env asset was deleted or changed. The default dispatcher also resolves live config at execution time, not from a frozen execution profile. Impact: the engine can silently skip work that should be re-run, or fail to replay completed work because live config/env changed. Fix: either freeze the resolved execution context into the plan/run, or include every execution-affecting input in the unit hash. Move env resolution into the dispatch path after durable-row reuse, or persist env binding identity/version in the hash.
The native executor returns summaries like “Executed N units…” rather than a task-specific summary of the actual work. That synthetic string is then passed into completeWorkflowStep as the summary for completion criteria validation. The docs admit artifact-judging gates are not implemented yet. Impact: gates can falsely reject good work or falsely pass bad work. on_error: continue makes this worse because the gate receives a generic count of failed units rather than structured evidence. Fix: do not market this as gated artifact execution yet. Either pass actual artifact/evidence into the judge, or disable criteria validation for native engine execution until R2 artifact gates exist.
concurrentMap catches every thrown worker error and leaves the slot undefined. The native executor then maps that to an "aborted" failure with a generic message. Under on_error: continue, those swallowed infrastructure errors can still allow the step to pass. Impact: DB write failures, programming errors, and unexpected dispatcher bugs can be hidden as ordinary unit failures. Fix: distinguish expected unit failures from infrastructure exceptions. concurrentMap should return { ok:false, error } or rethrow fatal errors, not erase them.
resultJson uses outcome.text ? JSON.stringify(outcome.text) : null, so a successful text output of "" becomes NULL. On replay, NULL becomes undefined, and the successful output is lost. Impact: downstream expressions can see null/missing output even though the original unit succeeded with an empty string. Fix: use outcome.text !== undefined instead of truthiness.
Docs say params are JSON-Schema declarations. But the compiler only keeps parameter names in the frozen plan, dropping the schemas. startWorkflowRun accepts and stores raw params without validating them against the declared schemas. Impact: invalid params fail later as expression/runtime errors, not as authoring/start validation errors. The declared schema is mostly documentation. Fix: freeze param schemas into the plan and validate --params at start/auto-start.
The docs say akm workflow validate checks “unknown step, unknown param, bad path” at lint time. The compiler checks whether a referenced step exists and comes earlier, but it does not validate the output path against the producer’s declared output schema. It explicitly does not check param presence. Impact: users will trust validation results that do not actually prove what the docs say they prove. Fix: either implement schema-aware path validation and declared-param checks, or weaken the docs.
The program schema allows step ids beginning with digits and containing dots. The expression parser’s requires a letter/underscore first and allows letters, digits, underscores, and hyphens — but not dots. So a step id like 1.review can be valid as a step id but impossible to reference as ${{ steps.1.review.output... }}. Impact: valid workflow programs can contain step ids that cannot be addressed by the expression language. Fix: make step ids expression-addressable, or tighten the step id pattern to the expression identifier grammar.
For required fields, the validator uses key in record, so inherited prototype properties can satisfy required. It also uses key in record for properties. additionalProperties: false is enforced only when properties exists. A schema like { type: "object", additionalProperties: false } should only allow {}, but this implementation will allow arbitrary properties. Impact: structured output validation can pass objects that violate the declared schema. Fix: use Object.hasOwn(record, key) and enforce additionalProperties: false even when properties is absent.
The allowed retry.on vocabulary is the PROGRAM_RETRY_REASONS set. But the executor can emit reasons outside that set, including validation_error, expression_error, unit_cap_exceeded, env_unsupported, and dispatch_error. Impact: workflow authors cannot write retry policies for actual engine failures. Some retryable conditions become unretryable by schema. Fix: define one canonical workflow failure taxonomy and make parser, executor, docs, and tests all use it.
The package is "type": "module" and TypeScript emits module: "ESNext". Yet runtime code uses bare require(...) in native-executor.ts and runs.ts. The repo has Node compatibility scripts, so this is not just a Bun-only concern. Impact: Node ESM execution can throw ReferenceError: require is not defined on these paths. Fix: use dynamic await import(...), or explicitly create require via createRequire(import.meta.url) in every ESM module that needs it.
seedJournaledRouteDecisions reads evidence.route.selected and applies it directly. journaledRouteSelection only checks that it is a non-empty string. applyRouteDecision then adds that arbitrary selected step to routeSelected and marks all declared targets as unselected. Impact: corrupted/manual/tampered evidence can cause all real branches to be skipped. Fix: validate journaled selected against Object.values(route.when) plus defaultStepId; fail loudly if it is not a legal branch target.
runWorkflowSteps calls getNextWorkflowStep, then seeds units and loads the frozen plan before the main loop. That happens even when next.done is already true. loadFrozenPlan can throw for corrupt plan JSON/hash or fall back to loading the live asset for legacy runs. Impact: akm workflow run can fail because a completed run’s plan is corrupt or legacy asset is missing, even though no execution is needed. Fix: return final state before loading the plan when next.done is true.
The repository uses INSERT OR REPLACE for unit starts. The lifetime cap is seeded from the number of unit rows. Impact: repeated crash/retry of the same base unit id can replace the row instead of increasing the durable dispatch count. That makes the “lifetime” cap less durable than advertised. Fix: append attempts instead of replacing, or keep a separate monotonic dispatch-attempt counter.
finishUnit runs an UPDATE ... WHERE run_id = ? AND unit_id = ? but ignores the affected row count. Impact: if the insert failed, row was deleted, or unit id mismatched, the executor can proceed as though persistence succeeded. Fix: check changes; throw a fatal persistence error if no row was updated. Major design / architecture issues
The plan is frozen, but execution substrate is not. Runner resolution, default profile, default LLM, model aliases, and harness resolution are read from live config at execution time. Impact: the same frozen plan can execute differently across invocations if config changes. Fix: freeze resolved execution settings or record config fingerprints into the run/unit hash.
completeWorkflowStep builds a judge only when a default LLM exists; otherwise judge is null and validation effectively does not block completion. Impact: “completion gates” are optional in a way that may surprise users, especially because the native engine claims to advance through gates. Fix: make fail-open explicit in docs/CLI output, or provide a config option to fail closed for workflows that require gates.
The parser only verifies that output is an object. The validator ignores $ref, combinators, pattern, format, and every unknown keyword. Impact: users can write schemas that look strict but are silently weak. Fix: either use a real JSON Schema validator or clearly name this as akmSchemaSubset and reject unsupported keywords instead of ignoring them.
Docs list typed step-output validation, artifact gates, gate.max_loops, run leases, budget ceilings, watch, and worktree isolation as not enforced. The compiler still carries some of those fields into the IR. Impact: users can author workflows that look safer/more controlled than they are. Fix: hide or reject not-yet-enforced fields unless an explicit experimental flag is enabled.
completeWorkflowStep always appends workflow_step_completed, regardless of whether the status is completed, failed, blocked, or skipped. It also includes notes in metadata. The engine uses completeWorkflowStep for skipped route targets and failed execution. This conflicts with the nearby event hygiene rule that raw workflow-authored content should stay out of events. Impact: consumers of events can misinterpret failed/skipped steps as completed, and raw notes may become prompt-injection surface. Fix: emit status-specific events or include status in metadata; remove raw notes from event metadata.
On route evaluation failure, the code writes notes with the route error, but persists only result.evidence, not an evidence field describing the route failure. Impact: status/evidence consumers do not get structured route failure diagnostics. Fix: persist evidence.routeError or equivalent.
startWorkflowRun checks for an existing active run and then inserts a new run in the same callback, but there is no unique partial index or lock shown here. Impact: two processes can race and both create active runs for the same workflow/scope. Fix: enforce this at the database level, not only in application code.
Runtime code supports origin-qualified refs by parsing origin//workflow:name. But resolveWorkflowFilePath only treats values starting with workflow: as refs; anything else is returned as a filesystem path. Impact: akm workflow validate some-origin//workflow:foo will be treated as a path, while other workflow commands can treat it as a ref. Fix: parse refs with parseAssetRef instead of startsWith("workflow:").
workflow template --yaml can print a YAML program. But createWorkflowAsset always parses imported content as markdown and writes through the markdown workflow creation path. Impact: users can validate/run YAML workflows, but the create command does not provide a clean YAML creation path. Fix: add akm workflow create --yaml or a separate workflow program create.
Docs say timeout values are strings: "ms" | "s" | "m" | "none". Parser also accepts bare numbers as milliseconds. Impact: docs and schema expectations drift; tests may miss numeric timeout behavior. Fix: either document bare numeric milliseconds or reject them.
loadExpressionChecker uses createRequire(import.meta.url) with "./expressions" and returns null on any failure. It also catches parser/checker exceptions and returns null, meaning malformed expressions can pass this parser pass if the checker import/API fails. Impact: duplicated parser/compiler validation gives false confidence and hides broken imports. Fix: directly import the expression parser. The “parallel task may not exist” TODO should be removed now that the file exists.
resolveEnvBindings merges each env ref with Object.assign. Later refs silently overwrite earlier keys. Impact: a workflow author may think multiple env assets are additive when they are order-dependent. Fix: detect duplicate keys and fail unless an explicit override policy is declared. Documentation / release-quality issues
The “Added” section says workflow orchestration is declared with markdown subsections like ### Runner, ### Fan-out, ### Schema, etc. The “Changed” section then says that same markdown grammar was removed before release and replaced with YAML programs. Impact: release notes are confusing and imply a surface that no longer exists. Fix: remove the obsolete markdown-orchestration “Added” language and describe only the final YAML surface.
The Codex builder says it is “NOT registered anywhere yet.” But the Codex harness imports the builder and exposes it on the registered harness descriptor. The result extractor has the same stale comment. Fix: remove stale integration-task comments before merge.
The code hardcodes fable to claude-fable-5 / opencode/claude-fable-5. Docs present fable as a built-in tier and recommend it for deep work. Impact: hardcoded provider model ids age badly. If these ids are not valid in every supported environment, default workflows fail. Fix: keep semantic aliases in docs/config examples, but avoid hardcoding speculative or provider-specific defaults unless covered by integration tests or official compatibility checks.
runs.ts has a duplicate /** before buildDefaultSummaryJudge. Fix: remove the extra opener.
compileProgramUnit says retry dispatch is “engine-rework scope,” but the executor implements bounded retry and tests it. Fix: update the comment; stale architecture comments are dangerous in this PR because the surface is already complex. Missing tests I would require before merge The added tests cover a lot of happy-path and peer-review scenarios, but the native executor tests intentionally use injected fake dispatchers and avoid real agent binaries/LLMs. I would add tests for:
Merge recommendation Block this PR until at least these are fixed: run lease enforcement, replay hash completeness, param/schema validation alignment, expression id grammar mismatch, JSON Schema validator bugs, ESM require usage, and misleading gate semantics. The rest can be follow-up only if the feature remains clearly experimental and the docs stop overstating what is enforced. |
…ll green Watchdog checkpoint (run wf_98b198b1-d0e in flight, review r2-b active). Landed since 51f98f3, tsc clean + 293 workflow tests green: - Budget ceilings end to end: YAML budget { max_tokens, max_units } → schema/parser/compiler → frozen plan.budget; enforcement journal- seeded, abort-driven, hard failure naming the ceiling. - workflow watch: NDJSON backlog + --stream polling with terminal-status exit (src/workflows/exec/watch.ts), onEvent seam on RunAgentOptions. - SDK per-cwd seam (open decision 1, proper option) + worktree isolation on agent AND sdk runners (git worktree lifecycle, worktree_path journaled at dispatch, clean-remove/dirty-retain) + sdk env bindings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
Watchdog checkpoint (run wf_98b198b1-d0e, final-check phase active). Since ed78519, tsc clean + 294 workflow tests green: - Review r2-b: 5 findings, 4 confirmed and fixed at root cause with regression tests proven to fail pre-fix (incl. worktree-leftovers integration coverage). - R2 docs: workflows.md YAML program section extended (budget, isolation, gate max_loops, replay divergence, run lease, watch), STABILITY.md orchestration bullet updated, CHANGELOG R2 entry, plan addendum annotated R2 SHIPPED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
Closes R2 of the redesign addendum (run wf_98b198b1-d0e, 14 agents, zero errors). This commit lands the final whole-diff review's confirmed fix plus the final-check's test relocation: - Budget-seed accounting: gate-evaluation journal rows (now stamped phase="gate" at insert) are excluded from the resume-time seed for budget.max_units and the lifetime unit cap — resumed runs no longer hit spurious "budget exceeded" failures from counting judge calls as dispatches. Discriminator is the phase column (step ids may contain dots, so a node_id suffix match could collide). Regression test proven to fail against the pre-fix code. - tests/workflows/worktree.test.ts moved unchanged to tests/integration/workflow-worktree-leftovers.test.ts: it spawns real git subprocesses, which unit scope forbids (lint-tests-isolation Rule 5); integration scope is the rule's own remedy. R2 delivered across this run (see the wip checkpoints for detail): content-derived unit identity + replay divergence, single-driver run lease, typed step artifacts + artifact-judged gates + bounded max_loops with journaled gate evaluations, budget ceilings, workflow watch + onEvent, SDK-per-cwd seam + worktree isolation + sdk env bindings. Verified: biome clean (12 intentional template-literal warnings), tsc clean, unit 24020 pass / integration 747 pass with only the documented pre-existing failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
@codex do an exhaustive code review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex do an exhaustive code review |
Extract the engine's step semantics (work-list/identity/hashing/prompt assembly, gate-feedback recovery, completion + route helpers) into src/workflows/exec/step-work.ts, behavior-preserving, and add the read-only `akm workflow brief` surface on top of it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e78cb16797
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| /** Lease lifetime: long enough to survive slow steps between renewals, short | ||
| * enough that a crashed engine frees the run quickly. Renewed per step. */ | ||
| const RUN_LEASE_TTL_MS = 90_000; |
There was a problem hiding this comment.
Keep run leases alive during long unit dispatches
With the lease expiring after 90s and only being renewed between steps, any unit that runs longer than the TTL (the default unit timeout is 10 minutes) leaves the run claimable while the first engine is still dispatching. A second akm workflow run after the expiry can acquire the lease and dispatch the same pending/running units, and insertUnit uses the same unit ids, so the two invocations can clobber each other's journal rows and double-run side-effecting work. The lease needs a heartbeat during long step execution or a TTL that cannot expire while dispatch is in flight.
Useful? React with 👍 / 👎.
| // Explicit extension wins (accepts refs like "release/ship.yaml"). | ||
| const lower = name.toLowerCase(); | ||
| for (const ext of WORKFLOW_EXTENSIONS) { | ||
| if (lower.endsWith(ext)) return path.join(typeRoot, name); | ||
| } |
There was a problem hiding this comment.
Reject YAML suffixes when creating markdown workflows
Because explicit workflow extensions now win here, akm workflow create foo.yaml routes the create path to workflows/foo.yaml, but createWorkflowAsset still writes and validates the markdown template with parseWorkflow. The resulting asset is immediately unusable: show/start/validate choose the YAML program parser by the .yaml extension and reject the markdown body. Either the create path should reject .yaml/.yml names or write/validate a YAML program for those suffixes.
Useful? React with 👍 / 👎.
| /** Step ids: `[A-Za-z0-9][A-Za-z0-9._-]*` (also pinned in the JSON Schema). */ | ||
| export const PROGRAM_STEP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; |
There was a problem hiding this comment.
Align step IDs with the expression grammar
This pattern accepts step ids that start with digits or contain dots, but step-output references are parsed with readIdent, which requires a letter/underscore first character and stops before dots. In workflows that later need to consume such a step's output, ids like 1build or build.js are valid in the parser/schema but cannot be addressed as ${{ steps.<id>.output }}, so map inputs, routes, and later unit templates become impossible to write without renaming the step.
Useful? React with 👍 / 👎.
| : outcome.text | ||
| ? JSON.stringify(outcome.text) | ||
| : null, |
There was a problem hiding this comment.
Preserve empty-string unit outputs in the journal
Successful text units that legitimately return an empty string are journaled as NULL here because the check uses truthiness instead of outcome.text !== undefined. On a later resume, reuseCompletedUnit rehydrates that completed row with no text or result, so the promoted step artifact changes from "" on the live run to null on replay, which can break downstream ${{ steps.*.output }} consumers and schema gates.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The live/replay divergence you identified was real and is fixed, but in the opposite direction (37e504c): an empty successful free-text output now normalizes to absent on every surface — live dispatch, journal rehydration on resume, and the R3 workflow report ingest all produce result_json = NULL and the same promoted artifact. Preserving "" was the other consistent option, but "no output" is the more honest reading of an empty result, and it keeps the failed-branch semantics (which already mapped ""→NULL) uniform. The semantics are documented in the executor module doc, and the EMPTY_OUTPUT driver-parity golden plus a chaos test pin all three surfaces to the identical unit graph. Schema-bearing units are unaffected: an empty output there fails schema validation rather than passing a null through.
Generated by Claude Code
…ress Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…fixes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…ogress Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
Codex round-3 findings and the remaining plan items are all landed (head Round-3 fixes — all 10 findings verified true: engine resume now seeds gate-loop state from the journal via the same shared helpers brief/report use (crash-after-rejection resumes at loop 2 with stored feedback, pinned by a parity golden); One disposition note on the Claude structured-output finding (review body, no inline thread): fixed in substance — the builder now emits Plan completion: the addendum's last outstanding promises are in — Generated by Claude Code |
|
Manual validation pass on the new workflow feature. I reviewed the docs, workflow-related stash memories/context, the new CLI/exec/runtime code paths, and then ran break-oriented CLI scenarios in an isolated sandbox. Sandbox/context used:
Findings
Validated
Open questions
If useful, I can break these into follow-up issues with minimal repro commands per item. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…ed contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
…mands, settle for terminal steps, SDK server disposal - `workflow list --active` returns exactly status='active'; blocked runs stay visible in the plain list (scope guards were already separate and are pinned unchanged). - brief's report command for a live-claimed unit now carries the claim holder (--session-id), so a second driver cannot mistake claimed work for free. - A fully-terminal-but-unfinalized step (e.g. required-gate blocked then resumed) is now first-class: brief says so and emits one settle command; `report --settle` finalizes it through the shared completion path under the finalize CAS. - Root cause of the post-success CLI hang: the opencode SDK server registry cached live `opencode serve` children with teardown only on process exit — which the children themselves prevented. The engine now drains dispatch resources in its run finally on every exit path. - Registry made a config leaf again (harness.ts) to fix the TDZ crash the disposal export introduced, with a subprocess load-order regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
Thanks for the break-oriented pass — all four findings reproduced, root-caused, and fixed (head 1. Is 2. Should brief ever show a claimed unit as 3. Intended UX after 4. The post-success hang — real, root-caused, fixed. The opencode SDK runner caches No follow-up issues needed unless #4 still reproduces for you on this head — if it does, that repro would be gold. Generated by Claude Code |
|
Follow-up retest after the fixes landed. Pulled latest Automated retest
Manual retest in a fresh sandbox
Additional real-world manual scenarios I added and ran
Remaining manual caveat
Runbook updated
If helpful, I can split the remaining live-harness hang into a dedicated follow-up issue with the minimal repro from the runbook. |
…r pin the CLI Registry draining alone was insufficient: the SDK's createOpencodeServer close() only sends SIGTERM and never unrefs the child or its stdio, so akm's event loop stayed pinned until the real `opencode serve` process actually exited — reproducibly hanging the caller after a successful run. The runner now owns the spawn (createManagedOpencode; the SDK package is used only for createOpencodeClient): after the URL handshake the child and its pipes are unref'ed/destroyed, and close() sends SIGTERM with an unref'ed 2s grace timer escalating to SIGKILL. The spawn stays in the factory call's synchronous prefix, preserving the env-overlay contract. Lifecycle proven against real children, including one that ignores SIGTERM. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
Thanks for the retest and the runbook — and especially for confirming the live-harness hang survived the registry drain. That repro pinpointed the real mechanism, now fixed in Why the drain wasn't enough: The fix: the runner now owns the spawn ( Proven against real children this time ( Your runbook's Generated by Claude Code |
|
Follow-up: I implemented the shared spawn-layer fix, reran the regressions, and then reran the manual verification end to end. What changed
Regression results
Manual verification rerun
Live harness matrix after the fix
So the cleanup hang no longer reproduced in the live harness matrix I reran. Production-style examples added to the manual workflow
Pushed commit
One note: the converted |
…mers The spawn-timer cleanup (265ee55) now routes the stdout/stderr stream-drain watchdogs through runAgent's injected setTimeoutFn seam, so this test's fake registers three timers, not one. Its single shared `timerCb` let the later drain-timer registrations clobber the wrapper-timeout callback, which then never fired — the hanging spawn wedged the test to its 30s limit. Fire each callback independently (matching the array-based fakes in the agent-spawn suites); no production change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok
|
Great to see the live-harness matrix (opencode/codex/claude) all exit cleanly now — that closes finding 4. Your spawn-timer commit It did trip one CI casualty ( The two spawn fixes are complementary and both needed: yours releases the CLI-path drain/SIGKILL timers, mine ( Generated by Claude Code |
|
Heads-up on the CI red on The failures are all the same class and look environmental, not a hard bug:
Most likely mechanism: these blocked-embedding indexing tests already run near the 30s wall under shard resource pressure, and your I didn't push anything since you're iterating on these exact files and there's no confident one-line fix — a CI re-run may well go green (flaky), or if you want the throughput back, restoring the Bun-native drain for the non-timeout/non-error path while keeping the structured result would remove the added latency. Happy to take either on if you'd like — just say the word. My two spawn-adjacent commits ( Generated by Claude Code |
…write files codex exec defaults to a read-only sandbox that silently blocks file writes. Without --sandbox workspace-write the unit returns 'workspace is read-only' and the engine marks the step complete with no real mutation — a silent no-op. The builder now injects --sandbox workspace-write after exec and any profile-supplied args, unless the profile already pins --sandbox/-s. --ask-for-approval is NOT injected: it only exists on interactive codex, not codex exec (verified via codex exec --help; attempting it errors with 'unexpected argument').
|
Quick note on the Safe to re-run or ignore — your next push will re-trigger CI and it'll almost certainly go green (it was green on Generated by Claude Code |
- configuration-agent-profiles.md: codex now has a dedicated builder (injects --sandbox workspace-write), not 'none — dispatch errors' - akm-workflows-orchestration-plan.md: update capability matrix command shape and drift note to reflect codex builder - pr-714-workflow-validation-repro.md: add codex sandbox notes to Scenario 8 expected interpretation and results sections Also accepted stash proposal 4187b809 updating knowledge:skills/ai-skills/agent-cli-tools/references/codex for codex-cli 0.142.5.
|
Post-merge triage note (0.9.0): PR #714 merged and closed PR-linked issue #559. Remaining open 0.9.0 milestone items are #711, #692, #672 (all still open after sync/main). I did not find any other open 0.9.0 milestone issues. Work split: #711 tasks multi-stash execution, #692 search salience ranking config/outcome gate, #672 improve prep accumulator + salience/outcome repo/widened X4 boundary; none were addressed by #714. |
Summary
Implements the orchestration plan P0 (Workflow Plan Graph compiler) and P1 (native executor) for engine-driven workflow execution. Workflows can now declare per-step orchestration subsections (
### Runner,### Model,### Timeout,### Fan-out,### Schema,### Env,### Depends On) and be executed viaakm workflow runwith automatic unit dispatch, fan-out concurrency, schema-validated structured output with retry, and per-unit persistence.Key changes:
Parser & Schema
parser-orchestration.ts)WorkflowStepOrchestrationtype to schema with runner, model, timeout, fan-out, schema, env, and dependency declarationsDepends OnreferencesIR Compilation (P0)
ir/schema.ts: Backend-agnostic Workflow Plan Graph withIrStepPlan,IrAgentNode,IrMapNode, andIrGateNodeir/compile.ts: Deterministic compiler from parsed workflows to plan graphs; linear workflows compile to agent chains (no behavior change), orchestrated steps compile fan-out into map nodesNative Executor (P1)
native-executor.ts: Executes one step's IR subgraph with fan-out through the scheduler, schema-validated structured output viarunStructured, per-unit persistence, andworkflow_unit_*eventsscheduler.ts: Concurrency policy layer with engine-wide limits (16 cores max, lifetime unit cap per run)unit-writer.ts: Serialized writer queue forworkflow_run_unitsto prevent SQLite contentionrun-workflow.ts: Engine loop that walks the plan graph and advances steps throughcompleteWorkflowStepgateStorage & Persistence
workflow_run_unitstable for per-unit state (runner, model, input hash, result, tokens, failure reason, timestamps)WorkflowRunUnitRowand related types inworkflow-runs-repository.tsSupporting Infrastructure
json-schema.ts: Bounded JSON Schema subset validator for structured output normalizationenv-binding.ts: Extracted env-ref resolution shared betweenakm env runand workflow engineDocumentation & Tests
fablemodel alias (Mythos-class tier)The implementation maintains the gated step spine invariant: every step advances strictly through
completeWorkflowStep, never by direct row writes. Durable-row resume re-dispatches only steps that never completed, using input hashes to avoid double-issuing side-effecting work.Test plan
native-executor.test.tscovering fan-out dispatch, item interpolation, evidence collection, schema validation with retry, and reducer logicparser-orchestration.test.tsfor extended grammar parsingir-compile.test.tsfor plan graph compilationrun-units.test.tsfor unit persistence and writer queuejson-schema-subset.test.tsfor schema validationcheckin-surfacing.test.tsfor check-in rendering fixesChecklist
bun test)bun run lint)https://claude.ai/code/session_018DptAcDcuW2TF2RfM3a4ok