feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/ * *
* DAG Workflow Executor
*
* Executes a ` nodes: ` - based workflow in topological order .
* Independent nodes within the same layer run concurrently via Promise . allSettled .
* Captures all assistant output regardless of streaming mode for $node_id . output substitution .
* /
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
import { execFileAsync } from '@archon/git' ;
feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath (closes #1136) (#1315)
* feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath
Collapses the awkward `~/.archon/.archon/workflows/` convention to a direct
`~/.archon/workflows/` child (matching `workspaces/`, `archon.db`, etc.), adds
home-scoped commands and scripts with the same loading story, and kills the
opt-in `globalSearchPath` parameter so every call site gets home-scope for free.
Closes #1136 (supersedes @jonasvanderhaegen's tactical fix — the bug was the
primitive itself: an easy-to-forget parameter that five of six call sites on
dev dropped).
Primitive changes:
- Home paths are direct children of `~/.archon/`. New helpers in `@archon/paths`:
`getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`,
and `getLegacyHomeWorkflowsPath()` (detection-only for migration).
- `discoverWorkflowsWithConfig(cwd, loadConfig)` reads home-scope internally.
The old `{ globalSearchPath }` option is removed. Chat command handler, Web
UI workflow picker, orchestrator resolve path — all inherit home-scope for
free without maintainer patches at every new site.
- `discoverScriptsForCwd(cwd)` merges home + repo scripts (repo wins on name
collision). dag-executor and validator use it; the hardcoded
`resolve(cwd, '.archon', 'scripts')` single-scope path is gone.
- Command resolution is now walked-by-basename in each scope. `loadCommand`
and `resolveCommand` walk 1 subfolder deep and match by `.md` basename, so
`.archon/commands/triage/review.md` resolves as `review` — closes the
latent bug where subfolder commands were listed but unresolvable.
- All three (`workflows/`, `commands/`, `scripts/`) enforce a 1-level
subfolder cap (matches the existing `defaults/` convention). Deeper
nesting is silently skipped.
- `WorkflowSource` gains `'global'` alongside `'bundled'` and `'project'`.
Web UI node palette shows a dedicated "Global (~/.archon/commands/)"
section; badges updated.
Migration (clean cut — no fallback read):
- First use after upgrade: if `~/.archon/.archon/workflows/` exists, Archon
logs a one-time WARN per process with the exact `mv` command:
`mv ~/.archon/.archon/workflows ~/.archon/workflows && rmdir ~/.archon/.archon`
The legacy path is NOT read — users migrate manually. Rollback caveat
noted in CHANGELOG.
Tests:
- `@archon/paths/archon-paths.test.ts`: new helper tests (default HOME,
ARCHON_HOME override, Docker), plus regression guards for the double-`.archon/`
path.
- `@archon/workflows/loader.test.ts`: home-scoped workflows, precedence,
subfolder 1-depth cap, legacy-path deprecation warning fires exactly once
per process.
- `@archon/workflows/validator.test.ts`: home-scoped commands + subfolder
resolution.
- `@archon/workflows/script-discovery.test.ts`: depth cap + merge semantics
(repo wins, home-missing tolerance).
- Existing CLI + orchestrator tests updated to drop `globalSearchPath`
assertions.
E2E smoke (verified locally, before cleanup):
- `.archon/workflows/e2e-home-scope.yaml` + scratch repo at /tmp
- Home-scoped workflow discovered from an unrelated git repo
- Home-scoped script (`~/.archon/scripts/*.ts`) executes inside a script node
- 1-level subfolder workflow (`~/.archon/workflows/triage/*.yaml`) listed
- Legacy path warning fires with actionable `mv` command; workflows there
are NOT loaded
Docs: `CLAUDE.md`, `docs-web/guides/global-workflows.md` (full rewrite for
three-type scope + subfolder convention + migration), `docs-web/reference/
configuration.md` (directory tree), `docs-web/reference/cli.md`,
`docs-web/guides/authoring-workflows.md`.
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
* test(script-discovery): normalize path separators in mocks for Windows
The 4 new tests in `scanScriptDir depth cap` and `discoverScriptsForCwd —
merge repo + home with repo winning` compared incoming mock paths with
hardcoded forward-slash strings (`if (path === '/scripts/triage')`). On
Windows, `path.join('/scripts', 'triage')` produces `\scripts\triage`, so
those branches never matched, readdir returned `[]`, and the tests failed.
Added a `norm()` helper at module scope and wrapped the incoming `path`
argument in every `mockImplementation` before comparing. Stored paths go
through `normalizeSep()` in production code, so the existing equality
assertions on `script.path` remain OS-independent.
Fixes Windows CI job `test (windows-latest)` on PR #1315.
* address review feedback: home-scope error handling, depth cap, and tests
Critical fixes:
- api.ts: add `maxDepth: 1` to all 3 findMarkdownFilesRecursive calls in
GET /api/commands (bundled/home/project). Without this the UI palette
surfaced commands from deep subfolders that the executor (capped at 1)
could not resolve — silent "command not found" at runtime.
- validator.ts: wrap home-scope findMarkdownFilesRecursive and
resolveCommandInDir calls in try/catch so EACCES/EPERM on
~/.archon/commands/ doesn't crash the validator with a raw filesystem
error. ENOENT still returns [] via the underlying helper.
Error handling fixes:
- workflow-discovery.ts: maybeWarnLegacyHomePath now sets the
"warned-once" flag eagerly before `await access()`, so concurrent
discovery calls (server startup with parallel codebase resolution)
can't double-warn. Non-ENOENT probe errors (EACCES/EPERM) now log at
WARN instead of DEBUG so permission issues on the legacy dir are
visible in default operation.
- dag-executor.ts: wrap discoverScriptsForCwd in its own try/catch so
an EACCES on ~/.archon/scripts/ routes through safeSendMessage /
logNodeError with a dedicated "failed to discover scripts" message
instead of being mis-attributed by the outer catch's
"permission denied (check cwd permissions)" branch.
Tests:
- load-command-prompt.test.ts (new): 6 tests covering the executor's
command resolution hot path — home-scope resolves when repo misses,
repo shadows home, 1-level subfolder resolvable by basename, 2-level
rejected, not-found, empty-file. Runs in its own bun test batch.
- archon-paths.test.ts: add getHomeScriptsPath describe block to match
the existing getHomeCommandsPath / getHomeWorkflowsPath coverage.
Comment clarity:
- workflow-discovery.ts: MAX_DISCOVERY_DEPTH comment now leads with the
actual value (1) before describing what 0 would mean.
- script-discovery.ts: copy the "routing ambiguity" rationale from
MAX_DISCOVERY_DEPTH to MAX_SCRIPT_DISCOVERY_DEPTH.
Cleanup:
- Remove .archon/workflows/e2e-home-scope.yaml — one-off smoke test that
would ship permanently in every project's workflow list. Equivalent
coverage exists in loader.test.ts.
Addresses all blocking and important feedback from the multi-agent
review on PR #1315.
---------
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
2026-04-20 18:45:32 +00:00
import { discoverScriptsForCwd } from './script-discovery' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
import type {
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
IWorkflowPlatform ,
WorkflowMessageMetadata ,
WorkflowConfig ,
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
WorkflowDeps ,
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
} from './deps' ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
import type {
SendQueryOptions ,
NodeConfig ,
ProviderCapabilities ,
TokenUsage ,
} from '@archon/providers/types' ;
2026-04-13 13:10:48 +00:00
import { getProviderCapabilities } from '@archon/providers' ;
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
import type {
DagNode ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
ApprovalNode ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
BashNode ,
CommandNode ,
PromptNode ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
LoopNode ,
2026-04-09 11:48:02 +00:00
ScriptNode ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
NodeOutput ,
TriggerRule ,
WorkflowRun ,
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
EffortLevel ,
ThinkingConfig ,
SandboxSettings ,
2026-03-26 22:07:46 +00:00
} from './schemas' ;
2026-04-09 11:48:02 +00:00
import {
isBashNode ,
isLoopNode ,
isApprovalNode ,
isCancelNode ,
isScriptNode ,
isApprovalContext ,
} from './schemas' ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
import { formatToolCall } from './utils/tool-formatter' ;
import { createLogger } from '@archon/paths' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
import { getWorkflowEventEmitter } from './event-emitter' ;
import { evaluateCondition } from './condition-evaluator' ;
2026-04-13 13:10:48 +00:00
import { inferProviderFromModel , isModelCompatible } from './model-validation' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
import {
logNodeStart ,
logNodeComplete ,
logNodeSkip ,
logNodeError ,
logAssistant ,
logTool ,
logWorkflowComplete ,
logWorkflowError ,
} from './logger' ;
2026-03-17 10:11:35 +00:00
import { withIdleTimeout , STEP_IDLE_TIMEOUT_MS } from './utils/idle-timeout' ;
refactor: extract duplicated helpers across executor, command-handler, cleanup-service (#633)
* refactor(workflows): extract shared execution utils to utils/execution-utils.ts
FATAL_PATTERNS, TRANSIENT_PATTERNS, ErrorType, matchesPattern, classifyError,
substituteWorkflowVariables, buildPromptWithContext, and loadCommandPrompt were
duplicated verbatim between executor.ts and dag-executor.ts. Move them to a
shared module. Each file keeps its own safeSendMessage (intentional divergence:
executor.ts has unknownErrorTracker logic that dag-executor.ts does not).
* refactor(core): extract findRepository helper in command-handler
The four-chained .find() repo matching block was duplicated verbatim in
the codebase-switch and repo-remove cases. Extract to findRepository()
alongside listRepositories() where it belongs.
* refactor(core): extract getRemovalBlocker helper in cleanup-service
The two-step guard (check uncommitted changes, then check conversation
references) was duplicated across four sites in runScheduledCleanup,
cleanupStaleWorktrees, and cleanupMergedWorktrees. Extract to
getRemovalBlocker() which returns the blocker reason string or null.
* fix: address review findings for helper extraction
- getRemovalBlocker returns structured discriminated union instead of
plain string, preserving conversationCount as queryable log field
and restoring per-callsite log levels (info for in-use, warn for
uncommitted changes)
- Unexport internal symbols from execution-utils.ts (FATAL_PATTERNS,
TRANSIENT_PATTERNS, matchesPattern, CONTEXT_VAR_PATTERN_STR, ErrorType)
- Fix module doc: remove incorrect "Rule of Three met" claim, expand
safeSendMessage exclusion note to mention circuit-breaker divergence
- Restore variable inventory in substituteWorkflowVariables JSDoc
- Improve loadCommandPrompt JSDoc to describe validation and search
- Update CLAUDE.md utils/ directory description
* refactor(core): remove dead deprecatedCommands list in command-handler
All commands in the deprecated list (clone, repos, repo, repo-remove,
setcwd, getcwd, etc.) are active case labels in the switch statement
above the default branch — they never reach it. The check was dead code
that falsely documented live commands as deprecated.
2026-03-16 09:40:37 +00:00
import {
classifyError ,
2026-04-02 07:18:30 +00:00
detectCreditExhaustion ,
2026-03-17 10:11:35 +00:00
loadCommandPrompt ,
refactor: extract duplicated helpers across executor, command-handler, cleanup-service (#633)
* refactor(workflows): extract shared execution utils to utils/execution-utils.ts
FATAL_PATTERNS, TRANSIENT_PATTERNS, ErrorType, matchesPattern, classifyError,
substituteWorkflowVariables, buildPromptWithContext, and loadCommandPrompt were
duplicated verbatim between executor.ts and dag-executor.ts. Move them to a
shared module. Each file keeps its own safeSendMessage (intentional divergence:
executor.ts has unknownErrorTracker logic that dag-executor.ts does not).
* refactor(core): extract findRepository helper in command-handler
The four-chained .find() repo matching block was duplicated verbatim in
the codebase-switch and repo-remove cases. Extract to findRepository()
alongside listRepositories() where it belongs.
* refactor(core): extract getRemovalBlocker helper in cleanup-service
The two-step guard (check uncommitted changes, then check conversation
references) was duplicated across four sites in runScheduledCleanup,
cleanupStaleWorktrees, and cleanupMergedWorktrees. Extract to
getRemovalBlocker() which returns the blocker reason string or null.
* fix: address review findings for helper extraction
- getRemovalBlocker returns structured discriminated union instead of
plain string, preserving conversationCount as queryable log field
and restoring per-callsite log levels (info for in-use, warn for
uncommitted changes)
- Unexport internal symbols from execution-utils.ts (FATAL_PATTERNS,
TRANSIENT_PATTERNS, matchesPattern, CONTEXT_VAR_PATTERN_STR, ErrorType)
- Fix module doc: remove incorrect "Rule of Three met" claim, expand
safeSendMessage exclusion note to mention circuit-breaker divergence
- Restore variable inventory in substituteWorkflowVariables JSDoc
- Improve loadCommandPrompt JSDoc to describe validation and search
- Update CLAUDE.md utils/ directory description
* refactor(core): remove dead deprecatedCommands list in command-handler
All commands in the deprecated list (clone, repos, repo, repo-remove,
setcwd, getcwd, etc.) are active case labels in the switch statement
above the default branch — they never reach it. The check was dead code
that falsely documented live commands as deprecated.
2026-03-16 09:40:37 +00:00
substituteWorkflowVariables ,
buildPromptWithContext ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
detectCompletionSignal ,
stripCompletionTags ,
2026-04-09 11:48:02 +00:00
isInlineScript ,
2026-03-17 10:11:35 +00:00
} from './executor-shared' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/** Lazy-initialized logger (deferred so test mocks can intercept createLogger) */
let cachedLog : ReturnType < typeof createLogger > | undefined ;
function getLog ( ) : ReturnType < typeof createLogger > {
if ( ! cachedLog ) cachedLog = createLogger ( 'workflow.dag-executor' ) ;
return cachedLog ;
}
2026-04-06 15:59:36 +00:00
/** Workflow-level Claude SDK options — per-node overrides take precedence via ?? */
interface WorkflowLevelOptions {
effort? : EffortLevel ;
thinking? : ThinkingConfig ;
fallbackModel? : string ;
betas? : string [ ] ;
sandbox? : SandboxSettings ;
}
2026-04-06 16:39:18 +00:00
/** Internal node execution result — extends NodeOutput with cost data for aggregation. */
type NodeExecutionResult = NodeOutput & { costUsd? : number } ;
2026-04-01 09:14:45 +00:00
/** Throttle state for cancel checks (reads — no write contention in WAL mode) */
const lastNodeCancelCheck = new Map < string , number > ( ) ;
const CANCEL_CHECK_INTERVAL_MS = 10 _000 ;
/** Throttle state for activity heartbeat writes (only used for stale/zombie detection) */
2026-03-14 18:25:35 +00:00
const lastNodeActivityUpdate = new Map < string , number > ( ) ;
2026-04-01 09:14:45 +00:00
const ACTIVITY_HEARTBEAT_INTERVAL_MS = 60 _000 ;
2026-03-14 18:25:35 +00:00
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/** Context for platform message sending */
interface SendMessageContext {
workflowId? : string ;
nodeName? : string ;
}
2026-03-14 16:04:40 +00:00
/** Default DAG node retry for TRANSIENT errors */
const DEFAULT_NODE_MAX_RETRIES = 2 ;
const DEFAULT_NODE_RETRY_DELAY_MS = 3000 ;
/ * *
* Get effective retry config for a DAG node .
* /
function getEffectiveNodeRetryConfig ( node : DagNode ) : {
maxRetries : number ;
delayMs : number ;
onError : 'transient' | 'all' ;
} {
if ( 'retry' in node && node . retry ) {
return {
maxRetries : node.retry.max_attempts ,
delayMs : node.retry.delay_ms ? ? DEFAULT_NODE_RETRY_DELAY_MS ,
onError : node.retry.on_error ? ? 'transient' ,
} ;
}
return {
maxRetries : DEFAULT_NODE_MAX_RETRIES ,
delayMs : DEFAULT_NODE_RETRY_DELAY_MS ,
onError : 'transient' ,
} ;
}
/ * *
2026-03-14 16:49:30 +00:00
* Check if a NodeOutput failure is transient by delegating to classifyError .
* FATAL patterns ( auth , permission , credits ) take priority over TRANSIENT patterns ,
* matching the same precedence rules as classifyError ( ) . This prevents an error
* message that contains both a FATAL substring and a TRANSIENT substring ( e . g .
* "unauthorized: process exited with code 1" ) from being silently retried .
2026-03-14 16:04:40 +00:00
* /
function isTransientNodeError ( errorMessage : string ) : boolean {
2026-03-14 16:49:30 +00:00
return classifyError ( new Error ( errorMessage ) ) === 'TRANSIENT' ;
2026-03-14 16:04:40 +00:00
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/ * *
* Safely send a message to the platform without crashing on failure .
* Returns true if message was sent successfully , false otherwise .
* /
async function safeSendMessage (
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
platform : IWorkflowPlatform ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
conversationId : string ,
message : string ,
context? : SendMessageContext ,
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
metadata? : WorkflowMessageMetadata
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
) : Promise < boolean > {
try {
await platform . sendMessage ( conversationId , message , metadata ) ;
return true ;
} catch ( error ) {
const err = error as Error ;
const errorType = classifyError ( err ) ;
getLog ( ) . error (
{
err ,
conversationId ,
messageLength : message.length ,
errorType ,
platformType : platform.getPlatformType ( ) ,
. . . context ,
} ,
'dag_node_message_send_failed'
) ;
if ( errorType === 'FATAL' ) {
throw new Error ( ` Platform authentication/permission error: ${ err . message } ` ) ;
}
return false ;
}
}
2026-03-13 07:29:39 +00:00
/ * *
* Single - quote a string for safe inline shell use .
* Replaces each ' with ' \ '' ( end quote , literal single - quote , re - open quote ) .
* /
function shellQuote ( value : string ) : string {
return ` ' ${ value . replaceAll ( "'" , "'\\''" ) } ' ` ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/ * *
* Substitute $node_id . output and $node_id . output . field references in a prompt .
* Called AFTER the standard substituteWorkflowVariables pass .
2026-03-13 07:29:39 +00:00
*
* @param escapedForBash - When true , wraps substituted values in single quotes so
* they are safe to embed in bash scripts passed to ` bash -c ` . Set true only for
* bash node script substitution ; AI / command prompt substitution should use false .
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
* /
export function substituteNodeOutputRefs (
prompt : string ,
2026-03-13 07:29:39 +00:00
nodeOutputs : Map < string , NodeOutput > ,
escapedForBash = false
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
) : string {
return prompt . replace (
/\$([a-zA-Z_][a-zA-Z0-9_-]*)\.output(?:\.([a-zA-Z_][a-zA-Z0-9_]*))?/g ,
2026-03-13 07:31:21 +00:00
( match , nodeId : string , field : string | undefined ) = > {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const nodeOutput = nodeOutputs . get ( nodeId ) ;
2026-03-13 07:31:21 +00:00
if ( ! nodeOutput ) {
getLog ( ) . warn ( { nodeId , match } , 'dag_node_output_ref_unknown_node' ) ;
return escapedForBash ? "''" : '' ;
}
2026-03-13 07:29:39 +00:00
if ( ! field ) {
return escapedForBash ? shellQuote ( nodeOutput . output ) : nodeOutput . output ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
try {
const parsed = JSON . parse ( nodeOutput . output ) as Record < string , unknown > ;
const value = parsed [ field ] ;
2026-03-13 07:29:39 +00:00
if ( typeof value === 'string' ) return escapedForBash ? shellQuote ( value ) : value ;
// numbers and booleans from JSON.parse are shell-safe without quoting:
// JSON disallows NaN/Infinity, so String(number) contains only digits, sign, and '.'.
// String(boolean) is 'true' or 'false' — no shell metacharacters.
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( typeof value === 'number' || typeof value === 'boolean' ) return String ( value ) ;
2026-03-13 07:29:39 +00:00
return escapedForBash ? "''" : '' ; // objects, null, undefined, symbol, bigint → empty
2026-03-16 09:08:35 +00:00
} catch ( jsonErr ) {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
getLog ( ) . warn (
2026-03-16 09:08:35 +00:00
{ nodeId , field , outputPreview : nodeOutput.output.slice ( 0 , 100 ) , err : jsonErr as Error } ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
'dag_node_output_ref_json_parse_failed'
) ;
2026-03-13 07:29:39 +00:00
return escapedForBash ? "''" : '' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
}
) ;
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// buildSDKHooksFromYAML moved to @archon/providers/src/claude/provider.ts
// loadMcpConfig moved to @archon/providers/src/claude/provider.ts
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/ * *
* Resolve per - node provider and model .
* Node - level overrides take precedence over workflow defaults .
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
*
* Provider - agnostic : builds universal base options + raw nodeConfig .
* The provider internally translates nodeConfig to SDK - specific options .
* Capability warnings inform users when features are unsupported .
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
* /
async function resolveNodeProviderAndModel (
node : DagNode ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
workflowProvider : string ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
workflowModel : string | undefined ,
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
config : WorkflowConfig ,
platform : IWorkflowPlatform ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
conversationId : string ,
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
workflowRunId : string ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
_cwd : string ,
2026-04-13 13:10:48 +00:00
workflowLevelOptions : WorkflowLevelOptions
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
) : Promise < {
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
provider : string ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
model : string | undefined ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
options : SendQueryOptions | undefined ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} > {
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
const provider : string = node . provider ? ? inferProviderFromModel ( node . model , workflowProvider ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
const providerAssistantConfig = config . assistants [ provider ] ;
const model : string | undefined =
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
node . model ? ?
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
( provider === workflowProvider
? workflowModel
: ( providerAssistantConfig ? . model as string | undefined ) ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( ! isModelCompatible ( provider , model ) ) {
throw new Error (
` Node ' ${ node . id } ': model " ${ model ? ? 'default' } " is not compatible with provider " ${ provider } " `
) ;
}
2026-04-13 13:10:48 +00:00
// Get provider capabilities for capability warnings (static lookup, no instantiation)
const caps = getProviderCapabilities ( provider ) ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Capability warnings — inform users when features are unsupported
const capChecks : [ string , keyof ProviderCapabilities , boolean ] [ ] = [
[
'allowed_tools/denied_tools' ,
'toolRestrictions' ,
node . allowed_tools !== undefined || node . denied_tools !== undefined ,
] ,
[ 'hooks' , 'hooks' , node . hooks !== undefined ] ,
[ 'mcp' , 'mcp' , node . mcp !== undefined ] ,
[ 'skills' , 'skills' , node . skills !== undefined && node . skills . length > 0 ] ,
2026-04-19 06:16:01 +00:00
[ 'agents' , 'agents' , node . agents !== undefined ] ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
[ 'effort' , 'effortControl' , ( node . effort ? ? workflowLevelOptions . effort ) !== undefined ] ,
[ 'thinking' , 'thinkingControl' , ( node . thinking ? ? workflowLevelOptions . thinking ) !== undefined ] ,
[ 'maxBudgetUsd' , 'costControl' , node . maxBudgetUsd !== undefined ] ,
[
'fallbackModel' ,
'fallbackModel' ,
( node . fallbackModel ? ? workflowLevelOptions . fallbackModel ) !== undefined ,
] ,
[ 'sandbox' , 'sandbox' , ( node . sandbox ? ? workflowLevelOptions . sandbox ) !== undefined ] ,
2026-04-13 12:21:57 +00:00
[ 'env' , 'envInjection' , ( config . envVars && Object . keys ( config . envVars ) . length > 0 ) === true ] ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
] ;
const unsupported : string [ ] = [ ] ;
for ( const [ field , cap , isSet ] of capChecks ) {
if ( isSet && ! caps [ cap ] ) {
unsupported . push ( field ) ;
feat: per-node and per-step tool restrictions (allowed_tools, denied_tools) (#454)
* feat: add per-node and per-step tool restrictions (allowed_tools, denied_tools)
Add `allowed_tools` (whitelist) and `denied_tools` (blacklist) fields to DAG
nodes and sequential steps, enforced at the Claude SDK level via Options.tools
and Options.disallowedTools.
- Extend AssistantRequestOptions with disallowedTools field
- Add allowed_tools/denied_tools to DagNodeBase and SingleStep types
- Parse and validate the arrays in parseDagNode and parseSingleStep
- Spread disallowedTools into Claude SDK Options in claude.ts
- Apply per-node restrictions in resolveNodeProviderAndModel (dag-executor)
- Merge per-step restrictions in executeStepInternal (executor)
- Emit Codex warning when these fields are set (unsupported per-call)
- Preserve empty allowed_tools: [] distinct from absent (disables all tools)
* docs: document allowed_tools and denied_tools for workflow tool restrictions
Add allowed_tools/denied_tools to the Step Options and Node Fields tables
in authoring-workflows.md, add a dedicated section with examples, update
the Summary list, and add a brief mention in README.md and CLAUDE.md.
* refactor: extract parseToolList helper, simplify tool restriction parsing
Extract duplicated allowed_tools/denied_tools parsing logic from
parseSingleStep and parseDagNode into a shared parseToolList helper.
Replaces nested ternaries and IIFE patterns with straightforward
conditionals. Simplify stepOptions construction in executor.ts to
use explicit if-statements instead of nested spreads.
* fix: address PR review issues in tool restriction validation and delivery
- parseSingleStep: add error propagation for non-array allowed_tools/denied_tools (return null on failure)
- parseDagNode: return null when tool field validation adds errors (errorsBeforeToolFields guard)
- parseToolList: warn on non-string entries for steps (id always passed now)
- Add warning when denied_tools is set alongside allowed_tools: []
- executor.ts: make Codex and Claude paths mutually exclusive (no dead tool fields built for Codex)
- dag-executor.ts: check safeSendMessage return for both output_format and tool restriction warnings, log error on delivery failure
- Add tests: denied_tools-only Codex warning, both tools on same step, merged options with model, DAG execution-level tool restriction tests
2026-02-19 12:48:27 +00:00
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
if ( unsupported . length > 0 ) {
getLog ( ) . warn ( { nodeId : node.id , provider , unsupported } , 'dag.unsupported_capabilities' ) ;
2026-03-16 09:08:35 +00:00
const delivered = await safeSendMessage (
platform ,
conversationId ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
` Warning: Node ' ${ node . id } ' uses ${ unsupported . join ( ', ' ) } but ${ provider } doesn't support ${ unsupported . length === 1 ? 'it' : 'them' } — ${ unsupported . length === 1 ? 'this will be' : 'these will be' } ignored. ` ,
2026-03-16 09:08:35 +00:00
{ workflowId : workflowRunId , nodeName : node.id }
) ;
if ( ! delivered ) {
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
getLog ( ) . error ( { nodeId : node.id , workflowRunId } , 'dag.capability_warning_delivery_failed' ) ;
2026-03-16 09:08:35 +00:00
}
}
2026-04-19 06:16:01 +00:00
// Surface agents + skills ID collision — user-defined 'dag-node-skills'
// silently overrides Archon's skills wrapper. User wins (by design) but
// the operator should know they've neutered the wrapper.
if (
node . agents ? . [ 'dag-node-skills' ] !== undefined &&
node . skills !== undefined &&
node . skills . length > 0
) {
getLog ( ) . warn ( { nodeId : node.id } , 'dag.agents_skills_id_collision' ) ;
await safeSendMessage (
platform ,
conversationId ,
` Warning: Node ' ${ node . id } ' defines an agent with reserved ID 'dag-node-skills' AND uses 'skills:'. Your inline agent overrides Archon's automatic skills wrapper — the 'skills:' field will NOT take effect. Rename the agent or remove 'skills:' to fix. ` ,
{ workflowId : workflowRunId , nodeName : node.id }
) ;
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Build universal base options
const baseOptions : SendQueryOptions = { } ;
if ( model ) baseOptions . model = model ;
if ( config . envVars && Object . keys ( config . envVars ) . length > 0 ) {
baseOptions . env = config . envVars ;
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
if ( node . systemPrompt !== undefined ) baseOptions . systemPrompt = node . systemPrompt ;
if ( node . maxBudgetUsd !== undefined ) baseOptions . maxBudgetUsd = node . maxBudgetUsd ;
const fb = node . fallbackModel ? ? workflowLevelOptions . fallbackModel ;
if ( fb ) baseOptions . fallbackModel = fb ;
if ( node . output_format ) {
baseOptions . outputFormat = { type : 'json_schema' , schema : node.output_format } ;
2026-03-17 07:21:47 +00:00
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Build raw nodeConfig — provider translates internally
const nodeConfig : NodeConfig = {
mcp : node.mcp ,
hooks : node.hooks ,
skills : node.skills ,
2026-04-19 06:16:01 +00:00
agents : node.agents ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
allowed_tools : node.allowed_tools ,
denied_tools : node.denied_tools ,
effort : node.effort ? ? workflowLevelOptions . effort ,
thinking : node.thinking ? ? workflowLevelOptions . thinking ,
sandbox : node.sandbox ? ? workflowLevelOptions . sandbox ,
betas : node.betas ? ? workflowLevelOptions . betas ,
output_format : node.output_format ,
maxBudgetUsd : node.maxBudgetUsd ,
systemPrompt : node.systemPrompt ,
fallbackModel : fb ,
} ;
2026-03-17 07:21:47 +00:00
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Pass assistantConfig from config — provider parses internally
const assistantConfig = config . assistants [ provider ] ? ? { } ;
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
const options : SendQueryOptions = {
. . . baseOptions ,
nodeConfig ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
assistantConfig ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
} ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
return { provider , model , options } ;
}
/** Evaluate trigger rule for a node given its upstream states */
export function checkTriggerRule (
node : DagNode ,
nodeOutputs : Map < string , NodeOutput >
) : 'run' | 'skip' {
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
const nodeDeps = node . depends_on ? ? [ ] ;
if ( nodeDeps . length === 0 ) return 'run' ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
const upstreams = nodeDeps . map (
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
id = >
nodeOutputs . get ( id ) ? ?
( {
state : 'failed' ,
output : '' ,
error : ` upstream ' ${ id } ' missing from outputs ` ,
} as NodeOutput )
) ;
const rule : TriggerRule = node . trigger_rule ? ? 'all_success' ;
switch ( rule ) {
case 'all_success' :
return upstreams . every ( u = > u . state === 'completed' ) ? 'run' : 'skip' ;
case 'one_success' :
return upstreams . some ( u = > u . state === 'completed' ) ? 'run' : 'skip' ;
case 'none_failed_min_one_success' : {
const anyFailed = upstreams . some ( u = > u . state === 'failed' ) ;
const anySucceeded = upstreams . some ( u = > u . state === 'completed' ) ;
return ! anyFailed && anySucceeded ? 'run' : 'skip' ;
}
case 'all_done' :
return upstreams . every ( u = > u . state !== 'pending' && u . state !== 'running' ) ? 'run' : 'skip' ;
}
}
/ * *
* Build topological layers from DAG nodes using Kahn ' s algorithm .
* Layer 0 : nodes with no dependencies .
* Layer N : nodes whose dependencies are all in layers 0 . . N - 1 .
*
* Cycle detection : if the sum of all layer sizes < nodes . length , a cycle exists .
* ( Cycle detection at load time is the primary guard ; this is a runtime safety check . )
* /
export function buildTopologicalLayers ( nodes : readonly DagNode [ ] ) : DagNode [ ] [ ] {
const inDegree = new Map < string , number > ( ) ;
const dependents = new Map < string , string [ ] > ( ) ;
for ( const node of nodes ) {
inDegree . set ( node . id , node . depends_on ? . length ? ? 0 ) ;
for ( const dep of node . depends_on ? ? [ ] ) {
const existing = dependents . get ( dep ) ? ? [ ] ;
existing . push ( node . id ) ;
dependents . set ( dep , existing ) ;
}
}
const layers : DagNode [ ] [ ] = [ ] ;
let ready = [ . . . nodes ] . filter ( n = > ( inDegree . get ( n . id ) ? ? 0 ) === 0 ) ;
while ( ready . length > 0 ) {
layers . push ( ready ) ;
const nextIds : string [ ] = [ ] ;
for ( const node of ready ) {
for ( const depId of dependents . get ( node . id ) ? ? [ ] ) {
const newDegree = ( inDegree . get ( depId ) ? ? 0 ) - 1 ;
inDegree . set ( depId , newDegree ) ;
if ( newDegree === 0 ) nextIds . push ( depId ) ;
}
}
ready = nextIds
. map ( id = > nodes . find ( n = > n . id === id ) )
. filter ( ( n ) : n is DagNode = > n !== undefined ) ;
}
const totalPlaced = layers . reduce ( ( sum , l ) = > sum + l . length , 0 ) ;
if ( totalPlaced < nodes . length ) {
// Should never happen — cycle detection runs at load time
throw new Error (
'[DagExecutor] Cycle detected at runtime — was cycle detection skipped at load?'
) ;
}
return layers ;
}
/ * *
2026-04-06 17:03:47 +00:00
* Execute a single DAG node . Returns NodeExecutionResult regardless of success / failure .
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
* Always accumulates assistant text output ( for $node_id . output substitution ) .
* Parallel nodes and context : 'fresh' nodes always receive fresh sessions ( caller ensures resumeSessionId is undefined ) .
* /
async function executeNodeInternal (
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
conversationId : string ,
cwd : string ,
workflowRun : WorkflowRun ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
node : CommandNode | PromptNode ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
provider : string ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
nodeOptions : SendQueryOptions | undefined ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
2026-04-06 13:26:59 +00:00
docsDir : string ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
nodeOutputs : Map < string , NodeOutput > ,
resumeSessionId : string | undefined ,
configuredCommandFolder? : string ,
issueContext? : string
2026-04-06 16:39:18 +00:00
) : Promise < NodeExecutionResult > {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const nodeStartTime = Date . now ( ) ;
const nodeContext : SendMessageContext = { workflowId : workflowRun.id , nodeName : node.id } ;
getLog ( ) . info ( { nodeId : node.id , provider } , 'dag_node_started' ) ;
await logNodeStart ( logDir , workflowRun . id , node . id , node . command ? ? '<inline>' ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_started' ,
step_name : node.id ,
data : { command : node.command ? ? null , provider } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_started' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_started' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
} ) ;
// Load prompt
let rawPrompt : string ;
if ( node . command !== undefined ) {
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
const promptResult = await loadCommandPrompt ( deps , cwd , node . command , configuredCommandFolder ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( ! promptResult . success ) {
const errMsg = promptResult . message ;
getLog ( ) . error ( { nodeId : node.id , error : errMsg } , 'dag_node_command_load_failed' ) ;
await logNodeError ( logDir , workflowRun . id , node . id , errMsg ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : errMsg } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ,
error : errMsg ,
} ) ;
return { state : 'failed' , output : '' , error : errMsg } ;
}
rawPrompt = promptResult . content ;
} else {
// node is PromptNode — prompt: string is guaranteed by the discriminated union
rawPrompt = node . prompt ;
}
// Standard variable substitution
2026-03-16 13:05:16 +00:00
let substitutedPrompt : string ;
try {
substitutedPrompt = buildPromptWithContext (
rawPrompt ,
workflowRun . id ,
workflowRun . user_message ,
artifactsDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
2026-03-16 13:05:16 +00:00
issueContext ,
` dag node ' ${ node . id } ' prompt `
) ;
} catch ( error ) {
const err = error as Error ;
2026-03-17 07:21:47 +00:00
getLog ( ) . error ( { nodeId : node.id , error : err.message } , 'dag.node_prompt_substitution_failed' ) ;
await safeSendMessage (
platform ,
conversationId ,
` Node ' ${ node . id } ' failed: ${ err . message } ` ,
nodeContext
) ;
2026-03-16 13:05:16 +00:00
return { state : 'failed' , output : '' , error : err.message } ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
// Substitute upstream node output references
const finalPrompt = substituteNodeOutputRefs ( substitutedPrompt , nodeOutputs ) ;
2026-04-12 10:11:21 +00:00
const aiClient = deps . getAgentProvider ( provider ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const streamingMode = platform . getStreamingMode ( ) ;
let nodeOutputText = '' ; // Always accumulate regardless of streaming mode
2026-03-12 10:38:21 +00:00
let structuredOutput : unknown ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
let newSessionId : string | undefined ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
let nodeTokens : TokenUsage | undefined ;
2026-04-06 16:39:18 +00:00
let nodeCostUsd : number | undefined ;
let nodeStopReason : string | undefined ;
let nodeNumTurns : number | undefined ;
let nodeModelUsage : Record < string , unknown > | undefined ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const batchMessages : string [ ] = [ ] ;
2026-03-12 00:24:33 +00:00
// Create per-node abort controller for idle timeout cleanup
const nodeAbortController = new AbortController ( ) ;
2026-03-25 23:06:23 +00:00
// Fork when resuming — leaves the source session untouched so retries are safe.
const shouldForkSession = resumeSessionId !== undefined ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
const nodeOptionsWithAbort : SendQueryOptions | undefined = {
2026-03-12 00:24:33 +00:00
. . . nodeOptions ,
abortSignal : nodeAbortController.signal ,
2026-03-25 23:06:23 +00:00
. . . ( shouldForkSession ? { forkSession : true } : { } ) ,
2026-03-12 00:24:33 +00:00
} ;
let nodeIdleTimedOut = false ;
feat: add archon-validate-pr workflow + per-node idle_timeout (#635)
* fix(sqlite): reorder params to match $N placeholder positions
The SQLite adapter's convertPlaceholders naively replaced $N with ?
but didn't reorder the params array. PostgreSQL uses explicit $N
indices so param order doesn't matter, but SQLite's ? is positional.
This caused failWorkflowRun to swap the WHERE id and metadata params,
silently failing to update workflow status from 'running' to 'failed'.
Also changed SQLite dialect helpers (jsonMerge, jsonArrayContains,
nowMinusDays) to emit $N placeholders instead of raw ? so all
parameter handling goes through convertPlaceholders consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PRs and junk artifacts in workflow runs
- Add --resume CLI flag to retry failed workflows from the failed step
instead of starting from scratch (reuses existing worktree and PR)
- Add findLastFailedRun() query matching on (workflow_name, codebase_id)
so resume works across CLI invocations with different conversation IDs
- Add pre-flight PR dedup check to archon-create-pr command (searches
for existing open PRs before creating duplicates)
- Add .gitignore patterns for *.db-shm, *.db-wal, *.db-journal, undefined/
- Fix setup.test.ts env var restoration that created literal undefined/ dirs
- Add defensive check in getArchonHome() for string "undefined"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add archon-validate-pr workflow + per-node idle_timeout
Add a DAG workflow for thorough PR validation (code review + E2E browser
testing on both main and feature branches). Also add per-node/per-step
idle_timeout override to the workflow engine so long-running nodes like
E2E tests aren't killed by the default 5-minute idle timeout.
Key changes:
- New archon-validate-pr workflow with 6 command files
- idle_timeout field on DagNodeBase and SingleStep types
- Loader validation for idle_timeout (positive number)
- DAG executor and step executor use node.idle_timeout ?? default
- Cross-platform port detection (bun -e instead of /dev/tcp)
- DAG restructured to limit concurrent Claude processes
- Strengthened cleanup-processes node with pkill fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings — validation, resume bugs, fail-fast
- Add isFinite() check to idle_timeout validation at all 3 loader sites
(prevents Infinity from passing via YAML .inf literal)
- Guard --resume against step_index=0 (nothing to resume) to prevent
zombie running runs in the database
- Change executor startFromStep guard from > 0 to >= 0 so pre-created
runs are always honored
- Change findLastFailedRun ORDER BY from nullable completed_at to
non-null started_at (consistent with findResumableRun)
- Add existsSync check on resume working_path before reusing it
- Make getArchonHome() throw on literal "undefined" env var instead of
silently falling back (fail-fast per CLAUDE.md)
- Add Infinity rejection test for idle_timeout loader validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: workflow race conditions and robustness improvements
- Serialize code reviews: feature review now depends on main review,
guaranteeing cross-reference artifact is available (was race condition)
- Replace git reset --hard on canonical repo with isolated worktree
for main branch E2E testing (safe for concurrent validation runs)
- Replace fixed sleep + ungated curl with polling retry loops (60s max)
for both backend and frontend startup in both E2E commands
- Server output now logged to artifact files instead of /dev/null
for debuggability when startup fails
- Replace dead .classify-testability-output file read with
$nodeId.output.field substitution (executor injects values directly)
- Add worktree cleanup to both E2E main command and cleanup-processes
safety net node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:43:24 +00:00
const effectiveIdleTimeout = node . idle_timeout ? ? STEP_IDLE_TIMEOUT_MS ;
fix: loading indicator race condition and workflow tool call durations (#654, #655) (#657)
* fix: preserve loading indicator on first message race condition (#654)
When the first message is sent, navigate() triggers a component remount
and a fresh SSE connection. Text events emitted before the connection
established were missed, leaving only the lock-release event, which
previously cleared isStreaming on empty thinking placeholders — making
the pulsing dots disappear before any AI text arrived.
Changes:
- Extract mapMessageRow() as a module-level helper (reused in re-fetch)
- Add conversationIdRef for stable access inside zero-dep onLockChange
- Fix needsStreamFix to skip empty placeholders (isStreaming && !!content)
- On lock release with a stuck placeholder, re-fetch completed messages
via REST to populate the placeholder with actual AI response content
- Remove redundant setSendInFlight(false) from onLockChange (handleSend
finally block already handles this after apiSendMessage returns)
Fixes #654
* fix: tool call cards always show 0ms duration in workflow logs (#655)
The workflow executors only persisted tool_called events with no timing
data. WorkflowLogs.tsx hardcoded duration: 0 when hydrating from the DB.
Changes:
- Add tool_completed to WorkflowEventType union in store.ts
- Emit tool_completed with duration_ms in executor.ts (sequential and loop)
- Emit tool_completed with duration_ms in dag-executor.ts (DAG nodes)
- Add duration? field to ToolEvent interface in WorkflowExecution.tsx
- Match tool_called/tool_completed events by name+time to compute duration
- Use te.duration instead of hardcoded 0 in WorkflowLogs.tsx
- Add tests for tool_completed emission in executor and dag-executor
Fixes #655
* fix: address review findings for PR #657
Fixed:
- HIGH: WorkflowLogs regression — loop workflow tool_completed events now emitted
in executeLoopWorkflow (Option A), and duration fallback `?? 0` added to
WorkflowLogs.tsx to prevent isRunning spinner for any remaining undefined cases
- LOW: Missing .catch() on stuck-placeholder re-fetch in ChatInterface — failure
now clears the stuck placeholder so user can retry
- LOW: Restore console.warn in mapMessageRow catch block — corrupted metadata
parse failures no longer silently swallowed
Skipped (YAGNI/out-of-scope):
- (none — all findings were real bugs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add .claude/worktrees to eslint ignores
ESLint was discovering files in .claude/worktrees/ (created by Claude
Code worktree sessions) and failing because they lack proper tsconfig
scope. The existing 'worktrees/**' pattern only matches top-level
worktrees, not nested .claude/worktrees.
* fix: address code review findings from PR #657 second review
- Fix Finding 1 (LOW/bug): Add setSendInFlight(false) before getMessages()
call in the race-condition recovery path (hasStuckPlaceholder=true). AI
processing is done when the lock releases, so the guard should be cleared
regardless of whether the REST re-fetch succeeds or fails. Without this fix,
a navigate-away/remount on the next message would incorrectly preserve any
isStreaming placeholder from the DB.
- Fix Finding 2 (LOW/style): Add clarifying comment to usedCompleted Set in
WorkflowExecution.tsx useMemo explaining the intentional local mutation
pattern inside .map().
- Skip Finding 3 (step_index on DAG tool_completed): Recommended as out-of-scope
by reviewer; DAG tool_called events also omit step_index by design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Auto-commit workflow artifacts (archon-assist)
* fix: workflow logs spinner/duration and second message crash
Bug 1 - WorkflowLogs tool cards show 0ms with no spinner:
- Remove ?? 0 fallback in hydrateMessages that coerced undefined (running)
to 0 (completed), killing the spinner and ticking elapsed counter
- Create assistant message in onToolCall when tools arrive before text,
so tool cards have a home in the SSE message list and participate in
the DB/SSE merge that preserves running state
Bug 2 - Second message in chat crashes/breaks:
- Move setSendInFlight(false) to be unconditional on lock release in
onLockChange, not gated behind hasStuckPlaceholder. When a workflow
dispatch replaces the thinking placeholder with status text,
hasStuckPlaceholder was false and the flag stayed true, causing ghost
streaming messages and disabled input on subsequent sends.
* fix: duplicate tool cards in workflow logs + Claude Code crash on 2nd message
Bug 1 - Duplicate tool cards when workflow completes:
The sseBySig Map merge logic used role:content as key, which collided
for multiple messages with content: '' (tool-only turns). One SSE entry
got grafted onto multiple DB messages, duplicating tool cards.
Fix: use DB messages exclusively once workflow stops running. SSE merge
is only needed during live execution for spinner state.
Bug 2 - Claude Code subprocess crashes on second message:
persistSession: false (claude.ts:253) discarded the session transcript.
When the second message tried to resume the session, the subprocess
couldn't find the transcript file and exited with code 1.
Fix: remove persistSession: false default so transcripts are written
and the resume mechanism works on subsequent messages.
* fix: eliminate duplicate tool cards during live workflow execution
Root cause: During live execution, both DB-polled messages and SSE-streamed
messages were shown together. When SSE text was still accumulating (partial
content) while DB had the full persisted content, signatures differed and
both appeared — causing duplicate tool cards and doubled text.
Fix: While running, treat SSE as the live source of truth. Only prepend
DB messages from before the SSE session started (older step messages).
After completion, switch to DB-only view. This cleanly separates the
live streaming path from the persisted history path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:00:05 +00:00
let lastToolStartedAt : { toolName : string ; startedAt : number } | null = null ;
2026-03-12 00:24:33 +00:00
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
try {
2026-03-12 00:24:33 +00:00
for await ( const msg of withIdleTimeout (
aiClient . sendQuery ( finalPrompt , cwd , resumeSessionId , nodeOptionsWithAbort ) ,
feat: add archon-validate-pr workflow + per-node idle_timeout (#635)
* fix(sqlite): reorder params to match $N placeholder positions
The SQLite adapter's convertPlaceholders naively replaced $N with ?
but didn't reorder the params array. PostgreSQL uses explicit $N
indices so param order doesn't matter, but SQLite's ? is positional.
This caused failWorkflowRun to swap the WHERE id and metadata params,
silently failing to update workflow status from 'running' to 'failed'.
Also changed SQLite dialect helpers (jsonMerge, jsonArrayContains,
nowMinusDays) to emit $N placeholders instead of raw ? so all
parameter handling goes through convertPlaceholders consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PRs and junk artifacts in workflow runs
- Add --resume CLI flag to retry failed workflows from the failed step
instead of starting from scratch (reuses existing worktree and PR)
- Add findLastFailedRun() query matching on (workflow_name, codebase_id)
so resume works across CLI invocations with different conversation IDs
- Add pre-flight PR dedup check to archon-create-pr command (searches
for existing open PRs before creating duplicates)
- Add .gitignore patterns for *.db-shm, *.db-wal, *.db-journal, undefined/
- Fix setup.test.ts env var restoration that created literal undefined/ dirs
- Add defensive check in getArchonHome() for string "undefined"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add archon-validate-pr workflow + per-node idle_timeout
Add a DAG workflow for thorough PR validation (code review + E2E browser
testing on both main and feature branches). Also add per-node/per-step
idle_timeout override to the workflow engine so long-running nodes like
E2E tests aren't killed by the default 5-minute idle timeout.
Key changes:
- New archon-validate-pr workflow with 6 command files
- idle_timeout field on DagNodeBase and SingleStep types
- Loader validation for idle_timeout (positive number)
- DAG executor and step executor use node.idle_timeout ?? default
- Cross-platform port detection (bun -e instead of /dev/tcp)
- DAG restructured to limit concurrent Claude processes
- Strengthened cleanup-processes node with pkill fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings — validation, resume bugs, fail-fast
- Add isFinite() check to idle_timeout validation at all 3 loader sites
(prevents Infinity from passing via YAML .inf literal)
- Guard --resume against step_index=0 (nothing to resume) to prevent
zombie running runs in the database
- Change executor startFromStep guard from > 0 to >= 0 so pre-created
runs are always honored
- Change findLastFailedRun ORDER BY from nullable completed_at to
non-null started_at (consistent with findResumableRun)
- Add existsSync check on resume working_path before reusing it
- Make getArchonHome() throw on literal "undefined" env var instead of
silently falling back (fail-fast per CLAUDE.md)
- Add Infinity rejection test for idle_timeout loader validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: workflow race conditions and robustness improvements
- Serialize code reviews: feature review now depends on main review,
guaranteeing cross-reference artifact is available (was race condition)
- Replace git reset --hard on canonical repo with isolated worktree
for main branch E2E testing (safe for concurrent validation runs)
- Replace fixed sleep + ungated curl with polling retry loops (60s max)
for both backend and frontend startup in both E2E commands
- Server output now logged to artifact files instead of /dev/null
for debuggability when startup fails
- Replace dead .classify-testability-output file read with
$nodeId.output.field substitution (executor injects values directly)
- Add worktree cleanup to both E2E main command and cleanup-processes
safety net node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:43:24 +00:00
effectiveIdleTimeout ,
2026-03-12 00:24:33 +00:00
( ) = > {
nodeIdleTimedOut = true ;
getLog ( ) . warn (
feat: add archon-validate-pr workflow + per-node idle_timeout (#635)
* fix(sqlite): reorder params to match $N placeholder positions
The SQLite adapter's convertPlaceholders naively replaced $N with ?
but didn't reorder the params array. PostgreSQL uses explicit $N
indices so param order doesn't matter, but SQLite's ? is positional.
This caused failWorkflowRun to swap the WHERE id and metadata params,
silently failing to update workflow status from 'running' to 'failed'.
Also changed SQLite dialect helpers (jsonMerge, jsonArrayContains,
nowMinusDays) to emit $N placeholders instead of raw ? so all
parameter handling goes through convertPlaceholders consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PRs and junk artifacts in workflow runs
- Add --resume CLI flag to retry failed workflows from the failed step
instead of starting from scratch (reuses existing worktree and PR)
- Add findLastFailedRun() query matching on (workflow_name, codebase_id)
so resume works across CLI invocations with different conversation IDs
- Add pre-flight PR dedup check to archon-create-pr command (searches
for existing open PRs before creating duplicates)
- Add .gitignore patterns for *.db-shm, *.db-wal, *.db-journal, undefined/
- Fix setup.test.ts env var restoration that created literal undefined/ dirs
- Add defensive check in getArchonHome() for string "undefined"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add archon-validate-pr workflow + per-node idle_timeout
Add a DAG workflow for thorough PR validation (code review + E2E browser
testing on both main and feature branches). Also add per-node/per-step
idle_timeout override to the workflow engine so long-running nodes like
E2E tests aren't killed by the default 5-minute idle timeout.
Key changes:
- New archon-validate-pr workflow with 6 command files
- idle_timeout field on DagNodeBase and SingleStep types
- Loader validation for idle_timeout (positive number)
- DAG executor and step executor use node.idle_timeout ?? default
- Cross-platform port detection (bun -e instead of /dev/tcp)
- DAG restructured to limit concurrent Claude processes
- Strengthened cleanup-processes node with pkill fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings — validation, resume bugs, fail-fast
- Add isFinite() check to idle_timeout validation at all 3 loader sites
(prevents Infinity from passing via YAML .inf literal)
- Guard --resume against step_index=0 (nothing to resume) to prevent
zombie running runs in the database
- Change executor startFromStep guard from > 0 to >= 0 so pre-created
runs are always honored
- Change findLastFailedRun ORDER BY from nullable completed_at to
non-null started_at (consistent with findResumableRun)
- Add existsSync check on resume working_path before reusing it
- Make getArchonHome() throw on literal "undefined" env var instead of
silently falling back (fail-fast per CLAUDE.md)
- Add Infinity rejection test for idle_timeout loader validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: workflow race conditions and robustness improvements
- Serialize code reviews: feature review now depends on main review,
guaranteeing cross-reference artifact is available (was race condition)
- Replace git reset --hard on canonical repo with isolated worktree
for main branch E2E testing (safe for concurrent validation runs)
- Replace fixed sleep + ungated curl with polling retry loops (60s max)
for both backend and frontend startup in both E2E commands
- Server output now logged to artifact files instead of /dev/null
for debuggability when startup fails
- Replace dead .classify-testability-output file read with
$nodeId.output.field substitution (executor injects values directly)
- Add worktree cleanup to both E2E main command and cleanup-processes
safety net node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:43:24 +00:00
{ nodeId : node.id , timeoutMs : effectiveIdleTimeout } ,
2026-03-12 00:24:33 +00:00
'dag_node_idle_timeout_reached'
) ;
nodeAbortController . abort ( ) ;
fix(workflows): idle timeout too aggressive on DAG nodes (#854) (#886)
* fix(workflows): idle timeout too aggressive — break after result, reset on all messages (#854)
The idle timeout (5 min default) caused two problems: (1) after a node's AI
finished (result message), the loop waited for the subprocess to exit, wasting
5 min on hangs; (2) tool messages didn't reset the timer, so long Bash calls
(tests, builds) triggered false timeouts on actively working nodes.
Changes:
- Break out of the for-await loop immediately after receiving the result message
in both command/prompt and loop node paths — no more post-completion waste
- Remove shouldResetTimer predicate so all message types (including tool) reset
the timer — timeout only fires on complete silence
- Increase STEP_IDLE_TIMEOUT_MS from 5 min to 30 min — with every message
resetting the timer, this is a deadlock detector, not a work limiter
Fixes #854
* fix(workflows): update withIdleTimeout JSDoc to match new timer behavior
Remove the tool-exclusion example from the shouldResetTimer docs since
that pattern was just removed from all call sites. Clarify that most
callers should omit the parameter.
* fix(workflows): address review findings — log cleanup errors, add break tests, fix stale docs
- Log generator cleanup errors in withIdleTimeout instead of silently swallowing
- Add behavioral tests for break-after-result in both command/prompt and loop nodes
- Fix stale "5 minutes" default in docs/loop-nodes.md (now 30 minutes)
- Clarify shouldResetTimer test names and comments (utility API, not executor behavior)
- Extract effectiveIdleTimeout in loop node path (matches command/prompt pattern)
- Remove redundant iterResult alias in withIdleTimeout
2026-03-30 11:49:14 +00:00
}
2026-03-12 00:24:33 +00:00
) ) {
2026-04-01 11:12:21 +00:00
const tickNow = Date . now ( ) ;
2026-03-14 18:25:35 +00:00
const nodeKey = ` ${ workflowRun . id } : ${ node . id } ` ;
fix(server,web,workflows): web approval gates auto-resume + reject-with-reason dialog
Fixes three tightly-coupled bugs that made web approval gates unusable:
1. orchestrator-agent did not pass parentConversationId to executeWorkflow
for any web-dispatched foreground / interactive / resumable run. Without
that field, findResumableRunByParentConversation (the machinery the CLI
relies on for resume) couldn't find the paused run from the same
conversation on a follow-up message, and the approve/reject API handlers
had no conversation to dispatch back to.
2. POST /api/workflows/runs/:runId/{approve,reject} recorded the decision
and returned "Send a message to continue the workflow." — the workflow
never actually resumed. Added tryAutoResumeAfterGate() that mirrors what
workflowApproveCommand / workflowRejectCommand already do on the CLI:
look up the parent conversation, dispatch `/workflow run <name>
<userMessage>` back through dispatchToOrchestrator. Failures are
non-fatal — the user can still send a manual message as a fallback.
3. The during-streaming cancel-check in dag-executor aborted any streaming
node whenever the run status left 'running', including the legitimate
transition to 'paused' that an approval node performs. A concurrent AI
node in the same DAG layer now tolerates 'paused' and finishes its own
stream; only truly terminal / unknown states (null, cancelled, failed,
completed) abort the in-flight stream.
Web UI: ConfirmRunActionDialog gains an optional reasonInput prop (label +
placeholder) that renders a textarea and passes the trimmed value to
onConfirm. WorkflowRunCard (dashboard) and WorkflowProgressCard (chat)
both use it for Reject now — the chat card was still on window.confirm,
which was both inconsistent with the dashboard and couldn't collect a
reason. The trimmed reason threads through to $REJECTION_REASON in the
workflow's on_reject prompt.
Supersedes #1147. @jonasvanderhaegen surfaced the root cause and shape of
the fix; that PR was 87 commits stale and pre-dated the reject-UX upgrade
(#1261 area), so this is a fresh re-do on current dev.
Tests:
- packages/server/src/routes/api.workflow-runs.test.ts — 5 new cases:
approve with parent dispatches; approve without parent returns "Send a
message"; approve with deleted parent conversation skips safely; reject
dispatches on-reject flows; reject that cancels (no on_reject) does NOT
dispatch.
- packages/core/src/orchestrator/orchestrator.test.ts — updated the two
synthesizedPrompt-dispatch tests for the new executeWorkflow arity.
Closes #1131.
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
2026-04-21 09:39:10 +00:00
// Cancel/pause check — read-only, no write contention in WAL mode (every 10s).
//
// `paused` is tolerated here: an approval node can transition the run to
// paused while this concurrent node is mid-stream (same topological layer).
// The streaming node should be allowed to finish its own output — the
// paused gate owns workflow progression, not individual node lifecycles.
// Only truly terminal / unknown states (null, cancelled, failed, completed)
// abort the in-flight stream.
2026-04-01 11:12:21 +00:00
if ( tickNow - ( lastNodeCancelCheck . get ( nodeKey ) ? ? 0 ) > CANCEL_CHECK_INTERVAL_MS ) {
lastNodeCancelCheck . set ( nodeKey , tickNow ) ;
2026-03-14 18:25:35 +00:00
try {
const streamStatus = await deps . store . getWorkflowRunStatus ( workflowRun . id ) ;
fix(server,web,workflows): web approval gates auto-resume + reject-with-reason dialog
Fixes three tightly-coupled bugs that made web approval gates unusable:
1. orchestrator-agent did not pass parentConversationId to executeWorkflow
for any web-dispatched foreground / interactive / resumable run. Without
that field, findResumableRunByParentConversation (the machinery the CLI
relies on for resume) couldn't find the paused run from the same
conversation on a follow-up message, and the approve/reject API handlers
had no conversation to dispatch back to.
2. POST /api/workflows/runs/:runId/{approve,reject} recorded the decision
and returned "Send a message to continue the workflow." — the workflow
never actually resumed. Added tryAutoResumeAfterGate() that mirrors what
workflowApproveCommand / workflowRejectCommand already do on the CLI:
look up the parent conversation, dispatch `/workflow run <name>
<userMessage>` back through dispatchToOrchestrator. Failures are
non-fatal — the user can still send a manual message as a fallback.
3. The during-streaming cancel-check in dag-executor aborted any streaming
node whenever the run status left 'running', including the legitimate
transition to 'paused' that an approval node performs. A concurrent AI
node in the same DAG layer now tolerates 'paused' and finishes its own
stream; only truly terminal / unknown states (null, cancelled, failed,
completed) abort the in-flight stream.
Web UI: ConfirmRunActionDialog gains an optional reasonInput prop (label +
placeholder) that renders a textarea and passes the trimmed value to
onConfirm. WorkflowRunCard (dashboard) and WorkflowProgressCard (chat)
both use it for Reject now — the chat card was still on window.confirm,
which was both inconsistent with the dashboard and couldn't collect a
reason. The trimmed reason threads through to $REJECTION_REASON in the
workflow's on_reject prompt.
Supersedes #1147. @jonasvanderhaegen surfaced the root cause and shape of
the fix; that PR was 87 commits stale and pre-dated the reject-UX upgrade
(#1261 area), so this is a fresh re-do on current dev.
Tests:
- packages/server/src/routes/api.workflow-runs.test.ts — 5 new cases:
approve with parent dispatches; approve without parent returns "Send a
message"; approve with deleted parent conversation skips safely; reject
dispatches on-reject flows; reject that cancels (no on_reject) does NOT
dispatch.
- packages/core/src/orchestrator/orchestrator.test.ts — updated the two
synthesizedPrompt-dispatch tests for the new executeWorkflow arity.
Closes #1131.
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
2026-04-21 09:39:10 +00:00
if ( streamStatus === null || ( streamStatus !== 'running' && streamStatus !== 'paused' ) ) {
2026-03-14 18:25:35 +00:00
getLog ( ) . info (
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
{ workflowRunId : workflowRun.id , nodeId : node.id , status : streamStatus ? ? 'deleted' } ,
'dag.stop_detected_during_streaming'
2026-03-14 18:25:35 +00:00
) ;
nodeAbortController . abort ( ) ;
break ;
}
} catch ( cancelCheckErr ) {
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
getLog ( ) . warn (
{ err : cancelCheckErr as Error , workflowRunId : workflowRun.id , nodeId : node.id } ,
'dag.status_check_failed'
) ;
2026-03-14 18:25:35 +00:00
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
2026-04-01 09:14:45 +00:00
// Activity heartbeat — write, throttled to every 60s (only for stale/zombie detection)
2026-04-01 11:12:21 +00:00
if ( tickNow - ( lastNodeActivityUpdate . get ( nodeKey ) ? ? 0 ) > ACTIVITY_HEARTBEAT_INTERVAL_MS ) {
lastNodeActivityUpdate . set ( nodeKey , tickNow ) ;
2026-04-01 09:14:45 +00:00
try {
await deps . store . updateWorkflowActivity ( workflowRun . id ) ;
} catch ( e ) {
getLog ( ) . warn (
{ err : e as Error , workflowRunId : workflowRun.id } ,
'dag.activity_update_failed'
) ;
}
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( msg . type === 'assistant' && msg . content ) {
nodeOutputText += msg . content ; // ALWAYS capture for $node_id.output
feat(providers/pi): interactive flag binds UIContext for extensions (#1299)
* feat(providers/pi): interactive flag binds UIContext for extensions
Adds `interactive: true` opt-in to Pi provider (in `.archon/config.yaml`
under `assistants.pi`) that binds a minimal `ExtensionUIContext` stub to
each session. Without this, Pi's `ExtensionRunner.hasUI()` reports false,
causing extensions like `@plannotator/pi-extension` to silently auto-approve
every plan instead of opening their browser review UI.
Semantics: clamped to `enableExtensions: true` — no extensions loaded
means nothing would consume `hasUI`, so `interactive` alone is silently
dropped. Stub forwards `notify()` to Archon's event stream; interactive
dialogs (select/confirm/input/editor/custom) resolve to undefined/false;
TUI-only setters (widgets/headers/footers/themes) no-op. Theme access
throws with a clear diagnostic — Pi's theme singleton is coupled to its
own `Symbol.for()` registry which Archon doesn't own.
Trust boundary: only binds when the operator has explicitly enabled
both flags. Extensions gated on `ctx.hasUI` (plannotator and similar)
get a functional UI context; extensions that reach for TUI features
still fail loudly rather than rendering garbage.
Includes smoke-test workflow documenting the integration surface.
End-to-end plannotator UI rendering requires plan-mode activation
(Pi `--plan` CLI flag or `/plannotator` TUI slash command) which is
out of reach for programmatic Archon sessions — manual test only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(providers/pi): end-to-end interactive extension UI
Three fixes that together get plannotator's browser review UI to actually
render from an Archon workflow and reach the reviewer's browser.
1. Call resourceLoader.reload() when enableExtensions is true.
createAgentSession's internal reload is gated on `!resourceLoader`, so
caller-supplied loaders must reload themselves. Without this,
getExtensions() returns the empty default, no ExtensionRunner is built,
and session.extensionRunner.setFlagValue() silently no-ops.
2. Set PLANNOTATOR_REMOTE=1 in interactive mode.
plannotator-browser.ts only calls ctx.ui.notify(url) when openBrowser()
returns { isRemote: true }; otherwise it spawns xdg-open/start on the
Archon server host — invisible to the user and untestable from bash
asserts. From the workflow runner's POV every Archon execution IS
remote; flipping the heuristic routes the URL through notify(), which
the ExtensionUIContext stub forwards into the event stream. Respect
explicit operator overrides.
3. notify() emits as assistant chunks, not system chunks.
The DAG executor's system-chunk filter only forwards warnings/MCP
prefixes, and only assistant chunks accumulate into $nodeId.output.
Emitting as assistant makes the URL available both in the user's
stream and in downstream bash/script nodes via output substitution.
Plus: extensionFlags config pass-through (equivalent to `pi --plan` on the
CLI) applied via ExtensionRunner.setFlagValue() BEFORE bindExtensions
fires session_start, so extensions reading flags in their startup handler
actually see them. Also bind extensions with an empty binding when
enableExtensions is on but interactive is off, so session_start still
fires for flag-driven but UI-less extensions.
Smoke test (.archon/workflows/e2e-plannotator-smoke.yaml) uses
openai-codex/gpt-5.4-mini (ChatGPT Plus OAuth compatible) and bumps
idle_timeout to 600000ms so plannotator's server survives while a human
approves in the browser.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(providers/pi): keep Archon extension-agnostic
Remove the plannotator-specific PLANNOTATOR_REMOTE=1 env var write from
the Pi provider. Archon's provider layer shouldn't know about any
specific extension's internals. Document the env var in the plannotator
smoke test instead — operators who use plannotator set it via their shell
or per-codebase env config.
Workflow smoke test updated with:
- Instructions for setting PLANNOTATOR_REMOTE=1 externally
- Simpler assertion (URL emission only) — validated in a real
reject-revise-approve run: reviewer annotated, clicked Send Feedback,
Pi received the feedback as a tool result, revised the plan (added
aria-label and WCAG contrast per the annotation), resubmitted, and
reviewer approved. Plannotator's tool result signals approval but
doesn't return the plan text, so the bash assertion now only checks
that the review URL reached the stream (not that plan content flowed
into \$nodeId.output — it can't).
- Known-limitation note documenting the tool-result shape so downstream
workflow authors know to Write the plan separately if they need it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(providers/pi): keep e2e-plannotator-smoke workflow local-only
The smoke test is plannotator-specific (calls plannotator_submit_plan,
expects PLAN.md on disk, requires PLANNOTATOR_REMOTE=1) and is better
kept out of the PR while the extension-agnostic infra lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(providers/pi): trim verbose inline comments
Collapse multi-paragraph SDK explanations to 1-2 line "why" notes across
provider.ts, types.ts, ui-context-stub.ts, and event-bridge.ts. No
behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(providers/pi): wire assistants.pi.env + theme-proxy identity
Two end-to-end fixes discovered while exercising the combined
plannotator + @pi-agents/loop smoke flow:
- PiProviderDefaults gains an optional `env` map; parsePiConfig picks
it up and the provider applies it to process.env at session start
(shell env wins, no override). Needed so extensions like plannotator
can read PLANNOTATOR_REMOTE=1 from config.yaml without requiring a
shell export before `archon workflow run`.
- ui-context-stub theme proxy returns identity decorators instead of
throwing on unknown methods. Styled strings flow into no-op
setStatus/setWidget sinks anyway, so the throw was blocking
plannotator_submit_plan after HTTP approval with no benefit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(providers/pi): flush notify() chunks immediately in batch mode
Batch-mode adapters (CLI) accumulate assistant chunks and only flush on
node completion. That broke plannotator's review-URL flow: Pi's notify()
emitted the URL as an assistant chunk, but the user needed the URL to
POST /api/approve — which is what unblocks the node in the first place.
Adds an optional `flush` flag on assistant MessageChunks. notify() sets
it, and the DAG executor drains pending batched content before surfacing
the flushed chunk so ordering is preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: mention Pi alongside Claude and Codex in README + top-level docs
The AI assistants docs page already covers Pi in depth, but the README
architecture diagram + docs table, overview "Further Reading" section,
and local-deployment .env comment still listed only Claude/Codex.
Left feature-specific mentions alone where Pi genuinely lacks support
(e.g. structured output — Claude + Codex only).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: note Pi structured output (best-effort) in matrix + workflow docs
Pi gained structured output support via prompt augmentation + JSON
extraction (see packages/providers/src/community/pi/capabilities.ts).
Unlike Claude/Codex, which use SDK-enforced JSON mode, Pi appends the
schema to the prompt and parses JSON out of the result text (bare or
fenced). Updates four stale references that still said Claude/Codex-only:
- ai-assistants.md capabilities matrix
- authoring-workflows.md (YAML example + field table)
- workflow-dag.md skill reference
- CLAUDE.md DAG-format node description
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(providers/pi): default extensions + interactive to on
Extensions (community packages like @plannotator/pi-extension and
user-authored ones) are a core reason users pick Pi. Defaulting
enableExtensions and interactive to false previously silenced installed
extensions with no signal, leading to "did my extension even load?"
confusion.
Opt out in .archon/config.yaml when you want the prior behavior:
assistants:
pi:
enableExtensions: false # skip extension discovery entirely
# interactive: false # load extensions, but no UI bridge
Docs gain a new "Extensions (on by default)" section in
getting-started/ai-assistants.md that documents the three config
surfaces (extensionFlags, env, workflow-level interactive) and uses
plannotator as a concrete walk-through example.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 12:37:40 +00:00
if ( streamingMode === 'stream' || msg . flush ) {
// `flush` chunks (e.g. Pi notify() emitting a plannotator review URL)
// must reach the user before the node blocks. Drain any queued batch
// content first so order is preserved.
if ( streamingMode === 'batch' && batchMessages . length > 0 ) {
await safeSendMessage (
platform ,
conversationId ,
batchMessages . join ( '\n\n' ) ,
nodeContext
) ;
batchMessages . length = 0 ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
await safeSendMessage ( platform , conversationId , msg . content , nodeContext ) ;
} else {
batchMessages . push ( msg . content ) ;
}
await logAssistant ( logDir , workflowRun . id , msg . content ) ;
} else if ( msg . type === 'tool' && msg . toolName ) {
fix: loading indicator race condition and workflow tool call durations (#654, #655) (#657)
* fix: preserve loading indicator on first message race condition (#654)
When the first message is sent, navigate() triggers a component remount
and a fresh SSE connection. Text events emitted before the connection
established were missed, leaving only the lock-release event, which
previously cleared isStreaming on empty thinking placeholders — making
the pulsing dots disappear before any AI text arrived.
Changes:
- Extract mapMessageRow() as a module-level helper (reused in re-fetch)
- Add conversationIdRef for stable access inside zero-dep onLockChange
- Fix needsStreamFix to skip empty placeholders (isStreaming && !!content)
- On lock release with a stuck placeholder, re-fetch completed messages
via REST to populate the placeholder with actual AI response content
- Remove redundant setSendInFlight(false) from onLockChange (handleSend
finally block already handles this after apiSendMessage returns)
Fixes #654
* fix: tool call cards always show 0ms duration in workflow logs (#655)
The workflow executors only persisted tool_called events with no timing
data. WorkflowLogs.tsx hardcoded duration: 0 when hydrating from the DB.
Changes:
- Add tool_completed to WorkflowEventType union in store.ts
- Emit tool_completed with duration_ms in executor.ts (sequential and loop)
- Emit tool_completed with duration_ms in dag-executor.ts (DAG nodes)
- Add duration? field to ToolEvent interface in WorkflowExecution.tsx
- Match tool_called/tool_completed events by name+time to compute duration
- Use te.duration instead of hardcoded 0 in WorkflowLogs.tsx
- Add tests for tool_completed emission in executor and dag-executor
Fixes #655
* fix: address review findings for PR #657
Fixed:
- HIGH: WorkflowLogs regression — loop workflow tool_completed events now emitted
in executeLoopWorkflow (Option A), and duration fallback `?? 0` added to
WorkflowLogs.tsx to prevent isRunning spinner for any remaining undefined cases
- LOW: Missing .catch() on stuck-placeholder re-fetch in ChatInterface — failure
now clears the stuck placeholder so user can retry
- LOW: Restore console.warn in mapMessageRow catch block — corrupted metadata
parse failures no longer silently swallowed
Skipped (YAGNI/out-of-scope):
- (none — all findings were real bugs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add .claude/worktrees to eslint ignores
ESLint was discovering files in .claude/worktrees/ (created by Claude
Code worktree sessions) and failing because they lack proper tsconfig
scope. The existing 'worktrees/**' pattern only matches top-level
worktrees, not nested .claude/worktrees.
* fix: address code review findings from PR #657 second review
- Fix Finding 1 (LOW/bug): Add setSendInFlight(false) before getMessages()
call in the race-condition recovery path (hasStuckPlaceholder=true). AI
processing is done when the lock releases, so the guard should be cleared
regardless of whether the REST re-fetch succeeds or fails. Without this fix,
a navigate-away/remount on the next message would incorrectly preserve any
isStreaming placeholder from the DB.
- Fix Finding 2 (LOW/style): Add clarifying comment to usedCompleted Set in
WorkflowExecution.tsx useMemo explaining the intentional local mutation
pattern inside .map().
- Skip Finding 3 (step_index on DAG tool_completed): Recommended as out-of-scope
by reviewer; DAG tool_called events also omit step_index by design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Auto-commit workflow artifacts (archon-assist)
* fix: workflow logs spinner/duration and second message crash
Bug 1 - WorkflowLogs tool cards show 0ms with no spinner:
- Remove ?? 0 fallback in hydrateMessages that coerced undefined (running)
to 0 (completed), killing the spinner and ticking elapsed counter
- Create assistant message in onToolCall when tools arrive before text,
so tool cards have a home in the SSE message list and participate in
the DB/SSE merge that preserves running state
Bug 2 - Second message in chat crashes/breaks:
- Move setSendInFlight(false) to be unconditional on lock release in
onLockChange, not gated behind hasStuckPlaceholder. When a workflow
dispatch replaces the thinking placeholder with status text,
hasStuckPlaceholder was false and the flag stayed true, causing ghost
streaming messages and disabled input on subsequent sends.
* fix: duplicate tool cards in workflow logs + Claude Code crash on 2nd message
Bug 1 - Duplicate tool cards when workflow completes:
The sseBySig Map merge logic used role:content as key, which collided
for multiple messages with content: '' (tool-only turns). One SSE entry
got grafted onto multiple DB messages, duplicating tool cards.
Fix: use DB messages exclusively once workflow stops running. SSE merge
is only needed during live execution for spinner state.
Bug 2 - Claude Code subprocess crashes on second message:
persistSession: false (claude.ts:253) discarded the session transcript.
When the second message tried to resume the session, the subprocess
couldn't find the transcript file and exited with code 1.
Fix: remove persistSession: false default so transcripts are written
and the resume mechanism works on subsequent messages.
* fix: eliminate duplicate tool cards during live workflow execution
Root cause: During live execution, both DB-polled messages and SSE-streamed
messages were shown together. When SSE text was still accumulating (partial
content) while DB had the full persisted content, signatures differed and
both appeared — causing duplicate tool cards and doubled text.
Fix: While running, treat SSE as the live source of truth. Only prepend
DB messages from before the SSE session started (older step messages).
After completion, switch to DB-only view. This cleanly separates the
live streaming path from the persisted history path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:00:05 +00:00
const now = Date . now ( ) ;
// Emit tool_completed for the previous tool (fire-and-forget)
if ( lastToolStartedAt ) {
const prevTool = lastToolStartedAt ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_completed' ,
runId : workflowRun.id ,
toolName : prevTool.toolName ,
stepName : node.id ,
durationMs : now - prevTool . startedAt ,
} ) ;
fix: loading indicator race condition and workflow tool call durations (#654, #655) (#657)
* fix: preserve loading indicator on first message race condition (#654)
When the first message is sent, navigate() triggers a component remount
and a fresh SSE connection. Text events emitted before the connection
established were missed, leaving only the lock-release event, which
previously cleared isStreaming on empty thinking placeholders — making
the pulsing dots disappear before any AI text arrived.
Changes:
- Extract mapMessageRow() as a module-level helper (reused in re-fetch)
- Add conversationIdRef for stable access inside zero-dep onLockChange
- Fix needsStreamFix to skip empty placeholders (isStreaming && !!content)
- On lock release with a stuck placeholder, re-fetch completed messages
via REST to populate the placeholder with actual AI response content
- Remove redundant setSendInFlight(false) from onLockChange (handleSend
finally block already handles this after apiSendMessage returns)
Fixes #654
* fix: tool call cards always show 0ms duration in workflow logs (#655)
The workflow executors only persisted tool_called events with no timing
data. WorkflowLogs.tsx hardcoded duration: 0 when hydrating from the DB.
Changes:
- Add tool_completed to WorkflowEventType union in store.ts
- Emit tool_completed with duration_ms in executor.ts (sequential and loop)
- Emit tool_completed with duration_ms in dag-executor.ts (DAG nodes)
- Add duration? field to ToolEvent interface in WorkflowExecution.tsx
- Match tool_called/tool_completed events by name+time to compute duration
- Use te.duration instead of hardcoded 0 in WorkflowLogs.tsx
- Add tests for tool_completed emission in executor and dag-executor
Fixes #655
* fix: address review findings for PR #657
Fixed:
- HIGH: WorkflowLogs regression — loop workflow tool_completed events now emitted
in executeLoopWorkflow (Option A), and duration fallback `?? 0` added to
WorkflowLogs.tsx to prevent isRunning spinner for any remaining undefined cases
- LOW: Missing .catch() on stuck-placeholder re-fetch in ChatInterface — failure
now clears the stuck placeholder so user can retry
- LOW: Restore console.warn in mapMessageRow catch block — corrupted metadata
parse failures no longer silently swallowed
Skipped (YAGNI/out-of-scope):
- (none — all findings were real bugs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add .claude/worktrees to eslint ignores
ESLint was discovering files in .claude/worktrees/ (created by Claude
Code worktree sessions) and failing because they lack proper tsconfig
scope. The existing 'worktrees/**' pattern only matches top-level
worktrees, not nested .claude/worktrees.
* fix: address code review findings from PR #657 second review
- Fix Finding 1 (LOW/bug): Add setSendInFlight(false) before getMessages()
call in the race-condition recovery path (hasStuckPlaceholder=true). AI
processing is done when the lock releases, so the guard should be cleared
regardless of whether the REST re-fetch succeeds or fails. Without this fix,
a navigate-away/remount on the next message would incorrectly preserve any
isStreaming placeholder from the DB.
- Fix Finding 2 (LOW/style): Add clarifying comment to usedCompleted Set in
WorkflowExecution.tsx useMemo explaining the intentional local mutation
pattern inside .map().
- Skip Finding 3 (step_index on DAG tool_completed): Recommended as out-of-scope
by reviewer; DAG tool_called events also omit step_index by design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Auto-commit workflow artifacts (archon-assist)
* fix: workflow logs spinner/duration and second message crash
Bug 1 - WorkflowLogs tool cards show 0ms with no spinner:
- Remove ?? 0 fallback in hydrateMessages that coerced undefined (running)
to 0 (completed), killing the spinner and ticking elapsed counter
- Create assistant message in onToolCall when tools arrive before text,
so tool cards have a home in the SSE message list and participate in
the DB/SSE merge that preserves running state
Bug 2 - Second message in chat crashes/breaks:
- Move setSendInFlight(false) to be unconditional on lock release in
onLockChange, not gated behind hasStuckPlaceholder. When a workflow
dispatch replaces the thinking placeholder with status text,
hasStuckPlaceholder was false and the flag stayed true, causing ghost
streaming messages and disabled input on subsequent sends.
* fix: duplicate tool cards in workflow logs + Claude Code crash on 2nd message
Bug 1 - Duplicate tool cards when workflow completes:
The sseBySig Map merge logic used role:content as key, which collided
for multiple messages with content: '' (tool-only turns). One SSE entry
got grafted onto multiple DB messages, duplicating tool cards.
Fix: use DB messages exclusively once workflow stops running. SSE merge
is only needed during live execution for spinner state.
Bug 2 - Claude Code subprocess crashes on second message:
persistSession: false (claude.ts:253) discarded the session transcript.
When the second message tried to resume the session, the subprocess
couldn't find the transcript file and exited with code 1.
Fix: remove persistSession: false default so transcripts are written
and the resume mechanism works on subsequent messages.
* fix: eliminate duplicate tool cards during live workflow execution
Root cause: During live execution, both DB-polled messages and SSE-streamed
messages were shown together. When SSE text was still accumulating (partial
content) while DB had the full persisted content, signatures differed and
both appeared — causing duplicate tool cards and doubled text.
Fix: While running, treat SSE as the live source of truth. Only prepend
DB messages from before the SSE session started (older step messages).
After completion, switch to DB-only view. This cleanly separates the
live streaming path from the persisted history path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:00:05 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_completed' ,
step_name : node.id ,
data : {
tool_name : prevTool.toolName ,
duration_ms : now - prevTool . startedAt ,
} ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'tool_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
}
lastToolStartedAt = { toolName : msg.toolName , startedAt : now } ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
// Emit tool_started for the current tool (fire-and-forget)
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_started' ,
runId : workflowRun.id ,
toolName : msg.toolName ,
stepName : node.id ,
} ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( streamingMode === 'stream' ) {
const toolMsg = formatToolCall ( msg . toolName , msg . toolInput ) ;
await safeSendMessage ( platform , conversationId , toolMsg , nodeContext , {
category : 'tool_call_formatted' ,
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
} as WorkflowMessageMetadata ) ;
2026-03-15 01:52:30 +00:00
// Send structured event to adapters that support it (Web UI)
if ( platform . sendStructuredEvent ) {
await platform . sendStructuredEvent ( conversationId , msg ) ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
await logTool ( logDir , workflowRun . id , msg . toolName , msg . toolInput ? ? { } ) ;
2026-03-14 23:04:07 +00:00
// Persist tool_called event for ALL adapters (fire-and-forget)
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_called' ,
step_name : node.id ,
data : {
tool_name : msg.toolName ,
tool_input : msg.toolInput ? ? { } ,
} ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'tool_called' } ,
'workflow_event_persist_failed'
) ;
} ) ;
2026-03-19 16:14:16 +00:00
} else if ( msg . type === 'tool_result' && msg . toolName ) {
if ( streamingMode === 'stream' && platform . sendStructuredEvent ) {
await platform . sendStructuredEvent ( conversationId , msg ) ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} else if ( msg . type === 'result' ) {
fix: loading indicator race condition and workflow tool call durations (#654, #655) (#657)
* fix: preserve loading indicator on first message race condition (#654)
When the first message is sent, navigate() triggers a component remount
and a fresh SSE connection. Text events emitted before the connection
established were missed, leaving only the lock-release event, which
previously cleared isStreaming on empty thinking placeholders — making
the pulsing dots disappear before any AI text arrived.
Changes:
- Extract mapMessageRow() as a module-level helper (reused in re-fetch)
- Add conversationIdRef for stable access inside zero-dep onLockChange
- Fix needsStreamFix to skip empty placeholders (isStreaming && !!content)
- On lock release with a stuck placeholder, re-fetch completed messages
via REST to populate the placeholder with actual AI response content
- Remove redundant setSendInFlight(false) from onLockChange (handleSend
finally block already handles this after apiSendMessage returns)
Fixes #654
* fix: tool call cards always show 0ms duration in workflow logs (#655)
The workflow executors only persisted tool_called events with no timing
data. WorkflowLogs.tsx hardcoded duration: 0 when hydrating from the DB.
Changes:
- Add tool_completed to WorkflowEventType union in store.ts
- Emit tool_completed with duration_ms in executor.ts (sequential and loop)
- Emit tool_completed with duration_ms in dag-executor.ts (DAG nodes)
- Add duration? field to ToolEvent interface in WorkflowExecution.tsx
- Match tool_called/tool_completed events by name+time to compute duration
- Use te.duration instead of hardcoded 0 in WorkflowLogs.tsx
- Add tests for tool_completed emission in executor and dag-executor
Fixes #655
* fix: address review findings for PR #657
Fixed:
- HIGH: WorkflowLogs regression — loop workflow tool_completed events now emitted
in executeLoopWorkflow (Option A), and duration fallback `?? 0` added to
WorkflowLogs.tsx to prevent isRunning spinner for any remaining undefined cases
- LOW: Missing .catch() on stuck-placeholder re-fetch in ChatInterface — failure
now clears the stuck placeholder so user can retry
- LOW: Restore console.warn in mapMessageRow catch block — corrupted metadata
parse failures no longer silently swallowed
Skipped (YAGNI/out-of-scope):
- (none — all findings were real bugs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add .claude/worktrees to eslint ignores
ESLint was discovering files in .claude/worktrees/ (created by Claude
Code worktree sessions) and failing because they lack proper tsconfig
scope. The existing 'worktrees/**' pattern only matches top-level
worktrees, not nested .claude/worktrees.
* fix: address code review findings from PR #657 second review
- Fix Finding 1 (LOW/bug): Add setSendInFlight(false) before getMessages()
call in the race-condition recovery path (hasStuckPlaceholder=true). AI
processing is done when the lock releases, so the guard should be cleared
regardless of whether the REST re-fetch succeeds or fails. Without this fix,
a navigate-away/remount on the next message would incorrectly preserve any
isStreaming placeholder from the DB.
- Fix Finding 2 (LOW/style): Add clarifying comment to usedCompleted Set in
WorkflowExecution.tsx useMemo explaining the intentional local mutation
pattern inside .map().
- Skip Finding 3 (step_index on DAG tool_completed): Recommended as out-of-scope
by reviewer; DAG tool_called events also omit step_index by design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Auto-commit workflow artifacts (archon-assist)
* fix: workflow logs spinner/duration and second message crash
Bug 1 - WorkflowLogs tool cards show 0ms with no spinner:
- Remove ?? 0 fallback in hydrateMessages that coerced undefined (running)
to 0 (completed), killing the spinner and ticking elapsed counter
- Create assistant message in onToolCall when tools arrive before text,
so tool cards have a home in the SSE message list and participate in
the DB/SSE merge that preserves running state
Bug 2 - Second message in chat crashes/breaks:
- Move setSendInFlight(false) to be unconditional on lock release in
onLockChange, not gated behind hasStuckPlaceholder. When a workflow
dispatch replaces the thinking placeholder with status text,
hasStuckPlaceholder was false and the flag stayed true, causing ghost
streaming messages and disabled input on subsequent sends.
* fix: duplicate tool cards in workflow logs + Claude Code crash on 2nd message
Bug 1 - Duplicate tool cards when workflow completes:
The sseBySig Map merge logic used role:content as key, which collided
for multiple messages with content: '' (tool-only turns). One SSE entry
got grafted onto multiple DB messages, duplicating tool cards.
Fix: use DB messages exclusively once workflow stops running. SSE merge
is only needed during live execution for spinner state.
Bug 2 - Claude Code subprocess crashes on second message:
persistSession: false (claude.ts:253) discarded the session transcript.
When the second message tried to resume the session, the subprocess
couldn't find the transcript file and exited with code 1.
Fix: remove persistSession: false default so transcripts are written
and the resume mechanism works on subsequent messages.
* fix: eliminate duplicate tool cards during live workflow execution
Root cause: During live execution, both DB-polled messages and SSE-streamed
messages were shown together. When SSE text was still accumulating (partial
content) while DB had the full persisted content, signatures differed and
both appeared — causing duplicate tool cards and doubled text.
Fix: While running, treat SSE as the live source of truth. Only prepend
DB messages from before the SSE session started (older step messages).
After completion, switch to DB-only view. This cleanly separates the
live streaming path from the persisted history path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:00:05 +00:00
// Emit tool_completed for the last tool in the node
if ( lastToolStartedAt ) {
const prevTool = lastToolStartedAt ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_completed' ,
runId : workflowRun.id ,
toolName : prevTool.toolName ,
stepName : node.id ,
durationMs : Date.now ( ) - prevTool . startedAt ,
} ) ;
fix: loading indicator race condition and workflow tool call durations (#654, #655) (#657)
* fix: preserve loading indicator on first message race condition (#654)
When the first message is sent, navigate() triggers a component remount
and a fresh SSE connection. Text events emitted before the connection
established were missed, leaving only the lock-release event, which
previously cleared isStreaming on empty thinking placeholders — making
the pulsing dots disappear before any AI text arrived.
Changes:
- Extract mapMessageRow() as a module-level helper (reused in re-fetch)
- Add conversationIdRef for stable access inside zero-dep onLockChange
- Fix needsStreamFix to skip empty placeholders (isStreaming && !!content)
- On lock release with a stuck placeholder, re-fetch completed messages
via REST to populate the placeholder with actual AI response content
- Remove redundant setSendInFlight(false) from onLockChange (handleSend
finally block already handles this after apiSendMessage returns)
Fixes #654
* fix: tool call cards always show 0ms duration in workflow logs (#655)
The workflow executors only persisted tool_called events with no timing
data. WorkflowLogs.tsx hardcoded duration: 0 when hydrating from the DB.
Changes:
- Add tool_completed to WorkflowEventType union in store.ts
- Emit tool_completed with duration_ms in executor.ts (sequential and loop)
- Emit tool_completed with duration_ms in dag-executor.ts (DAG nodes)
- Add duration? field to ToolEvent interface in WorkflowExecution.tsx
- Match tool_called/tool_completed events by name+time to compute duration
- Use te.duration instead of hardcoded 0 in WorkflowLogs.tsx
- Add tests for tool_completed emission in executor and dag-executor
Fixes #655
* fix: address review findings for PR #657
Fixed:
- HIGH: WorkflowLogs regression — loop workflow tool_completed events now emitted
in executeLoopWorkflow (Option A), and duration fallback `?? 0` added to
WorkflowLogs.tsx to prevent isRunning spinner for any remaining undefined cases
- LOW: Missing .catch() on stuck-placeholder re-fetch in ChatInterface — failure
now clears the stuck placeholder so user can retry
- LOW: Restore console.warn in mapMessageRow catch block — corrupted metadata
parse failures no longer silently swallowed
Skipped (YAGNI/out-of-scope):
- (none — all findings were real bugs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add .claude/worktrees to eslint ignores
ESLint was discovering files in .claude/worktrees/ (created by Claude
Code worktree sessions) and failing because they lack proper tsconfig
scope. The existing 'worktrees/**' pattern only matches top-level
worktrees, not nested .claude/worktrees.
* fix: address code review findings from PR #657 second review
- Fix Finding 1 (LOW/bug): Add setSendInFlight(false) before getMessages()
call in the race-condition recovery path (hasStuckPlaceholder=true). AI
processing is done when the lock releases, so the guard should be cleared
regardless of whether the REST re-fetch succeeds or fails. Without this fix,
a navigate-away/remount on the next message would incorrectly preserve any
isStreaming placeholder from the DB.
- Fix Finding 2 (LOW/style): Add clarifying comment to usedCompleted Set in
WorkflowExecution.tsx useMemo explaining the intentional local mutation
pattern inside .map().
- Skip Finding 3 (step_index on DAG tool_completed): Recommended as out-of-scope
by reviewer; DAG tool_called events also omit step_index by design.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Auto-commit workflow artifacts (archon-assist)
* fix: workflow logs spinner/duration and second message crash
Bug 1 - WorkflowLogs tool cards show 0ms with no spinner:
- Remove ?? 0 fallback in hydrateMessages that coerced undefined (running)
to 0 (completed), killing the spinner and ticking elapsed counter
- Create assistant message in onToolCall when tools arrive before text,
so tool cards have a home in the SSE message list and participate in
the DB/SSE merge that preserves running state
Bug 2 - Second message in chat crashes/breaks:
- Move setSendInFlight(false) to be unconditional on lock release in
onLockChange, not gated behind hasStuckPlaceholder. When a workflow
dispatch replaces the thinking placeholder with status text,
hasStuckPlaceholder was false and the flag stayed true, causing ghost
streaming messages and disabled input on subsequent sends.
* fix: duplicate tool cards in workflow logs + Claude Code crash on 2nd message
Bug 1 - Duplicate tool cards when workflow completes:
The sseBySig Map merge logic used role:content as key, which collided
for multiple messages with content: '' (tool-only turns). One SSE entry
got grafted onto multiple DB messages, duplicating tool cards.
Fix: use DB messages exclusively once workflow stops running. SSE merge
is only needed during live execution for spinner state.
Bug 2 - Claude Code subprocess crashes on second message:
persistSession: false (claude.ts:253) discarded the session transcript.
When the second message tried to resume the session, the subprocess
couldn't find the transcript file and exited with code 1.
Fix: remove persistSession: false default so transcripts are written
and the resume mechanism works on subsequent messages.
* fix: eliminate duplicate tool cards during live workflow execution
Root cause: During live execution, both DB-polled messages and SSE-streamed
messages were shown together. When SSE text was still accumulating (partial
content) while DB had the full persisted content, signatures differed and
both appeared — causing duplicate tool cards and doubled text.
Fix: While running, treat SSE as the live source of truth. Only prepend
DB messages from before the SSE session started (older step messages).
After completion, switch to DB-only view. This cleanly separates the
live streaming path from the persisted history path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:00:05 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_completed' ,
step_name : node.id ,
data : {
tool_name : prevTool.toolName ,
duration_ms : Date.now ( ) - prevTool . startedAt ,
} ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'tool_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
lastToolStartedAt = null ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( msg . sessionId ) newSessionId = msg . sessionId ;
if ( msg . tokens ) nodeTokens = msg . tokens ;
2026-04-06 16:39:18 +00:00
if ( msg . cost !== undefined ) nodeCostUsd = msg . cost ;
2026-04-06 17:03:47 +00:00
if ( msg . stopReason !== undefined ) nodeStopReason = msg . stopReason ;
2026-04-06 16:39:18 +00:00
if ( msg . numTurns !== undefined ) nodeNumTurns = msg . numTurns ;
if ( msg . modelUsage ) nodeModelUsage = msg . modelUsage ;
2026-03-12 10:38:21 +00:00
if ( msg . structuredOutput !== undefined ) structuredOutput = msg . structuredOutput ;
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
// Fail the node if the SDK reports a cost cap exceeded error
if ( msg . isError && msg . errorSubtype === 'error_max_budget_usd' ) {
2026-04-06 15:59:36 +00:00
const cap = nodeOptions ? . maxBudgetUsd ;
getLog ( ) . warn (
{ nodeId : node.id , maxBudgetUsd : cap , durationMs : Date.now ( ) - nodeStartTime } ,
'dag.node_budget_cap_exceeded'
) ;
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
throw new Error (
` Node ' ${ node . id } ' exceeded cost cap ${ cap !== undefined ? ` of $ ${ cap . toFixed ( 2 ) } ` : '' } . `
) ;
}
2026-04-18 20:02:35 +00:00
// Fail loudly on any other SDK error result. Previously we broke out of
// the stream silently, producing empty/partial output without signaling
// failure — which let failed iterations masquerade as successes (#1208).
if ( msg . isError ) {
const subtype = msg . errorSubtype ? ? 'unknown' ;
const errorsDetail = msg . errors ? . length ? ` — ${ msg . errors . join ( '; ' ) } ` : '' ;
getLog ( ) . error (
{
nodeId : node.id ,
errorSubtype : subtype ,
errors : msg.errors ,
sessionId : msg.sessionId ,
stopReason : msg.stopReason ,
durationMs : Date.now ( ) - nodeStartTime ,
} ,
'dag.node_sdk_error_result'
) ;
throw new Error ( ` Node ' ${ node . id } ' failed: SDK returned ${ subtype } ${ errorsDetail } ` ) ;
}
fix(workflows): idle timeout too aggressive on DAG nodes (#854) (#886)
* fix(workflows): idle timeout too aggressive — break after result, reset on all messages (#854)
The idle timeout (5 min default) caused two problems: (1) after a node's AI
finished (result message), the loop waited for the subprocess to exit, wasting
5 min on hangs; (2) tool messages didn't reset the timer, so long Bash calls
(tests, builds) triggered false timeouts on actively working nodes.
Changes:
- Break out of the for-await loop immediately after receiving the result message
in both command/prompt and loop node paths — no more post-completion waste
- Remove shouldResetTimer predicate so all message types (including tool) reset
the timer — timeout only fires on complete silence
- Increase STEP_IDLE_TIMEOUT_MS from 5 min to 30 min — with every message
resetting the timer, this is a deadlock detector, not a work limiter
Fixes #854
* fix(workflows): update withIdleTimeout JSDoc to match new timer behavior
Remove the tool-exclusion example from the shouldResetTimer docs since
that pattern was just removed from all call sites. Clarify that most
callers should omit the parameter.
* fix(workflows): address review findings — log cleanup errors, add break tests, fix stale docs
- Log generator cleanup errors in withIdleTimeout instead of silently swallowing
- Add behavioral tests for break-after-result in both command/prompt and loop nodes
- Fix stale "5 minutes" default in docs/loop-nodes.md (now 30 minutes)
- Clarify shouldResetTimer test names and comments (utility API, not executor behavior)
- Extract effectiveIdleTimeout in loop node path (matches command/prompt pattern)
- Remove redundant iterResult alias in withIdleTimeout
2026-03-30 11:49:14 +00:00
break ; // Result is the "I'm done" signal — don't wait for subprocess to exit
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
} else if ( msg . type === 'system' && msg . content ) {
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Forward provider warnings (⚠️) and MCP connection failures to the user.
// Providers yield system chunks for user-actionable issues (missing env vars,
// Haiku+MCP, structured output failures, etc.)
if (
msg . content . startsWith ( 'MCP server connection failed:' ) ||
msg . content . startsWith ( '⚠️' )
) {
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
getLog ( ) . warn (
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
{ nodeId : node.id , systemContent : msg.content } ,
'dag.provider_warning_forwarded'
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
) ;
const delivered = await safeSendMessage (
platform ,
conversationId ,
msg . content ,
nodeContext
) ;
if ( ! delivered ) {
getLog ( ) . error (
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
{ nodeId : node.id , workflowRunId : workflowRun.id } ,
'dag.provider_warning_delivery_failed'
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
) ;
}
} else {
getLog ( ) . debug (
{ nodeId : node.id , systemContent : msg.content } ,
'dag.system_message_unhandled'
) ;
}
2026-03-12 10:38:21 +00:00
}
2026-04-06 17:03:47 +00:00
// rate_limit chunks: already log.warn'd in claude.ts; not surfaced to SSE per design
2026-03-12 10:38:21 +00:00
}
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// When output_format is set and the provider returned structured_output,
// use it instead of the concatenated assistant text (which includes prose).
// Each provider normalizes its own structured output onto the result chunk —
// no provider-specific branching here.
2026-03-12 10:38:21 +00:00
if ( nodeOptions ? . outputFormat ) {
if ( structuredOutput !== undefined ) {
try {
nodeOutputText =
typeof structuredOutput === 'string'
? structuredOutput
: JSON . stringify ( structuredOutput ) ;
} catch ( serializeErr ) {
const err = serializeErr as Error ;
throw new Error (
` Node ' ${ node . id } ': failed to serialize structured_output to JSON: ${ err . message } `
) ;
}
getLog ( ) . debug ( { nodeId : node.id , streamingMode } , 'dag.structured_output_override' ) ;
} else {
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Provider did not populate structuredOutput — warn the user.
// If the provider detected invalid output, it already yielded a system warning.
2026-03-12 10:38:21 +00:00
getLog ( ) . warn (
{ nodeId : node.id , workflowRunId : workflowRun.id } ,
'dag.structured_output_missing'
) ;
await safeSendMessage (
platform ,
conversationId ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
` Warning: Node ' ${ node . id } ' requested output_format but the provider did not return structured output. Downstream conditions may not evaluate correctly. ` ,
2026-03-12 10:38:21 +00:00
nodeContext
) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
}
2026-03-12 00:24:33 +00:00
// If the node completed via idle timeout, log it
if ( nodeIdleTimedOut ) {
getLog ( ) . warn (
feat: add archon-validate-pr workflow + per-node idle_timeout (#635)
* fix(sqlite): reorder params to match $N placeholder positions
The SQLite adapter's convertPlaceholders naively replaced $N with ?
but didn't reorder the params array. PostgreSQL uses explicit $N
indices so param order doesn't matter, but SQLite's ? is positional.
This caused failWorkflowRun to swap the WHERE id and metadata params,
silently failing to update workflow status from 'running' to 'failed'.
Also changed SQLite dialect helpers (jsonMerge, jsonArrayContains,
nowMinusDays) to emit $N placeholders instead of raw ? so all
parameter handling goes through convertPlaceholders consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PRs and junk artifacts in workflow runs
- Add --resume CLI flag to retry failed workflows from the failed step
instead of starting from scratch (reuses existing worktree and PR)
- Add findLastFailedRun() query matching on (workflow_name, codebase_id)
so resume works across CLI invocations with different conversation IDs
- Add pre-flight PR dedup check to archon-create-pr command (searches
for existing open PRs before creating duplicates)
- Add .gitignore patterns for *.db-shm, *.db-wal, *.db-journal, undefined/
- Fix setup.test.ts env var restoration that created literal undefined/ dirs
- Add defensive check in getArchonHome() for string "undefined"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add archon-validate-pr workflow + per-node idle_timeout
Add a DAG workflow for thorough PR validation (code review + E2E browser
testing on both main and feature branches). Also add per-node/per-step
idle_timeout override to the workflow engine so long-running nodes like
E2E tests aren't killed by the default 5-minute idle timeout.
Key changes:
- New archon-validate-pr workflow with 6 command files
- idle_timeout field on DagNodeBase and SingleStep types
- Loader validation for idle_timeout (positive number)
- DAG executor and step executor use node.idle_timeout ?? default
- Cross-platform port detection (bun -e instead of /dev/tcp)
- DAG restructured to limit concurrent Claude processes
- Strengthened cleanup-processes node with pkill fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings — validation, resume bugs, fail-fast
- Add isFinite() check to idle_timeout validation at all 3 loader sites
(prevents Infinity from passing via YAML .inf literal)
- Guard --resume against step_index=0 (nothing to resume) to prevent
zombie running runs in the database
- Change executor startFromStep guard from > 0 to >= 0 so pre-created
runs are always honored
- Change findLastFailedRun ORDER BY from nullable completed_at to
non-null started_at (consistent with findResumableRun)
- Add existsSync check on resume working_path before reusing it
- Make getArchonHome() throw on literal "undefined" env var instead of
silently falling back (fail-fast per CLAUDE.md)
- Add Infinity rejection test for idle_timeout loader validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: workflow race conditions and robustness improvements
- Serialize code reviews: feature review now depends on main review,
guaranteeing cross-reference artifact is available (was race condition)
- Replace git reset --hard on canonical repo with isolated worktree
for main branch E2E testing (safe for concurrent validation runs)
- Replace fixed sleep + ungated curl with polling retry loops (60s max)
for both backend and frontend startup in both E2E commands
- Server output now logged to artifact files instead of /dev/null
for debuggability when startup fails
- Replace dead .classify-testability-output file read with
$nodeId.output.field substitution (executor injects values directly)
- Add worktree cleanup to both E2E main command and cleanup-processes
safety net node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:43:24 +00:00
{ nodeId : node.id , timeoutMs : effectiveIdleTimeout } ,
2026-03-12 00:24:33 +00:00
'dag_node_completed_via_idle_timeout'
) ;
await safeSendMessage (
platform ,
conversationId ,
feat: add archon-validate-pr workflow + per-node idle_timeout (#635)
* fix(sqlite): reorder params to match $N placeholder positions
The SQLite adapter's convertPlaceholders naively replaced $N with ?
but didn't reorder the params array. PostgreSQL uses explicit $N
indices so param order doesn't matter, but SQLite's ? is positional.
This caused failWorkflowRun to swap the WHERE id and metadata params,
silently failing to update workflow status from 'running' to 'failed'.
Also changed SQLite dialect helpers (jsonMerge, jsonArrayContains,
nowMinusDays) to emit $N placeholders instead of raw ? so all
parameter handling goes through convertPlaceholders consistently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PRs and junk artifacts in workflow runs
- Add --resume CLI flag to retry failed workflows from the failed step
instead of starting from scratch (reuses existing worktree and PR)
- Add findLastFailedRun() query matching on (workflow_name, codebase_id)
so resume works across CLI invocations with different conversation IDs
- Add pre-flight PR dedup check to archon-create-pr command (searches
for existing open PRs before creating duplicates)
- Add .gitignore patterns for *.db-shm, *.db-wal, *.db-journal, undefined/
- Fix setup.test.ts env var restoration that created literal undefined/ dirs
- Add defensive check in getArchonHome() for string "undefined"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add archon-validate-pr workflow + per-node idle_timeout
Add a DAG workflow for thorough PR validation (code review + E2E browser
testing on both main and feature branches). Also add per-node/per-step
idle_timeout override to the workflow engine so long-running nodes like
E2E tests aren't killed by the default 5-minute idle timeout.
Key changes:
- New archon-validate-pr workflow with 6 command files
- idle_timeout field on DagNodeBase and SingleStep types
- Loader validation for idle_timeout (positive number)
- DAG executor and step executor use node.idle_timeout ?? default
- Cross-platform port detection (bun -e instead of /dev/tcp)
- DAG restructured to limit concurrent Claude processes
- Strengthened cleanup-processes node with pkill fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings — validation, resume bugs, fail-fast
- Add isFinite() check to idle_timeout validation at all 3 loader sites
(prevents Infinity from passing via YAML .inf literal)
- Guard --resume against step_index=0 (nothing to resume) to prevent
zombie running runs in the database
- Change executor startFromStep guard from > 0 to >= 0 so pre-created
runs are always honored
- Change findLastFailedRun ORDER BY from nullable completed_at to
non-null started_at (consistent with findResumableRun)
- Add existsSync check on resume working_path before reusing it
- Make getArchonHome() throw on literal "undefined" env var instead of
silently falling back (fail-fast per CLAUDE.md)
- Add Infinity rejection test for idle_timeout loader validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: workflow race conditions and robustness improvements
- Serialize code reviews: feature review now depends on main review,
guaranteeing cross-reference artifact is available (was race condition)
- Replace git reset --hard on canonical repo with isolated worktree
for main branch E2E testing (safe for concurrent validation runs)
- Replace fixed sleep + ungated curl with polling retry loops (60s max)
for both backend and frontend startup in both E2E commands
- Server output now logged to artifact files instead of /dev/null
for debuggability when startup fails
- Replace dead .classify-testability-output file read with
$nodeId.output.field substitution (executor injects values directly)
- Add worktree cleanup to both E2E main command and cleanup-processes
safety net node
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:43:24 +00:00
` ⚠️ Node \` ${ node . id } \` completed via idle timeout (no output for ${ String ( effectiveIdleTimeout / 60000 ) } min). The AI likely finished but the subprocess didn't exit cleanly. ` ,
2026-03-12 00:24:33 +00:00
nodeContext
) ;
}
2026-03-14 18:25:35 +00:00
// If cancelled during streaming (not idle timeout), return as failed with cancel reason
if ( nodeAbortController . signal . aborted && ! nodeIdleTimedOut ) {
const duration = Date . now ( ) - nodeStartTime ;
getLog ( ) . info (
{ nodeId : node.id , durationMs : duration } ,
'dag_node_cancelled_during_streaming'
) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : 'Cancelled by user' , duration_ms : duration } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
error : 'Cancelled by user' ,
} ) ;
2026-04-01 09:14:45 +00:00
// Clean up throttle entries
lastNodeCancelCheck . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
2026-03-14 18:25:35 +00:00
lastNodeActivityUpdate . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
return { state : 'failed' , output : nodeOutputText , error : 'Cancelled by user' } ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
if ( streamingMode === 'batch' && batchMessages . length > 0 ) {
2026-03-12 10:38:21 +00:00
const batchContent =
structuredOutput !== undefined && nodeOptions ? . outputFormat
? nodeOutputText
: batchMessages . join ( '\n\n' ) ;
await safeSendMessage ( platform , conversationId , batchContent , nodeContext ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
2026-04-02 07:32:34 +00:00
// Detect credit exhaustion: SDK returns it as assistant text, not a thrown error.
const creditError = detectCreditExhaustion ( nodeOutputText ) ;
2026-04-02 07:18:30 +00:00
if ( creditError ) {
const duration = Date . now ( ) - nodeStartTime ;
2026-04-02 07:32:34 +00:00
getLog ( ) . warn ( { nodeId : node.id , durationMs : duration } , 'dag.node_credit_exhausted' ) ;
2026-04-02 07:18:30 +00:00
await logNodeError ( logDir , workflowRun . id , node . id , creditError ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : creditError } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
error : creditError ,
} ) ;
lastNodeCancelCheck . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
lastNodeActivityUpdate . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
return { state : 'failed' , output : nodeOutputText , error : creditError } ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const duration = Date . now ( ) - nodeStartTime ;
getLog ( ) . info ( { nodeId : node.id , durationMs : duration } , 'dag_node_completed' ) ;
await logNodeComplete ( logDir , workflowRun . id , node . id , node . command ? ? '<inline>' , {
durationMs : duration ,
tokens : nodeTokens ,
} ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_completed' ,
step_name : node.id ,
2026-04-06 16:39:18 +00:00
data : {
duration_ms : duration ,
node_output : nodeOutputText ,
. . . ( nodeCostUsd !== undefined ? { cost_usd : nodeCostUsd } : { } ) ,
. . . ( nodeStopReason ? { stop_reason : nodeStopReason } : { } ) ,
. . . ( nodeNumTurns !== undefined ? { num_turns : nodeNumTurns } : { } ) ,
. . . ( nodeModelUsage ? { model_usage : nodeModelUsage } : { } ) ,
} ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_completed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
duration ,
2026-04-06 16:39:18 +00:00
. . . ( nodeCostUsd !== undefined ? { costUsd : nodeCostUsd } : { } ) ,
. . . ( nodeStopReason ? { stopReason : nodeStopReason } : { } ) ,
. . . ( nodeNumTurns !== undefined ? { numTurns : nodeNumTurns } : { } ) ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} ) ;
2026-04-01 09:14:45 +00:00
// Clean up throttle entries on completion
lastNodeCancelCheck . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
2026-03-14 18:25:35 +00:00
lastNodeActivityUpdate . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
2026-04-06 16:39:18 +00:00
return {
state : 'completed' ,
output : nodeOutputText ,
sessionId : newSessionId ,
costUsd : nodeCostUsd ,
} ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} catch ( error ) {
const err = error as Error ;
2026-03-14 18:25:35 +00:00
2026-04-01 09:14:45 +00:00
// Clean up throttle entries on failure
lastNodeCancelCheck . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
2026-03-14 18:25:35 +00:00
lastNodeActivityUpdate . delete ( ` ${ workflowRun . id } : ${ node . id } ` ) ;
// If the abort was triggered by user cancel (not idle timeout), classify as cancel
if ( nodeAbortController . signal . aborted && ! nodeIdleTimedOut ) {
getLog ( ) . info ( { nodeId : node.id } , 'dag_node_cancelled_via_abort' ) ;
2026-04-06 16:39:18 +00:00
return {
state : 'failed' ,
output : nodeOutputText ,
error : 'Cancelled by user' ,
costUsd : nodeCostUsd ,
} ;
2026-03-14 18:25:35 +00:00
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
getLog ( ) . error ( { err , nodeId : node.id } , 'dag_node_failed' ) ;
await logNodeError ( logDir , workflowRun . id , node . id , err . message ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : err.message } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
error : err.message ,
} ) ;
2026-04-06 16:39:18 +00:00
return { state : 'failed' , output : '' , error : err.message , costUsd : nodeCostUsd } ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
}
2026-04-09 11:48:02 +00:00
/** Default timeout for subprocess nodes (bash, script): 2 minutes */
const SUBPROCESS_DEFAULT_TIMEOUT = 120 _000 ;
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
/ * *
* Execute a bash ( shell script ) DAG node .
* Runs the script via ` bash -c ` , captures stdout as node output .
* No AI session is created — bash nodes are free / deterministic .
* /
async function executeBashNode (
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
conversationId : string ,
cwd : string ,
workflowRun : WorkflowRun ,
node : BashNode ,
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
2026-04-06 13:26:59 +00:00
docsDir : string ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
nodeOutputs : Map < string , NodeOutput > ,
2026-04-13 12:21:57 +00:00
issueContext? : string ,
envVars? : Record < string , string >
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
) : Promise < NodeOutput > {
const nodeStartTime = Date . now ( ) ;
const nodeContext : SendMessageContext = { workflowId : workflowRun.id , nodeName : node.id } ;
getLog ( ) . info ( { nodeId : node.id , type : 'bash' } , 'dag_node_started' ) ;
await logNodeStart ( logDir , workflowRun . id , node . id , '<bash>' ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_started' ,
step_name : node.id ,
data : { type : 'bash' } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_started' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_started' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
} ) ;
// Variable substitution on script
const { prompt : substitutedScript } = substituteWorkflowVariables (
node . bash ,
workflowRun . id ,
workflowRun . user_message ,
artifactsDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
issueContext
) ;
2026-03-13 07:29:39 +00:00
const finalScript = substituteNodeOutputRefs ( substitutedScript , nodeOutputs , true ) ;
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
2026-04-09 11:48:02 +00:00
const timeout = node . timeout ? ? SUBPROCESS_DEFAULT_TIMEOUT ;
2026-04-13 12:21:57 +00:00
const subprocessEnv =
envVars && Object . keys ( envVars ) . length > 0 ? { . . . process . env , . . . envVars } : undefined ;
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
try {
const { stdout , stderr } = await execFileAsync ( 'bash' , [ '-c' , finalScript ] , {
cwd ,
timeout ,
2026-04-13 12:21:57 +00:00
env : subprocessEnv ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
} ) ;
// Trim trailing newline from stdout (common shell behavior)
const output = stdout . replace ( /\n$/ , '' ) ;
if ( stderr . trim ( ) ) {
getLog ( ) . warn ( { nodeId : node.id , stderr : stderr.trim ( ) } , 'bash_node_stderr' ) ;
await safeSendMessage (
platform ,
conversationId ,
` Bash node ' ${ node . id } ' stderr: \ n \` \` \` \ n ${ stderr . trim ( ) } \ n \` \` \` ` ,
nodeContext
) ;
}
const duration = Date . now ( ) - nodeStartTime ;
getLog ( ) . info ( { nodeId : node.id , durationMs : duration } , 'dag_node_completed' ) ;
await logNodeComplete ( logDir , workflowRun . id , node . id , '<bash>' , { durationMs : duration } ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_completed' ,
step_name : node.id ,
2026-03-24 09:15:07 +00:00
data : { duration_ms : duration , type : 'bash' , node_output : output } ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_completed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
duration ,
} ) ;
return { state : 'completed' , output } ;
} catch ( error ) {
const err = error as Error & { killed? : boolean ; code? : number | string } ;
const isTimeout = err . killed === true || ( err . message ? ? '' ) . includes ( 'timed out' ) ;
let errorMsg : string ;
if ( isTimeout ) {
errorMsg = ` Bash node ' ${ node . id } ' timed out after ${ String ( timeout ) } ms ` ;
} else if ( err . message ? . includes ( 'ENOENT' ) ) {
errorMsg = ` Bash node ' ${ node . id } ' failed: bash executable not found in PATH ` ;
} else if ( err . message ? . includes ( 'EACCES' ) ) {
errorMsg = ` Bash node ' ${ node . id } ' failed: permission denied (check cwd permissions) ` ;
} else {
errorMsg = ` Bash node ' ${ node . id } ' failed: ${ err . message } ` ;
}
getLog ( ) . error ( { err , nodeId : node.id , isTimeout } , 'dag_node_failed' ) ;
await logNodeError ( logDir , workflowRun . id , node . id , errorMsg ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : errorMsg , type : 'bash' } ,
} )
. catch ( ( dbErr : Error ) = > {
getLog ( ) . error (
{ err : dbErr , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
error : errorMsg ,
} ) ;
return { state : 'failed' , output : '' , error : errorMsg } ;
}
}
2026-04-09 11:48:02 +00:00
/ * *
* Execute a script ( TypeScript via bun or Python via uv ) DAG node .
* Supports both inline code snippets and named scripts discovered from . archon / scripts / .
* stdout is captured and trimmed as the node output ; stderr is logged as a warning .
* /
async function executeScriptNode (
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
conversationId : string ,
cwd : string ,
workflowRun : WorkflowRun ,
node : ScriptNode ,
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
docsDir : string ,
nodeOutputs : Map < string , NodeOutput > ,
2026-04-13 12:21:57 +00:00
issueContext? : string ,
envVars? : Record < string , string >
2026-04-09 11:48:02 +00:00
) : Promise < NodeOutput > {
const nodeStartTime = Date . now ( ) ;
const nodeContext : SendMessageContext = { workflowId : workflowRun.id , nodeName : node.id } ;
getLog ( ) . info ( { nodeId : node.id , type : 'script' , runtime : node.runtime } , 'dag_node_started' ) ;
await logNodeStart ( logDir , workflowRun . id , node . id , '<script>' ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_started' ,
step_name : node.id ,
data : { type : 'script' , runtime : node.runtime } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_started' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_started' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
} ) ;
// Variable substitution on script field
const { prompt : substitutedScript } = substituteWorkflowVariables (
node . script ,
workflowRun . id ,
workflowRun . user_message ,
artifactsDir ,
baseBranch ,
docsDir ,
issueContext
) ;
const finalScript = substituteNodeOutputRefs ( substitutedScript , nodeOutputs , false ) ;
const timeout = node . timeout ? ? SUBPROCESS_DEFAULT_TIMEOUT ;
2026-04-13 12:21:57 +00:00
const subprocessEnv =
envVars && Object . keys ( envVars ) . length > 0 ? { . . . process . env , . . . envVars } : undefined ;
2026-04-09 11:48:02 +00:00
// Build the command and args based on runtime and inline vs named
let cmd = '' ;
let args : string [ ] = [ ] ;
const nodeDeps = node . deps ? ? [ ] ;
try {
if ( isInlineScript ( finalScript ) ) {
// Inline code execution
if ( node . runtime === 'bun' ) {
cmd = 'bun' ;
2026-04-13 10:46:24 +00:00
// --no-env-file prevents Bun from auto-loading .env from the execution
// cwd (the target repo). Without this, repo .env leaks into the script
// subprocess despite Archon's parent process cleanup.
args = [ '--no-env-file' , '-e' , finalScript ] ;
2026-04-09 11:48:02 +00:00
} else {
// uv run --with dep1 --with dep2 python -c <code>
cmd = 'uv' ;
const withFlags = nodeDeps . flatMap ( dep = > [ '--with' , dep ] ) ;
args = [ 'run' , . . . withFlags , 'python' , '-c' , finalScript ] ;
}
} else {
feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath (closes #1136) (#1315)
* feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath
Collapses the awkward `~/.archon/.archon/workflows/` convention to a direct
`~/.archon/workflows/` child (matching `workspaces/`, `archon.db`, etc.), adds
home-scoped commands and scripts with the same loading story, and kills the
opt-in `globalSearchPath` parameter so every call site gets home-scope for free.
Closes #1136 (supersedes @jonasvanderhaegen's tactical fix — the bug was the
primitive itself: an easy-to-forget parameter that five of six call sites on
dev dropped).
Primitive changes:
- Home paths are direct children of `~/.archon/`. New helpers in `@archon/paths`:
`getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`,
and `getLegacyHomeWorkflowsPath()` (detection-only for migration).
- `discoverWorkflowsWithConfig(cwd, loadConfig)` reads home-scope internally.
The old `{ globalSearchPath }` option is removed. Chat command handler, Web
UI workflow picker, orchestrator resolve path — all inherit home-scope for
free without maintainer patches at every new site.
- `discoverScriptsForCwd(cwd)` merges home + repo scripts (repo wins on name
collision). dag-executor and validator use it; the hardcoded
`resolve(cwd, '.archon', 'scripts')` single-scope path is gone.
- Command resolution is now walked-by-basename in each scope. `loadCommand`
and `resolveCommand` walk 1 subfolder deep and match by `.md` basename, so
`.archon/commands/triage/review.md` resolves as `review` — closes the
latent bug where subfolder commands were listed but unresolvable.
- All three (`workflows/`, `commands/`, `scripts/`) enforce a 1-level
subfolder cap (matches the existing `defaults/` convention). Deeper
nesting is silently skipped.
- `WorkflowSource` gains `'global'` alongside `'bundled'` and `'project'`.
Web UI node palette shows a dedicated "Global (~/.archon/commands/)"
section; badges updated.
Migration (clean cut — no fallback read):
- First use after upgrade: if `~/.archon/.archon/workflows/` exists, Archon
logs a one-time WARN per process with the exact `mv` command:
`mv ~/.archon/.archon/workflows ~/.archon/workflows && rmdir ~/.archon/.archon`
The legacy path is NOT read — users migrate manually. Rollback caveat
noted in CHANGELOG.
Tests:
- `@archon/paths/archon-paths.test.ts`: new helper tests (default HOME,
ARCHON_HOME override, Docker), plus regression guards for the double-`.archon/`
path.
- `@archon/workflows/loader.test.ts`: home-scoped workflows, precedence,
subfolder 1-depth cap, legacy-path deprecation warning fires exactly once
per process.
- `@archon/workflows/validator.test.ts`: home-scoped commands + subfolder
resolution.
- `@archon/workflows/script-discovery.test.ts`: depth cap + merge semantics
(repo wins, home-missing tolerance).
- Existing CLI + orchestrator tests updated to drop `globalSearchPath`
assertions.
E2E smoke (verified locally, before cleanup):
- `.archon/workflows/e2e-home-scope.yaml` + scratch repo at /tmp
- Home-scoped workflow discovered from an unrelated git repo
- Home-scoped script (`~/.archon/scripts/*.ts`) executes inside a script node
- 1-level subfolder workflow (`~/.archon/workflows/triage/*.yaml`) listed
- Legacy path warning fires with actionable `mv` command; workflows there
are NOT loaded
Docs: `CLAUDE.md`, `docs-web/guides/global-workflows.md` (full rewrite for
three-type scope + subfolder convention + migration), `docs-web/reference/
configuration.md` (directory tree), `docs-web/reference/cli.md`,
`docs-web/guides/authoring-workflows.md`.
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
* test(script-discovery): normalize path separators in mocks for Windows
The 4 new tests in `scanScriptDir depth cap` and `discoverScriptsForCwd —
merge repo + home with repo winning` compared incoming mock paths with
hardcoded forward-slash strings (`if (path === '/scripts/triage')`). On
Windows, `path.join('/scripts', 'triage')` produces `\scripts\triage`, so
those branches never matched, readdir returned `[]`, and the tests failed.
Added a `norm()` helper at module scope and wrapped the incoming `path`
argument in every `mockImplementation` before comparing. Stored paths go
through `normalizeSep()` in production code, so the existing equality
assertions on `script.path` remain OS-independent.
Fixes Windows CI job `test (windows-latest)` on PR #1315.
* address review feedback: home-scope error handling, depth cap, and tests
Critical fixes:
- api.ts: add `maxDepth: 1` to all 3 findMarkdownFilesRecursive calls in
GET /api/commands (bundled/home/project). Without this the UI palette
surfaced commands from deep subfolders that the executor (capped at 1)
could not resolve — silent "command not found" at runtime.
- validator.ts: wrap home-scope findMarkdownFilesRecursive and
resolveCommandInDir calls in try/catch so EACCES/EPERM on
~/.archon/commands/ doesn't crash the validator with a raw filesystem
error. ENOENT still returns [] via the underlying helper.
Error handling fixes:
- workflow-discovery.ts: maybeWarnLegacyHomePath now sets the
"warned-once" flag eagerly before `await access()`, so concurrent
discovery calls (server startup with parallel codebase resolution)
can't double-warn. Non-ENOENT probe errors (EACCES/EPERM) now log at
WARN instead of DEBUG so permission issues on the legacy dir are
visible in default operation.
- dag-executor.ts: wrap discoverScriptsForCwd in its own try/catch so
an EACCES on ~/.archon/scripts/ routes through safeSendMessage /
logNodeError with a dedicated "failed to discover scripts" message
instead of being mis-attributed by the outer catch's
"permission denied (check cwd permissions)" branch.
Tests:
- load-command-prompt.test.ts (new): 6 tests covering the executor's
command resolution hot path — home-scope resolves when repo misses,
repo shadows home, 1-level subfolder resolvable by basename, 2-level
rejected, not-found, empty-file. Runs in its own bun test batch.
- archon-paths.test.ts: add getHomeScriptsPath describe block to match
the existing getHomeCommandsPath / getHomeWorkflowsPath coverage.
Comment clarity:
- workflow-discovery.ts: MAX_DISCOVERY_DEPTH comment now leads with the
actual value (1) before describing what 0 would mean.
- script-discovery.ts: copy the "routing ambiguity" rationale from
MAX_DISCOVERY_DEPTH to MAX_SCRIPT_DISCOVERY_DEPTH.
Cleanup:
- Remove .archon/workflows/e2e-home-scope.yaml — one-off smoke test that
would ship permanently in every project's workflow list. Equivalent
coverage exists in loader.test.ts.
Addresses all blocking and important feedback from the multi-agent
review on PR #1315.
---------
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
2026-04-20 18:45:32 +00:00
// Named script — look up across repo and home scopes.
// Precedence: <cwd>/.archon/scripts/ > ~/.archon/scripts/ (repo wins).
// Wrap discovery in its own try/catch so a permission error on ~/.archon/scripts/
// isn't mis-attributed by the outer catch's "permission denied (check cwd
// permissions)" branch — that branch is for execFileAsync EACCES.
let scripts : Awaited < ReturnType < typeof discoverScriptsForCwd > > ;
try {
scripts = await discoverScriptsForCwd ( cwd ) ;
} catch ( discoveryErr ) {
const err = discoveryErr as Error ;
const errorMsg = ` Script node ' ${ node . id } ': failed to discover scripts — ${ err . message } ` ;
getLog ( ) . error ( { err , nodeId : node.id , cwd } , 'script_discovery_failed' ) ;
await safeSendMessage ( platform , conversationId , errorMsg , nodeContext ) ;
await logNodeError ( logDir , workflowRun . id , node . id , errorMsg ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
error : errorMsg ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : errorMsg , type : 'script' } ,
} )
. catch ( ( dbErr : Error ) = > {
getLog ( ) . error (
{ err : dbErr , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
return { state : 'failed' , output : '' , error : errorMsg } ;
}
2026-04-09 11:48:02 +00:00
const scriptDef = scripts . get ( finalScript ) ;
if ( ! scriptDef ) {
feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath (closes #1136) (#1315)
* feat(paths,workflows): unify ~/.archon/{workflows,commands,scripts} + drop globalSearchPath
Collapses the awkward `~/.archon/.archon/workflows/` convention to a direct
`~/.archon/workflows/` child (matching `workspaces/`, `archon.db`, etc.), adds
home-scoped commands and scripts with the same loading story, and kills the
opt-in `globalSearchPath` parameter so every call site gets home-scope for free.
Closes #1136 (supersedes @jonasvanderhaegen's tactical fix — the bug was the
primitive itself: an easy-to-forget parameter that five of six call sites on
dev dropped).
Primitive changes:
- Home paths are direct children of `~/.archon/`. New helpers in `@archon/paths`:
`getHomeWorkflowsPath()`, `getHomeCommandsPath()`, `getHomeScriptsPath()`,
and `getLegacyHomeWorkflowsPath()` (detection-only for migration).
- `discoverWorkflowsWithConfig(cwd, loadConfig)` reads home-scope internally.
The old `{ globalSearchPath }` option is removed. Chat command handler, Web
UI workflow picker, orchestrator resolve path — all inherit home-scope for
free without maintainer patches at every new site.
- `discoverScriptsForCwd(cwd)` merges home + repo scripts (repo wins on name
collision). dag-executor and validator use it; the hardcoded
`resolve(cwd, '.archon', 'scripts')` single-scope path is gone.
- Command resolution is now walked-by-basename in each scope. `loadCommand`
and `resolveCommand` walk 1 subfolder deep and match by `.md` basename, so
`.archon/commands/triage/review.md` resolves as `review` — closes the
latent bug where subfolder commands were listed but unresolvable.
- All three (`workflows/`, `commands/`, `scripts/`) enforce a 1-level
subfolder cap (matches the existing `defaults/` convention). Deeper
nesting is silently skipped.
- `WorkflowSource` gains `'global'` alongside `'bundled'` and `'project'`.
Web UI node palette shows a dedicated "Global (~/.archon/commands/)"
section; badges updated.
Migration (clean cut — no fallback read):
- First use after upgrade: if `~/.archon/.archon/workflows/` exists, Archon
logs a one-time WARN per process with the exact `mv` command:
`mv ~/.archon/.archon/workflows ~/.archon/workflows && rmdir ~/.archon/.archon`
The legacy path is NOT read — users migrate manually. Rollback caveat
noted in CHANGELOG.
Tests:
- `@archon/paths/archon-paths.test.ts`: new helper tests (default HOME,
ARCHON_HOME override, Docker), plus regression guards for the double-`.archon/`
path.
- `@archon/workflows/loader.test.ts`: home-scoped workflows, precedence,
subfolder 1-depth cap, legacy-path deprecation warning fires exactly once
per process.
- `@archon/workflows/validator.test.ts`: home-scoped commands + subfolder
resolution.
- `@archon/workflows/script-discovery.test.ts`: depth cap + merge semantics
(repo wins, home-missing tolerance).
- Existing CLI + orchestrator tests updated to drop `globalSearchPath`
assertions.
E2E smoke (verified locally, before cleanup):
- `.archon/workflows/e2e-home-scope.yaml` + scratch repo at /tmp
- Home-scoped workflow discovered from an unrelated git repo
- Home-scoped script (`~/.archon/scripts/*.ts`) executes inside a script node
- 1-level subfolder workflow (`~/.archon/workflows/triage/*.yaml`) listed
- Legacy path warning fires with actionable `mv` command; workflows there
are NOT loaded
Docs: `CLAUDE.md`, `docs-web/guides/global-workflows.md` (full rewrite for
three-type scope + subfolder convention + migration), `docs-web/reference/
configuration.md` (directory tree), `docs-web/reference/cli.md`,
`docs-web/guides/authoring-workflows.md`.
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
* test(script-discovery): normalize path separators in mocks for Windows
The 4 new tests in `scanScriptDir depth cap` and `discoverScriptsForCwd —
merge repo + home with repo winning` compared incoming mock paths with
hardcoded forward-slash strings (`if (path === '/scripts/triage')`). On
Windows, `path.join('/scripts', 'triage')` produces `\scripts\triage`, so
those branches never matched, readdir returned `[]`, and the tests failed.
Added a `norm()` helper at module scope and wrapped the incoming `path`
argument in every `mockImplementation` before comparing. Stored paths go
through `normalizeSep()` in production code, so the existing equality
assertions on `script.path` remain OS-independent.
Fixes Windows CI job `test (windows-latest)` on PR #1315.
* address review feedback: home-scope error handling, depth cap, and tests
Critical fixes:
- api.ts: add `maxDepth: 1` to all 3 findMarkdownFilesRecursive calls in
GET /api/commands (bundled/home/project). Without this the UI palette
surfaced commands from deep subfolders that the executor (capped at 1)
could not resolve — silent "command not found" at runtime.
- validator.ts: wrap home-scope findMarkdownFilesRecursive and
resolveCommandInDir calls in try/catch so EACCES/EPERM on
~/.archon/commands/ doesn't crash the validator with a raw filesystem
error. ENOENT still returns [] via the underlying helper.
Error handling fixes:
- workflow-discovery.ts: maybeWarnLegacyHomePath now sets the
"warned-once" flag eagerly before `await access()`, so concurrent
discovery calls (server startup with parallel codebase resolution)
can't double-warn. Non-ENOENT probe errors (EACCES/EPERM) now log at
WARN instead of DEBUG so permission issues on the legacy dir are
visible in default operation.
- dag-executor.ts: wrap discoverScriptsForCwd in its own try/catch so
an EACCES on ~/.archon/scripts/ routes through safeSendMessage /
logNodeError with a dedicated "failed to discover scripts" message
instead of being mis-attributed by the outer catch's
"permission denied (check cwd permissions)" branch.
Tests:
- load-command-prompt.test.ts (new): 6 tests covering the executor's
command resolution hot path — home-scope resolves when repo misses,
repo shadows home, 1-level subfolder resolvable by basename, 2-level
rejected, not-found, empty-file. Runs in its own bun test batch.
- archon-paths.test.ts: add getHomeScriptsPath describe block to match
the existing getHomeCommandsPath / getHomeWorkflowsPath coverage.
Comment clarity:
- workflow-discovery.ts: MAX_DISCOVERY_DEPTH comment now leads with the
actual value (1) before describing what 0 would mean.
- script-discovery.ts: copy the "routing ambiguity" rationale from
MAX_DISCOVERY_DEPTH to MAX_SCRIPT_DISCOVERY_DEPTH.
Cleanup:
- Remove .archon/workflows/e2e-home-scope.yaml — one-off smoke test that
would ship permanently in every project's workflow list. Equivalent
coverage exists in loader.test.ts.
Addresses all blocking and important feedback from the multi-agent
review on PR #1315.
---------
Co-authored-by: Jonas Vanderhaegen <7755555+jonasvanderhaegen@users.noreply.github.com>
2026-04-20 18:45:32 +00:00
const errorMsg = ` Script node ' ${ node . id } ': named script ' ${ finalScript } ' not found in .archon/scripts/ or ~/.archon/scripts/ ` ;
2026-04-09 11:48:02 +00:00
getLog ( ) . error ( { nodeId : node.id , scriptName : finalScript } , 'script_not_found' ) ;
await safeSendMessage ( platform , conversationId , errorMsg , nodeContext ) ;
await logNodeError ( logDir , workflowRun . id , node . id , errorMsg ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
error : errorMsg ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : errorMsg , type : 'script' } ,
} )
. catch ( ( dbErr : Error ) = > {
getLog ( ) . error (
{ err : dbErr , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
return { state : 'failed' , output : '' , error : errorMsg } ;
}
// Use scriptDef.runtime (canonical source) instead of re-deriving from extension
if ( scriptDef . runtime === 'uv' ) {
cmd = 'uv' ;
const withFlags = nodeDeps . flatMap ( dep = > [ '--with' , dep ] ) ;
args = [ 'run' , . . . withFlags , scriptDef . path ] ;
} else {
cmd = 'bun' ;
2026-04-13 10:46:24 +00:00
args = [ '--no-env-file' , 'run' , scriptDef . path ] ;
2026-04-09 11:48:02 +00:00
}
}
const { stdout , stderr } = await execFileAsync ( cmd , args , {
cwd ,
timeout ,
2026-04-13 12:21:57 +00:00
env : subprocessEnv ,
2026-04-09 11:48:02 +00:00
} ) ;
// Trim trailing newline from stdout (common shell behavior)
const output = stdout . replace ( /\n$/ , '' ) ;
if ( stderr . trim ( ) ) {
getLog ( ) . warn ( { nodeId : node.id , stderr : stderr.trim ( ) } , 'script_node_stderr' ) ;
await safeSendMessage (
platform ,
conversationId ,
` Script node ' ${ node . id } ' stderr: \ n \` \` \` \ n ${ stderr . trim ( ) } \ n \` \` \` ` ,
nodeContext
) ;
}
const duration = Date . now ( ) - nodeStartTime ;
getLog ( ) . info ( { nodeId : node.id , durationMs : duration } , 'dag_node_completed' ) ;
await logNodeComplete ( logDir , workflowRun . id , node . id , '<script>' , { durationMs : duration } ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_completed' ,
step_name : node.id ,
data : { duration_ms : duration , type : 'script' , node_output : output } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_completed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
duration ,
} ) ;
return { state : 'completed' , output } ;
} catch ( error ) {
const err = error as Error & { killed? : boolean ; code? : number | string ; stderr? : string } ;
const isTimeout = err . killed === true || ( err . message ? ? '' ) . includes ( 'timed out' ) ;
const stderrHint = err . stderr ? . trim ( ) ? ` \ n \ nScript output: \ n ${ err . stderr . trim ( ) } ` : '' ;
let errorMsg : string ;
if ( isTimeout ) {
errorMsg = ` Script node ' ${ node . id } ' timed out after ${ String ( timeout ) } ms ` ;
} else if ( err . message ? . includes ( 'ENOENT' ) ) {
errorMsg = ` Script node ' ${ node . id } ' failed: ' ${ cmd } ' executable not found in PATH ` ;
} else if ( err . message ? . includes ( 'EACCES' ) ) {
errorMsg = ` Script node ' ${ node . id } ' failed: permission denied (check cwd permissions) ` ;
} else {
errorMsg = ` Script node ' ${ node . id } ' failed: ${ err . message } ${ stderrHint } ` ;
}
getLog ( ) . error ( { err , nodeId : node.id , isTimeout } , 'dag_node_failed' ) ;
await logNodeError ( logDir , workflowRun . id , node . id , errorMsg ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : errorMsg , type : 'script' } ,
} )
. catch ( ( dbErr : Error ) = > {
getLog ( ) . error (
{ err : dbErr , workflowRunId : workflowRun.id , eventType : 'node_failed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
error : errorMsg ,
} ) ;
return { state : 'failed' , output : '' , error : errorMsg } ;
}
}
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
/ * *
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
* Build SendQueryOptions from resolved provider , model , and config .
* Uses the same nodeConfig + assistantConfig pattern as resolveNodeProviderAndModel .
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
* /
function buildLoopNodeOptions (
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
provider : string ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
model : string | undefined ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
config : WorkflowConfig ,
workflowLevelOptions? : WorkflowLevelOptions
) : SendQueryOptions {
const options : SendQueryOptions = { } ;
if ( model ) options . model = model ;
if ( config . envVars && Object . keys ( config . envVars ) . length > 0 ) {
options . env = config . envVars ;
}
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
options . assistantConfig = config . assistants [ provider ] ? ? { } ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
// Pass workflow-level options as nodeConfig so providers can apply them
if ( workflowLevelOptions ) {
options . nodeConfig = {
effort : workflowLevelOptions.effort ,
thinking : workflowLevelOptions.thinking ,
sandbox : workflowLevelOptions.sandbox ,
betas : workflowLevelOptions.betas ,
fallbackModel : workflowLevelOptions.fallbackModel ,
} ;
}
return options ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
/ * *
* Execute a loop node — runs prompt repeatedly until completion signal or max iterations .
*
* Key behaviors :
2026-04-06 17:03:47 +00:00
* - Returns NodeExecutionResult ( not void ) — DAG executor owns workflow lifecycle
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
* - Receives upstream node outputs for $nodeId . output substitution
* - Does not write current_step_index ( DAG tracks per - node completion )
* /
async function executeLoopNode (
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
conversationId : string ,
cwd : string ,
workflowRun : WorkflowRun ,
node : LoopNode ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
workflowProvider : string ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
workflowModel : string | undefined ,
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
2026-04-06 13:26:59 +00:00
docsDir : string ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
nodeOutputs : Map < string , NodeOutput > ,
config : WorkflowConfig ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
issueContext? : string ,
workflowLevelOptions? : WorkflowLevelOptions
2026-04-06 16:39:18 +00:00
) : Promise < NodeExecutionResult > {
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const loop = node . loop ;
const msgContext = { workflowId : workflowRun.id , nodeName : node.id } ;
// Resolve AI client — fail fast with descriptive error
2026-04-12 10:11:21 +00:00
let aiClient : ReturnType < typeof deps.getAgentProvider > ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
try {
2026-04-12 10:11:21 +00:00
aiClient = deps . getAgentProvider ( workflowProvider ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
} catch ( error ) {
const err = error as Error ;
const errorMsg = ` Invalid provider ' ${ workflowProvider } ' for loop node ' ${ node . id } '. Check workflow YAML or .archon/config.yaml. Original: ${ err . message } ` ;
getLog ( ) . error (
{ err , nodeId : node.id , provider : workflowProvider } ,
'loop_node.provider_failed'
) ;
return { state : 'failed' , output : '' , error : errorMsg } ;
}
2026-04-01 15:33:34 +00:00
// Detect interactive loop resume — check if workflowRun.metadata has loop gate state for this node
2026-04-01 16:17:56 +00:00
const rawApproval = workflowRun . metadata ? . approval ;
const loopGateMeta = isApprovalContext ( rawApproval ) ? rawApproval : undefined ;
2026-04-01 15:33:34 +00:00
const isLoopResume = loopGateMeta ? . type === 'interactive_loop' && loopGateMeta . nodeId === node . id ;
const startIteration = isLoopResume ? ( loopGateMeta . iteration ? ? 0 ) + 1 : 1 ;
let currentSessionId : string | undefined = isLoopResume ? loopGateMeta.sessionId : undefined ;
const loopUserInput = isLoopResume
? ( ( workflowRun . metadata ? . loop_user_input as string | undefined ) ? ? '' )
: '' ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
let lastIterationOutput = '' ;
2026-04-06 16:39:18 +00:00
let loopTotalCostUsd : number | undefined ;
let loopFinalStopReason : string | undefined ;
let loopTotalNumTurns : number | undefined ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
const resolvedOptions = buildLoopNodeOptions (
workflowProvider ,
workflowModel ,
config ,
workflowLevelOptions
) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
// Helper to log event store errors consistently
const logEventStoreError = ( err : Error , iteration : number ) : void = > {
getLog ( ) . error ( { err , nodeId : node.id , iteration } , 'loop_node.iteration_event_failed' ) ;
} ;
2026-04-01 15:33:34 +00:00
for ( let i = startIteration ; i <= loop . max_iterations ; i ++ ) {
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const iterationStart = Date . now ( ) ;
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
// Check for non-running status between iterations (cancellation, deletion, or future: pause)
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const runStatus = await deps . store . getWorkflowRunStatus ( workflowRun . id ) ;
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
if ( runStatus === null || runStatus !== 'running' ) {
const effectiveStatus = runStatus ? ? 'deleted' ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
getLog ( ) . info (
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
{ workflowRunId : workflowRun.id , nodeId : node.id , iteration : i , status : effectiveStatus } ,
'loop_node.stop_detected'
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
) ;
await safeSendMessage (
platform ,
conversationId ,
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
` Loop node ' ${ node . id } ' stopped at iteration ${ String ( i ) } ( ${ effectiveStatus } ) ` ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
msgContext
) ;
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
return { state : 'failed' , output : '' , error : ` Workflow ${ effectiveStatus } ` } ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
// Emit iteration started
getWorkflowEventEmitter ( ) . emit ( {
type : 'loop_iteration_started' ,
runId : workflowRun.id ,
nodeId : node.id ,
iteration : i ,
maxIterations : loop.max_iterations ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'loop_iteration_started' ,
step_name : node.id ,
data : { iteration : i , maxIterations : loop.max_iterations , nodeId : node.id } ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
// Session threading
2026-03-26 21:00:02 +00:00
const needsFreshSession = loop . fresh_context || i === 1 ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const resumeSessionId = needsFreshSession ? undefined : currentSessionId ;
// Stream AI response for this iteration
let fullOutput = '' ; // raw, for signal detection
let cleanOutput = '' ; // stripped, for platform display
let iterationIdleTimedOut = false ;
const iterationAbortController = new AbortController ( ) ;
try {
// Build prompt — substituteWorkflowVariables throws if $BASE_BRANCH referenced but empty
2026-04-01 16:17:56 +00:00
// Pass loopUserInput on the first resumed iteration; '' on all others (non-interactive
// or subsequent iterations) so $LOOP_USER_INPUT substitutes to empty string explicitly.
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const { prompt : substitutedPrompt } = substituteWorkflowVariables (
loop . prompt ,
workflowRun . id ,
workflowRun . user_message ,
artifactsDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
2026-04-01 15:33:34 +00:00
issueContext ,
2026-04-01 16:17:56 +00:00
i === startIteration ? loopUserInput : ''
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
) ;
const finalPrompt = substituteNodeOutputRefs ( substitutedPrompt , nodeOutputs ) ;
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
const iterationOptions : SendQueryOptions | undefined = {
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
. . . resolvedOptions ,
abortSignal : iterationAbortController.signal ,
} ;
const generator = aiClient . sendQuery ( finalPrompt , cwd , resumeSessionId , iterationOptions ) ;
let lastToolStartedAt : { toolName : string ; startedAt : number } | null = null ;
fix(workflows): idle timeout too aggressive on DAG nodes (#854) (#886)
* fix(workflows): idle timeout too aggressive — break after result, reset on all messages (#854)
The idle timeout (5 min default) caused two problems: (1) after a node's AI
finished (result message), the loop waited for the subprocess to exit, wasting
5 min on hangs; (2) tool messages didn't reset the timer, so long Bash calls
(tests, builds) triggered false timeouts on actively working nodes.
Changes:
- Break out of the for-await loop immediately after receiving the result message
in both command/prompt and loop node paths — no more post-completion waste
- Remove shouldResetTimer predicate so all message types (including tool) reset
the timer — timeout only fires on complete silence
- Increase STEP_IDLE_TIMEOUT_MS from 5 min to 30 min — with every message
resetting the timer, this is a deadlock detector, not a work limiter
Fixes #854
* fix(workflows): update withIdleTimeout JSDoc to match new timer behavior
Remove the tool-exclusion example from the shouldResetTimer docs since
that pattern was just removed from all call sites. Clarify that most
callers should omit the parameter.
* fix(workflows): address review findings — log cleanup errors, add break tests, fix stale docs
- Log generator cleanup errors in withIdleTimeout instead of silently swallowing
- Add behavioral tests for break-after-result in both command/prompt and loop nodes
- Fix stale "5 minutes" default in docs/loop-nodes.md (now 30 minutes)
- Clarify shouldResetTimer test names and comments (utility API, not executor behavior)
- Extract effectiveIdleTimeout in loop node path (matches command/prompt pattern)
- Remove redundant iterResult alias in withIdleTimeout
2026-03-30 11:49:14 +00:00
const effectiveIdleTimeout = node . idle_timeout ? ? STEP_IDLE_TIMEOUT_MS ;
for await ( const msg of withIdleTimeout ( generator , effectiveIdleTimeout , ( ) = > {
iterationIdleTimedOut = true ;
getLog ( ) . warn (
{ nodeId : node.id , iteration : i , timeoutMs : effectiveIdleTimeout } ,
'loop_node.idle_timeout_reached'
) ;
iterationAbortController . abort ( ) ;
} ) ) {
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
if ( msg . type === 'assistant' ) {
fullOutput += msg . content ;
const cleaned = stripCompletionTags ( msg . content ) ;
cleanOutput += cleaned ;
if ( platform . getStreamingMode ( ) === 'stream' && cleaned ) {
await safeSendMessage ( platform , conversationId , cleaned , msgContext ) ;
}
await logAssistant ( logDir , workflowRun . id , msg . content ) ;
} else if ( msg . type === 'result' ) {
// Emit tool_completed for the last tool in the iteration
if ( lastToolStartedAt ) {
const prevTool = lastToolStartedAt ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_completed' ,
runId : workflowRun.id ,
toolName : prevTool.toolName ,
stepName : node.id ,
durationMs : Date.now ( ) - prevTool . startedAt ,
} ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_completed' ,
step_name : node.id ,
data : {
tool_name : prevTool.toolName ,
duration_ms : Date.now ( ) - prevTool . startedAt ,
} ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
lastToolStartedAt = null ;
}
if ( msg . sessionId ) currentSessionId = msg . sessionId ;
2026-04-06 16:39:18 +00:00
if ( msg . cost !== undefined ) {
loopTotalCostUsd = ( loopTotalCostUsd ? ? 0 ) + msg . cost ;
}
2026-04-06 17:03:47 +00:00
if ( msg . stopReason !== undefined ) loopFinalStopReason = msg . stopReason ;
2026-04-06 16:39:18 +00:00
if ( msg . numTurns !== undefined ) {
loopTotalNumTurns = ( loopTotalNumTurns ? ? 0 ) + msg . numTurns ;
}
2026-04-18 20:02:35 +00:00
// Fail the iteration loudly on SDK error results. Previously we broke
// silently, producing empty output and continuing to the next iteration —
// which made `error_during_execution` on resumed interactive loops look
// like a "5-second crash" that kept burning iterations (#1208).
if ( msg . isError ) {
const subtype = msg . errorSubtype ? ? 'unknown' ;
const errorsDetail = msg . errors ? . length ? ` — ${ msg . errors . join ( '; ' ) } ` : '' ;
getLog ( ) . error (
{
nodeId : node.id ,
iteration : i ,
errorSubtype : subtype ,
errors : msg.errors ,
sessionId : msg.sessionId ,
stopReason : msg.stopReason ,
} ,
'loop_node.iteration_sdk_error'
) ;
throw new Error (
` Loop ' ${ node . id } ' iteration ${ String ( i ) } failed: SDK returned ${ subtype } ${ errorsDetail } `
) ;
}
fix(workflows): idle timeout too aggressive on DAG nodes (#854) (#886)
* fix(workflows): idle timeout too aggressive — break after result, reset on all messages (#854)
The idle timeout (5 min default) caused two problems: (1) after a node's AI
finished (result message), the loop waited for the subprocess to exit, wasting
5 min on hangs; (2) tool messages didn't reset the timer, so long Bash calls
(tests, builds) triggered false timeouts on actively working nodes.
Changes:
- Break out of the for-await loop immediately after receiving the result message
in both command/prompt and loop node paths — no more post-completion waste
- Remove shouldResetTimer predicate so all message types (including tool) reset
the timer — timeout only fires on complete silence
- Increase STEP_IDLE_TIMEOUT_MS from 5 min to 30 min — with every message
resetting the timer, this is a deadlock detector, not a work limiter
Fixes #854
* fix(workflows): update withIdleTimeout JSDoc to match new timer behavior
Remove the tool-exclusion example from the shouldResetTimer docs since
that pattern was just removed from all call sites. Clarify that most
callers should omit the parameter.
* fix(workflows): address review findings — log cleanup errors, add break tests, fix stale docs
- Log generator cleanup errors in withIdleTimeout instead of silently swallowing
- Add behavioral tests for break-after-result in both command/prompt and loop nodes
- Fix stale "5 minutes" default in docs/loop-nodes.md (now 30 minutes)
- Clarify shouldResetTimer test names and comments (utility API, not executor behavior)
- Extract effectiveIdleTimeout in loop node path (matches command/prompt pattern)
- Remove redundant iterResult alias in withIdleTimeout
2026-03-30 11:49:14 +00:00
break ; // Result is the "I'm done" signal — don't wait for subprocess to exit
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
} else if ( msg . type === 'tool' && msg . toolName ) {
const now = Date . now ( ) ;
// Emit tool_completed for the previous tool
if ( lastToolStartedAt ) {
const prevTool = lastToolStartedAt ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_completed' ,
runId : workflowRun.id ,
toolName : prevTool.toolName ,
stepName : node.id ,
durationMs : now - prevTool . startedAt ,
} ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_completed' ,
step_name : node.id ,
data : { tool_name : prevTool.toolName , duration_ms : now - prevTool . startedAt } ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
}
lastToolStartedAt = { toolName : msg.toolName , startedAt : now } ;
feat(web): live step & tool progress on Mission Control dashboard (#730)
* feat(web): live step & tool progress on Mission Control dashboard (#711)
- Emit tool_started/tool_completed events from workflow executor (sequential, loop, DAG)
- Bridge tool activity events to SSE as workflow_tool_activity
- Add __dashboard__ multiplexed SSE endpoint for all workflow events
- Extend DashboardWorkflowRun with current step name/status and agent counts via correlated subqueries (SQLite + PostgreSQL dialect-aware)
- Add useDashboardSSE hook connecting to __dashboard__ SSE stream
- Add handleWorkflowToolActivity to Zustand workflow store
- WorkflowRunCard subscribes to Zustand store directly for live step/tool updates
- DashboardPage hydrates store from REST data for active runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(web): correct event_index SQL bug, deduplicate CASE subquery, and type/code quality fixes
- Replace non-existent `event_index` column with `created_at` in all 8 correlated subqueries in `listDashboardRuns` (CRITICAL runtime fix — would crash dashboard for all users)
- Remove `current_step_event_index` field from `DashboardWorkflowRun` and `DashboardRunResponse` (field was never consumed by frontend)
- Deduplicate the triplicated `CASE` subquery into a single `CASE expr WHEN ...` form (HIGH performance/correctness fix)
- Add `WorkflowToolActivityEvent` to `SSEEvent` discriminated union in `types.ts` (MEDIUM type safety)
- Remove unused `sourceRef` from `useDashboardSSE` hook (MEDIUM YAGNI)
- Add `{ streamId: '__dashboard__' }` context object to all dashboard SSE log calls (MEDIUM logging compliance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: totalSteps JSON key mismatch and extract IIFE to named component
- Fix total_steps always null: change jsonIntExtract key from 'totalSteps'
to 'total_steps' to match what the executor writes
- Extract 25-line IIFE in WorkflowRunCard JSX to named StepProgress component
- Fix stepIndex > 0 guard to stepIndex != null (was hiding Step 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move WorkflowState import to top of file (ESLint import/first)
The import was placed after the PLATFORM_ICONS constant, violating
ESLint's import/first rule which fails CI with --max-warnings 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(workflows): emit tool_started/tool_completed events from loop node executor
The loop node executor in dag-executor.ts was writing tool events to the
database but not emitting them via getWorkflowEventEmitter(). This meant
the WorkflowEventBridge never received tool activity events for loop
nodes, so the dashboard SSE stream had no workflow_tool_activity events
and the WorkflowRunCard's currentTool display stayed empty.
Add tool_started/tool_completed emitter calls to executeLoopNode(),
matching the pattern already used in executeNodeInternal() for regular
DAG nodes and executeStepInternal() for sequential steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): persist last tool activity on dashboard cards instead of flashing
currentTool was a plain string set on tool_started and cleared to null
on tool_completed, causing sub-second flashes that were invisible to
users. Change currentTool to a rich object { name, status, durationMs }
so completed tools display as "Read (5.7s)" in muted text and running
tools show as "Read…" in accent color, persisting until the next tool
starts or the workflow finishes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web): make live tool progress prominent on dashboard cards
Move StepProgress out of the tiny metadata row into its own dedicated
section with a highlighted background. Step info renders at text-sm with
font-medium, tool calls in monospace. Running tools show a CSS spinner.
Much more visible than the previous inline text-xs rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 21:52:39 +00:00
// Emit tool_started for the current tool (fire-and-forget)
getWorkflowEventEmitter ( ) . emit ( {
type : 'tool_started' ,
runId : workflowRun.id ,
toolName : msg.toolName ,
stepName : node.id ,
} ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
if ( platform . getStreamingMode ( ) === 'stream' ) {
const toolMsg = formatToolCall ( msg . toolName , msg . toolInput ) ;
if ( toolMsg ) {
2026-04-10 20:48:40 +00:00
await safeSendMessage ( platform , conversationId , toolMsg , msgContext , {
category : 'tool_call_formatted' ,
} as WorkflowMessageMetadata ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
if ( platform . sendStructuredEvent ) {
await platform . sendStructuredEvent ( conversationId , msg ) ;
}
}
const toolInput : Record < string , unknown > = msg . toolInput
? Object . fromEntries (
Object . entries ( msg . toolInput ) . map ( ( [ k , v ] ) = >
typeof v === 'string' && v . length > 500 ? [ k , v . slice ( 0 , 500 ) + '...' ] : [ k , v ]
)
)
: { } ;
await logTool ( logDir , workflowRun . id , msg . toolName , toolInput ) ;
// Persist tool_called event
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'tool_called' ,
step_name : node.id ,
data : { tool_name : msg.toolName , tool_input : toolInput } ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
} else if ( msg . type === 'tool_result' && platform . sendStructuredEvent ) {
await platform . sendStructuredEvent ( conversationId , msg ) ;
}
2026-04-06 17:03:47 +00:00
// rate_limit chunks: already log.warn'd in claude.ts; not surfaced to SSE per design
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
} catch ( error ) {
const err = error as Error ;
const duration = Date . now ( ) - iterationStart ;
getLog ( ) . error ( { err , nodeId : node.id , iteration : i } , 'loop_node.iteration_failed' ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'loop_iteration_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
iteration : i ,
error : err.message ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'loop_iteration_failed' ,
step_name : node.id ,
data : { iteration : i , error : err.message , duration , nodeId : node.id } ,
} )
. catch ( ( evtErr : Error ) = > {
logEventStoreError ( evtErr , i ) ;
} ) ;
2026-04-06 16:39:18 +00:00
return {
state : 'failed' ,
output : '' ,
error : ` Loop iteration ${ i } failed: ${ err . message } ` ,
costUsd : loopTotalCostUsd ,
} ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
// Notify on idle timeout
if ( iterationIdleTimedOut ) {
await safeSendMessage (
platform ,
conversationId ,
` Loop node ' ${ node . id } ' iteration ${ String ( i ) } completed via idle timeout (no output for ${ String ( ( node . idle_timeout ? ? STEP_IDLE_TIMEOUT_MS ) / 60000 ) } min) ` ,
msgContext
) ;
}
// Batch mode: send accumulated output
if ( platform . getStreamingMode ( ) === 'batch' && cleanOutput ) {
await safeSendMessage ( platform , conversationId , cleanOutput , msgContext ) ;
}
lastIterationOutput = cleanOutput || fullOutput ;
2026-04-01 21:24:13 +00:00
// Check LLM completion signal — the AI decides whether the user approved.
// For interactive loops, the AI emits the signal when the user explicitly approves
// (e.g., "approved", "looks good"). The prompt instructs the AI on when to emit it.
const signalDetected = detectCompletionSignal ( fullOutput , loop . until ) ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
// Check deterministic bash condition (if configured)
let bashComplete = false ;
if ( loop . until_bash ) {
try {
const { prompt : bashPrompt } = substituteWorkflowVariables (
loop . until_bash ,
workflowRun . id ,
workflowRun . user_message ,
artifactsDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
issueContext
) ;
const substitutedBash = substituteNodeOutputRefs (
bashPrompt ,
nodeOutputs ,
true // escapedForBash
) ;
await execFileAsync ( 'bash' , [ '-c' , substitutedBash ] , { cwd } ) ;
bashComplete = true ; // exit 0 = complete
} catch ( e ) {
const bashErr = e as NodeJS . ErrnoException ;
// ENOENT or other system errors are unexpected — log them
if ( bashErr . code === 'ENOENT' ) {
getLog ( ) . warn (
{ err : bashErr , nodeId : node.id , iteration : i } ,
'loop_node.until_bash_exec_error'
) ;
}
bashComplete = false ; // non-zero exit = not complete
}
}
const duration = Date . now ( ) - iterationStart ;
const completionDetected = signalDetected || bashComplete ;
// Emit iteration completed
getWorkflowEventEmitter ( ) . emit ( {
type : 'loop_iteration_completed' ,
runId : workflowRun.id ,
nodeId : node.id ,
iteration : i ,
duration ,
completionDetected ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'loop_iteration_completed' ,
step_name : node.id ,
data : { iteration : i , duration , completionDetected , nodeId : node.id } ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
await logNodeComplete ( logDir , workflowRun . id , ` ${ node . id } -iteration- ${ String ( i ) } ` , node . id , {
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
durationMs : duration ,
} ) ;
2026-04-01 21:24:13 +00:00
// Completion signal detected — exit the loop.
// For interactive loops: only honor the signal when the AI had user input to evaluate
// (i.e., this is a resume iteration with loopUserInput). On the first iteration of a
// fresh interactive loop, the user hasn't seen anything yet — always gate first.
// For non-interactive loops: the AI signals task completion at any point.
const interactiveFirstRun = loop . interactive && ! isLoopResume ;
if ( completionDetected && ! interactiveFirstRun ) {
await safeSendMessage (
platform ,
conversationId ,
` Loop node ' ${ node . id } ' completed after ${ String ( i ) } iteration ${ i > 1 ? 's' : '' } ` ,
msgContext
) ;
// Write node_completed event so resume logic (getCompletedDagNodeOutputs) knows this
// node is done. Without this, a resumed DAG would re-enter the loop node.
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_completed' ,
step_name : node.id ,
2026-04-06 16:39:18 +00:00
data : {
duration_ms : Date.now ( ) - iterationStart ,
node_output : lastIterationOutput ,
. . . ( loopTotalCostUsd !== undefined ? { cost_usd : loopTotalCostUsd } : { } ) ,
. . . ( loopFinalStopReason ? { stop_reason : loopFinalStopReason } : { } ) ,
. . . ( loopTotalNumTurns !== undefined ? { num_turns : loopTotalNumTurns } : { } ) ,
} ,
2026-04-01 21:24:13 +00:00
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'node_completed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.id ,
duration : Date.now ( ) - iterationStart ,
2026-04-06 16:39:18 +00:00
. . . ( loopTotalCostUsd !== undefined ? { costUsd : loopTotalCostUsd } : { } ) ,
. . . ( loopFinalStopReason ? { stopReason : loopFinalStopReason } : { } ) ,
. . . ( loopTotalNumTurns !== undefined ? { numTurns : loopTotalNumTurns } : { } ) ,
2026-04-01 21:24:13 +00:00
} ) ;
2026-04-06 16:39:18 +00:00
return {
state : 'completed' ,
output : lastIterationOutput ,
sessionId : currentSessionId ,
costUsd : loopTotalCostUsd ,
} ;
2026-04-01 21:24:13 +00:00
}
// Interactive loop gate — pause after every iteration where the AI did NOT emit the
// completion signal. The user reviews the AI's output and provides feedback or approval.
// On approval, the AI will emit the signal in the next iteration, exiting above.
2026-04-01 15:33:34 +00:00
if ( loop . interactive && loop . gate_message ) {
const gateMsg =
` \ u23f8 **Input required** (loop \` ${ node . id } \` , iteration ${ String ( i ) } ): ${ loop . gate_message } \ n \ n ` +
` Run ID: \` ${ workflowRun . id } \` \ n ` +
` Respond: \` /workflow approve ${ workflowRun . id } <your feedback> \` | Cancel: \` /workflow reject ${ workflowRun . id } \` ` ;
2026-04-01 16:17:56 +00:00
const gateSent = await safeSendMessage ( platform , conversationId , gateMsg , {
2026-04-01 15:33:34 +00:00
workflowId : workflowRun.id ,
nodeName : node.id ,
} ) ;
2026-04-01 16:17:56 +00:00
if ( ! gateSent ) {
// Gate message failed to deliver — do not pause; fail the node so the user
// sees a clear error rather than a silently orphaned paused run.
getLog ( ) . error (
{ nodeId : node.id , workflowRunId : workflowRun.id , iteration : i } ,
'loop_node.gate_message_send_failed'
) ;
return {
state : 'failed' ,
output : lastIterationOutput ,
error : ` Loop gate message failed to deliver for node ' ${ node . id } ' — cannot pause safely ` ,
} ;
}
2026-04-01 15:33:34 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'approval_requested' ,
step_name : node.id ,
data : { message : loop.gate_message , iteration : i } ,
} )
. catch ( ( err : Error ) = > {
logEventStoreError ( err , i ) ;
} ) ;
await deps . store . pauseWorkflowRun ( workflowRun . id , {
nodeId : node.id ,
message : loop.gate_message ,
type : 'interactive_loop' ,
iteration : i ,
sessionId : currentSessionId ,
} ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'approval_pending' ,
runId : workflowRun.id ,
nodeId : node.id ,
message : loop.gate_message ,
} ) ;
2026-04-01 16:17:56 +00:00
// Return completed — the between-layer status check sees 'paused' and halts cleanly.
// This mirrors the approval-node pattern, preventing false "DAG nodes failed" warnings
// in multi-node workflows. Resume correctness relies on the 'paused' DB status, not
// on the node's output state.
2026-04-06 16:39:18 +00:00
return { state : 'completed' , output : lastIterationOutput , costUsd : loopTotalCostUsd } ;
2026-04-01 15:33:34 +00:00
}
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
// Max iterations exceeded
const errorMsg = ` Loop node ' ${ node . id } ' exceeded max iterations ( ${ String ( loop . max_iterations ) } ) without completion signal ' ${ loop . until } ' ` ;
getLog ( ) . warn (
{ nodeId : node.id , maxIterations : loop.max_iterations , signal : loop.until } ,
'loop_node.max_iterations_reached'
) ;
await safeSendMessage ( platform , conversationId , errorMsg , msgContext ) ;
2026-04-06 16:39:18 +00:00
return {
state : 'failed' ,
output : lastIterationOutput ,
error : errorMsg ,
costUsd : loopTotalCostUsd ,
} ;
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
}
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
/ * *
* Execute an approval node — pauses workflow for human review .
* On rejection resume ( when on_reject is configured ) : runs the on_reject prompt via AI ,
* then re - pauses at the approval gate . After max_attempts rejections , cancels normally .
* /
async function executeApprovalNode (
node : ApprovalNode ,
workflowRun : WorkflowRun ,
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
conversationId : string ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
workflowProvider : string ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
workflowModel : string | undefined ,
cwd : string ,
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
2026-04-06 13:26:59 +00:00
docsDir : string ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
nodeOutputs : Map < string , NodeOutput > ,
config : WorkflowConfig ,
2026-04-06 15:59:36 +00:00
workflowLevelOptions : WorkflowLevelOptions ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
configuredCommandFolder? : string ,
issueContext? : string
) : Promise < NodeOutput > {
const msgContext = { workflowId : workflowRun.id , nodeName : node.id } ;
// Detect rejection resume — check metadata for rejection_reason set by reject handlers
const rawApproval = workflowRun . metadata ? . approval ;
const approvalMeta = isApprovalContext ( rawApproval ) ? rawApproval : undefined ;
const rawRejection = workflowRun . metadata ? . rejection_reason ;
const rejectionReason =
approvalMeta ? . type === 'approval' &&
approvalMeta . nodeId === node . id &&
typeof rawRejection === 'string' &&
rawRejection !== ''
? rawRejection
: '' ;
// On rejection resume with on_reject configured: run the on_reject prompt via AI
2026-04-02 08:05:19 +00:00
if ( rejectionReason !== '' && node . approval . on_reject ) {
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
const maxAttempts = node . approval . on_reject . max_attempts ? ? 3 ;
const rejectionCount = ( workflowRun . metadata ? . rejection_count as number | undefined ) ? ? 0 ;
// Check if max attempts exhausted
if ( rejectionCount >= maxAttempts ) {
await deps . store . cancelWorkflowRun ( workflowRun . id ) ;
fix: address review findings for on_reject and capture_response
- emit workflow_cancelled event + SSE notification on max-attempts exhaustion
in executeApprovalNode (dag-executor.ts)
- add missing Record<string, unknown> type annotation on metadataUpdate in
orchestrator-agent.ts
- wrap CLI hasOnReject DB calls in try/catch matching the else-branch pattern
- add tests for $REJECTION_REASON substitution in executor-shared.test.ts
- add tests for command-handler reject on_reject branch and captureResponse
approve behavior
- add tests for API approve/reject endpoints (on_reject routing, max attempts,
captureResponse, 404/400 error cases)
- add tests for workflowRejectCommand (on_reject, working_path guard, max
attempts, plain cancel)
- add approvalOnRejectSchema validation tests (empty prompt, out-of-range
max_attempts)
- update docs/approval-nodes.md: capture_response opt-in behavior, new fields
table, conditional rejection behavior, on_reject lifecycle section
- update docs/authoring-workflows.md: add $REJECTION_REASON and $LOOP_USER_INPUT
to variable table, update Human-in-the-Loop pattern
- add $REJECTION_REASON to CLAUDE.md variable substitution list
2026-04-02 08:01:37 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'workflow_cancelled' ,
step_name : node.id ,
data : { reason : ` max_attempts ( ${ String ( maxAttempts ) } ) exhausted ` } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'workflow_cancelled' } ,
'workflow.event_persist_failed'
) ;
} ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'workflow_cancelled' ,
runId : workflowRun.id ,
nodeId : node.id ,
reason : ` max_attempts ( ${ String ( maxAttempts ) } ) exhausted ` ,
} ) ;
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
const cancelMsg = ` ❌ Approval node \` ${ node . id } \` cancelled after ${ String ( maxAttempts ) } rejections. ` ;
await safeSendMessage ( platform , conversationId , cancelMsg , msgContext ) ;
return { state : 'completed' as const , output : '' } ;
}
// Run the on_reject prompt via AI
const { prompt : substitutedPrompt } = substituteWorkflowVariables (
node . approval . on_reject . prompt ,
workflowRun . id ,
workflowRun . user_message ? ? '' ,
artifactsDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
issueContext ,
undefined , // loopUserInput
rejectionReason
) ;
// Build a synthetic PromptNode to reuse executeNodeInternal
const syntheticNode : PromptNode = {
id : node.id ,
prompt : substituteNodeOutputRefs ( substitutedPrompt , nodeOutputs ) ,
. . . ( node . depends_on ? { depends_on : node.depends_on } : { } ) ,
. . . ( node . idle_timeout ? { idle_timeout : node.idle_timeout } : { } ) ,
} ;
const { provider , options : nodeOptions } = await resolveNodeProviderAndModel (
syntheticNode ,
workflowProvider ,
workflowModel ,
config ,
platform ,
conversationId ,
workflowRun . id ,
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
cwd ,
2026-04-13 13:10:48 +00:00
workflowLevelOptions
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
) ;
const output = await executeNodeInternal (
deps ,
platform ,
conversationId ,
cwd ,
workflowRun ,
syntheticNode ,
provider ,
nodeOptions ,
artifactsDir ,
logDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
nodeOutputs ,
undefined , // fresh session
configuredCommandFolder ,
issueContext
) ;
if ( output . state === 'failed' ) {
return output ;
}
// Fall through to re-pause at the approval gate
}
// Standard approval gate — send message and pause
const approvalMsg =
` ⏸ **Approval required**: ${ node . approval . message } \ n \ n ` +
` Run ID: \` ${ workflowRun . id } \` \ n ` +
` Approve: \` /workflow approve ${ workflowRun . id } \` | Reject: \` /workflow reject ${ workflowRun . id } \` ` ;
await safeSendMessage ( platform , conversationId , approvalMsg , msgContext ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'approval_requested' ,
step_name : node.id ,
data : { message : node.approval.message } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'approval_requested' } ,
'workflow.event_persist_failed'
) ;
} ) ;
await deps . store . pauseWorkflowRun ( workflowRun . id , {
message : node.approval.message ,
nodeId : node.id ,
type : 'approval' ,
captureResponse : node.approval.capture_response ,
onRejectPrompt : node.approval.on_reject?.prompt ,
onRejectMaxAttempts : node.approval.on_reject?.max_attempts ,
} ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'approval_pending' ,
runId : workflowRun.id ,
nodeId : node.id ,
message : node.approval.message ,
} ) ;
// Return completed — the between-layer status check will see 'paused' and break.
// On resume, the approve endpoint writes a real node_completed event with the user's response.
return { state : 'completed' as const , output : '' } ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
/ * *
* Execute a complete DAG workflow .
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
* Called from executeWorkflow ( ) in executor . ts .
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
* /
export async function executeDagWorkflow (
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps : WorkflowDeps ,
platform : IWorkflowPlatform ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
conversationId : string ,
cwd : string ,
2026-04-06 15:59:36 +00:00
workflow : { name : string ; nodes : readonly DagNode [ ] } & WorkflowLevelOptions ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
workflowRun : WorkflowRun ,
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
workflowProvider : string ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
workflowModel : string | undefined ,
artifactsDir : string ,
logDir : string ,
baseBranch : string ,
2026-04-06 13:26:59 +00:00
docsDir : string ,
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
config : WorkflowConfig ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
configuredCommandFolder? : string ,
2026-03-24 09:15:07 +00:00
issueContext? : string ,
priorCompletedNodes? : Map < string , string >
2026-03-31 00:49:40 +00:00
) : Promise < string | undefined > {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const dagStartTime = Date . now ( ) ;
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
const workflowLevelOptions = {
effort : workflow.effort ,
thinking : workflow.thinking ,
fallbackModel : workflow.fallbackModel ,
betas : workflow.betas ,
sandbox : workflow.sandbox ,
} ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const layers = buildTopologicalLayers ( workflow . nodes ) ;
const nodeOutputs = new Map < string , NodeOutput > ( ) ;
2026-03-24 09:15:07 +00:00
// Pre-populate nodeOutputs from prior run so already-completed nodes are
// treated as done for trigger-rule and $nodeId.output substitution purposes.
if ( priorCompletedNodes && priorCompletedNodes . size > 0 ) {
for ( const [ nodeId , output ] of priorCompletedNodes ) {
nodeOutputs . set ( nodeId , { state : 'completed' , output } ) ;
}
getLog ( ) . info (
{ workflowRunId : workflowRun.id , priorCompletedCount : priorCompletedNodes.size } ,
'dag.workflow_resume_prepopulated'
) ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
getLog ( ) . info (
{
workflowName : workflow.name ,
nodeCount : workflow.nodes.length ,
layerCount : layers.length ,
2026-03-19 07:35:31 +00:00
hasIssueContext : ! ! issueContext ,
issueContextLength : issueContext?.length ? ? 0 ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} ,
'dag_workflow_starting'
) ;
// Session threading: for sequential single-node layers, thread the session forward.
// For parallel layers (>1 node), always fresh (can't share a session).
let lastSequentialSessionId : string | undefined ;
2026-04-06 17:03:47 +00:00
// Note: accumulates cost for this invocation only. If this is a resume, nodes skipped
// from the prior run are not included — total_cost_usd will reflect resumed-portion cost only.
2026-04-06 16:39:18 +00:00
let totalCostUsd = 0 ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
for ( let layerIdx = 0 ; layerIdx < layers . length ; layerIdx ++ ) {
const layer = layers [ layerIdx ] ;
const isParallelLayer = layer . length > 1 ;
if ( isParallelLayer ) {
lastSequentialSessionId = undefined ; // reset — parallel nodes can't share sessions
}
// Execute all nodes in the layer concurrently
const layerResults = await Promise . allSettled (
2026-04-06 16:39:18 +00:00
layer . map ( async ( node ) : Promise < { nodeId : string ; output : NodeExecutionResult } > = > {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
try {
2026-03-24 09:15:07 +00:00
// 0. Skip if this node completed successfully in a prior run (resume path)
if ( priorCompletedNodes ? . has ( node . id ) ) {
getLog ( ) . info ( { nodeId : node.id } , 'dag.node_skipped_prior_success' ) ;
await logNodeSkip ( logDir , workflowRun . id , node . id , 'prior_success' ) . catch (
( err : Error ) = > {
getLog ( ) . warn ( { err , nodeId : node.id } , 'dag.node_skip_log_write_failed' ) ;
}
) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_skipped_prior_success' ,
step_name : node.id ,
data : { reason : 'prior_success' } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_skipped_prior_success' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitterPrior = getWorkflowEventEmitter ( ) ;
emitterPrior . emit ( {
type : 'node_skipped' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
reason : 'prior_success' ,
} ) ;
// Return the pre-populated output (already in nodeOutputs)
return {
nodeId : node.id ,
output : nodeOutputs.get ( node . id ) ? ? { state : 'skipped' as const , output : '' } ,
} ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
// 1. Evaluate trigger rule
const triggerDecision = checkTriggerRule ( node , nodeOutputs ) ;
if ( triggerDecision === 'skip' ) {
getLog ( ) . info ( { nodeId : node.id , reason : 'trigger_rule' } , 'dag_node_skipped' ) ;
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
await logNodeSkip ( logDir , workflowRun . id , node . id , 'trigger_rule' ) . catch (
( err : Error ) = > {
getLog ( ) . warn ( { err , nodeId : node.id } , 'dag.node_skip_log_write_failed' ) ;
}
) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_skipped' ,
step_name : node.id ,
data : { reason : 'trigger_rule' } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_skipped' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_skipped' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
reason : 'trigger_rule' ,
} ) ;
return { nodeId : node.id , output : { state : 'skipped' as const , output : '' } } ;
}
// 2. Evaluate when: condition
if ( node . when !== undefined ) {
const { result : conditionPasses , parsed : conditionParsed } = evaluateCondition (
node . when ,
nodeOutputs
) ;
if ( ! conditionParsed ) {
2026-04-02 07:28:14 +00:00
const parseErrMsg = ` \ u26a0 \ ufe0f Node ' ${ node . id } ': unparseable \` when: \` expression " ${ node . when } " \ u2014 node skipped (fail-closed). Check syntax: \` $ nodeId.output == 'VALUE' \` , \` $ nodeId.output > '5' \` , or compound \` $ a.output == 'X' && $ b.output != 'Y' \` . ` ;
2026-03-13 07:30:01 +00:00
await safeSendMessage ( platform , conversationId , parseErrMsg , {
workflowId : workflowRun.id ,
nodeName : node.id ,
} ) ;
getLog ( ) . error (
{ nodeId : node.id , when : node.when } ,
'dag_node_skipped_condition_parse_error'
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
) ;
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
await logNodeSkip (
logDir ,
workflowRun . id ,
node . id ,
'when_condition_parse_error'
) . catch ( ( err : Error ) = > {
getLog ( ) . warn ( { err , nodeId : node.id } , 'dag.node_skip_log_write_failed' ) ;
} ) ;
2026-03-13 07:30:01 +00:00
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_skipped' ,
step_name : node.id ,
data : { reason : 'when_condition_parse_error' , expr : node.when } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_skipped' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_skipped' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
reason : 'when_condition_parse_error' ,
} ) ;
return { nodeId : node.id , output : { state : 'skipped' as const , output : '' } } ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
if ( ! conditionPasses ) {
getLog ( ) . info ( { nodeId : node.id , when : node.when } , 'dag_node_skipped_condition' ) ;
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
await logNodeSkip ( logDir , workflowRun . id , node . id , 'when_condition' ) . catch (
( err : Error ) = > {
getLog ( ) . warn ( { err , nodeId : node.id } , 'dag.node_skip_log_write_failed' ) ;
}
) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_skipped' ,
step_name : node.id ,
data : { reason : 'when_condition' , expr : node.when } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'node_skipped' } ,
'workflow_event_persist_failed'
) ;
} ) ;
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'node_skipped' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
reason : 'when_condition' ,
} ) ;
return {
nodeId : node.id ,
output : { state : 'skipped' as const , output : '' } ,
} ;
}
}
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
// 3. Bash node dispatch — no AI, no session
if ( isBashNode ( node ) ) {
const output = await executeBashNode (
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
platform ,
conversationId ,
cwd ,
workflowRun ,
node ,
artifactsDir ,
logDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
nodeOutputs ,
2026-04-13 12:21:57 +00:00
issueContext ,
config . envVars
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
) ;
return { nodeId : node.id , output } ;
}
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
// 3b. Loop node dispatch — manages its own AI sessions and iteration
if ( isLoopNode ( node ) ) {
2026-04-06 18:02:17 +00:00
// Resolve per-node provider/model overrides (same logic as other node types)
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
const loopProvider : string =
2026-04-13 13:10:48 +00:00
node . provider ? ? inferProviderFromModel ( node . model , workflowProvider ) ;
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
const loopAssistantConfig = config . assistants [ loopProvider ] ;
const loopModel : string | undefined =
2026-04-06 18:02:17 +00:00
node . model ? ?
( loopProvider === workflowProvider
? workflowModel
feat: Phase 2 — community-friendly provider registry system (#1195)
* feat: replace hardcoded provider factory with typed registry system
Replace the built-in-only factory switch with a typed ProviderRegistration
registry where entries carry metadata (displayName, capabilities,
isModelCompatible) alongside the factory function. This enables community
providers to register without modifying core code.
- Add ProviderRegistration and ProviderInfo types to contract layer
- Create registry.ts with register/get/list/clear API, delete factory.ts
- Bootstrap registerBuiltinProviders() at server and CLI entrypoints
- Widen provider unions from 'claude' | 'codex' to string across schemas,
config types, deps, executors, and API validation
- Replace hardcoded model-validation with registry-driven isModelCompatible
and inferProviderFromModel (built-in only inference)
- Add GET /api/providers endpoint returning registry metadata
- Dynamic provider dropdowns in Web UI (BuilderToolbar, NodeInspector,
WorkflowBuilder, SettingsPage) via useProviders hook
- Dynamic provider selection in CLI setup command
- Registry test suite covering full lifecycle
* feat: generalize assistant config and tighten registry validation
- Add ProviderDefaults/ProviderDefaultsMap generic types to contract layer
- Add index signatures to ClaudeProviderDefaults/CodexProviderDefaults
- Introduce AssistantDefaults/AssistantDefaultsConfig intersection types
that combine ProviderDefaultsMap with typed built-in entries
- Replace hardcoded claude/codex config merging with generic
mergeAssistantDefaults() that iterates all provider entries
- Replace hardcoded toSafeConfig projection with generic
toSafeAssistantDefaults() that strips server-internal fields
- Validate provider strings at all config-entry surfaces: env override,
global config, repo config all throw on unknown providers
- Validate provider on PATCH /api/config/assistants (400 on unknown)
- Move validator.ts from hardcoded Codex checks to capability-driven
warnings using registry getProviderCapabilities()
- Remove resolveProvider() default to 'claude' — returns undefined when
no provider is set, skipping capability warnings for unresolved nodes
- Widen config API schemas to generic Record<string, ProviderDefaults>
- Rewrite SettingsPage to iterate providers dynamically with built-in
specific UI for Claude/Codex and generic JSON view for community
- Extract bootstrap to provider-bootstrap modules in CLI and server
- Remove all as Record<...> casts from dag-executor, executor,
orchestrator — clean indexing via ProviderDefaultsMap intersection
* fix: remove remaining hardcoded provider assumptions and regenerate types
- Replace hardcoded 'claude' defaults in CLI setup with registry lookup
(getRegisteredProviders().find(p => p.builtIn)?.id)
- Replace hardcoded 'claude' default in clone.ts folder detection with
registry-driven fallback
- Update config YAML comment from "claude or codex" to "registered provider"
- Make bootstrap test assertions use toContain instead of exact toEqual
so they don't break when community providers are registered
- Widen validator.test.ts helper from 'claude' | 'codex' to string
- Remove unnecessary type casts in NodeInspector, WorkflowBuilder,
SettingsPage now that generated types use string
- Regenerate api.generated.d.ts from updated OpenAPI spec — all provider
fields are now string instead of 'claude' | 'codex' union
* fix: address PR review findings — consistency, tests, docs
Critical fixes:
- isModelCompatible now throws on unknown providers (fail-fast parity
with getProviderCapabilities) instead of silently returning true
- Schema provider fields use z.string().trim().min(1) to reject
whitespace-only values
- validator.ts resolveProvider accepts defaultProvider param so
capability warnings fire for config-inherited providers
- PATCH /api/config/assistants validates assistants keys against
registry (rejects unknown provider IDs in the map)
YAGNI cleanup:
- Delete provider-bootstrap.ts wrappers in CLI and server — call
registerBuiltinProviders() directly
- Remove no-op .map(provider => provider) in SettingsPage
Test coverage:
- Add GET /api/providers endpoint tests (shape, projection, capabilities)
- Add config-loader throw-path tests for unknown providers in env var,
global config, and repo config
- Add isModelCompatible throw test for unknown providers
Docs:
- CLAUDE.md: factory.ts → registry.ts in directory tree, add
GET /api/providers to API endpoints section
- .env.example: update DEFAULT_AI_ASSISTANT comment
- docs-web configuration reference: update provider constraint docs
UI:
- Settings default-assistant dropdown uses allProviderEntries fallback
(no longer silently empty on API failure)
- clearRegistry marked @internal in JSDoc
* fix: use registry defaults in getDefaults/registerProject, document type design
- getDefaults() initializes assistant defaults from registered providers
instead of hardcoding { claude: {}, codex: {} }
- getDefaults() uses first registered built-in as default assistant
instead of hardcoding 'claude'
- handleRegisterProject uses config.assistant instead of hardcoded 'claude'
for new codebase ai_assistant_type
- Document AssistantDefaults/AssistantDefaultsConfig intersection types:
built-in keys are typed for parseClaudeConfig/parseCodexConfig type
safety; community providers use the generic [string] index
- Document WorkflowConfig.assistants intersection type with same rationale
* docs: update stale provider references to reflect registry system
- architecture.md: DB schema comment now says 'registered provider'
- first-workflow.md: provider field accepts any registered provider
- quick-reference.md: provider type changed from enum to string
- authoring-workflows.md: provider type changed from enum to string
- title-generator.ts: @param doc updated from 'claude or codex' to
generic provider identifier
* docs: fix remaining stale provider references in quick-reference and authoring guide
- quick-reference.md: per-node provider type changed from enum to string
- quick-reference.md: model mismatch guidance updated for registry pattern
- authoring-workflows.md: provider comment says 'any registered provider'
2026-04-13 18:27:11 +00:00
: ( loopAssistantConfig ? . model as string | undefined ) ) ;
2026-04-06 18:02:17 +00:00
if ( ! isModelCompatible ( loopProvider , loopModel ) ) {
return {
nodeId : node.id ,
output : {
state : 'failed' as const ,
output : '' ,
error : ` Node ' ${ node . id } ': model " ${ loopModel ? ? 'default' } " is not compatible with provider " ${ loopProvider } " ` ,
} ,
} ;
}
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
const output = await executeLoopNode (
deps ,
platform ,
conversationId ,
cwd ,
workflowRun ,
node ,
2026-04-06 18:02:17 +00:00
loopProvider ,
loopModel ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
artifactsDir ,
logDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
nodeOutputs ,
config ,
refactor: extract providers from @archon/core into @archon/providers (#1137)
* refactor: extract providers from @archon/core into @archon/providers
Move Claude and Codex provider implementations, factory, and SDK
dependencies into a new @archon/providers package. This establishes a
clean boundary: providers own SDK translation, core owns business logic.
Key changes:
- New @archon/providers package with zero-dep contract layer (types.ts)
- @archon/workflows imports from @archon/providers/types — no mirror types
- dag-executor delegates option building to providers via nodeConfig
- IAgentProvider gains getCapabilities() for provider-agnostic warnings
- @archon/core no longer depends on SDK packages directly
- UnknownProviderError standardizes error shape across all surfaces
Zero user-facing changes — same providers, same config, same behavior.
* refactor: remove config type duplication and backward-compat re-exports
Address review findings:
- Move ClaudeProviderDefaults and CodexProviderDefaults to the
@archon/providers/types contract layer as the single source of truth.
@archon/core/config/config-types.ts now imports from there.
- Remove provider re-exports from @archon/core (index.ts and types/).
Consumers should import from @archon/providers directly.
- Update @archon/server to depend on @archon/providers for MessageChunk.
* refactor: move structured output validation into providers
Each provider now normalizes its own structured output semantics:
- Claude already yields structuredOutput from the SDK's native field
- Codex now parses inline agent_message text as JSON when outputFormat
is set, populating structuredOutput on the result chunk
This eliminates the last provider === 'codex' branch from dag-executor,
making it fully provider-agnostic. The dag-executor checks structuredOutput
uniformly regardless of provider.
Also removes the ClaudeCodexProviderDefaults deprecated alias — all
consumers now use ClaudeProviderDefaults directly.
* fix: address PR review — restore warnings, fix loop options, cleanup
Critical fixes:
- Restore MCP missing env vars user-facing warning (was silently dropped)
- Restore Haiku + MCP tool search warning
- Fix buildLoopNodeOptions to pass workflow-level nodeConfig (effort,
thinking, betas, sandbox were silently lost for loop nodes)
- Add TODO(#1135) comments documenting env-leak gate gap
Cleanup:
- Remove backward-compat type aliases from deps.ts (keep WorkflowTokenUsage)
- Remove 26 unnecessary eslint-disable comments from test files
- Trim internal helpers from providers barrel (withFirstMessageTimeout,
getProcessUid, loadMcpConfig, buildSDKHooksFromYAML)
- Add @archon/providers dep to CLI package.json
- Fix 8 stale documentation paths pointing to deleted core/src/providers/
- Add E2E smoke test workflows for both Claude and Codex providers
* fix: forward provider system warnings to users in dag-executor
The dag-executor only forwarded system chunks starting with
"MCP server connection failed:" — all other provider warnings
(missing env vars, Haiku+MCP, structured output issues) were
logged but never reached the user.
Now forwards all system chunks starting with ⚠️ (the prefix
providers use for user-actionable warnings).
* fix: add providers package to Dockerfile and fix CI module resolution
- Add packages/providers/ to all three Dockerfile stages (deps,
production package.json copy, production source copy)
- Replace wildcard export map (./*) with explicit subpath entries
to fix module resolution in CI (bun workspace linking)
* chore: update bun.lock for providers package exports
2026-04-13 06:21:36 +00:00
issueContext ,
workflowLevelOptions
feat(workflows)!: replace standalone loop with DAG loop node (#785)
* feat(workflows): add loop node type to DAG workflows
Add LoopNode as a fourth DAG node type alongside command, prompt, and
bash. Loop nodes run an AI prompt repeatedly until a completion signal
is detected (LLM-decided via <promise>SIGNAL</promise>) or a
deterministic bash condition succeeds (until_bash exit 0).
This enables Ralph-style autonomous iteration as a composable node
within DAG workflows — upstream nodes can produce plans/task lists
that feed into the loop, and downstream nodes can act on the loop's
output via $nodeId.output substitution.
Changes:
- Add LoopNodeConfig, LoopNode interface, isLoopNode type guard
- Add loop branch in parseDagNode with full validation
- Extract detectCompletionSignal/stripCompletionTags to executor-shared
- Add executeLoopNode function in dag-executor with iteration logic
- Add nodeId field to loop iteration event interfaces
- Add 17 new tests (9 loader + 8 executor)
- Add archon-test-loop-dag and archon-ralph-dag default workflows
The standalone loop: workflow type is preserved but deprecated.
* refactor(workflows): rewrite archon-ralph-dag prompt to match command quality bar
Expand the loop prompt from ~75 lines to ~430 lines with:
- 7 numbered phases with checkpoints (matching archon-implement.md pattern)
- Environment setup: dependency install, CLAUDE.md reading, git state check
- Explicit DO/DON'T implementation rules
- Per-failure-type validation handling (type-check, lint, tests, format)
- Acceptance criteria verification before commit
- Exact commit message template with heredoc format
- Edge case handling (validation loops, blocked stories, dirty state, large stories)
- File format specs for prd.json schema and progress.txt structure
- Critical fix: "context is stale — re-read from disk" for fresh_context loops
Also improved bash setup node (dep install, structured output delimiters,
story counts) and report node (git log/diff stats, PR status check).
* feat(workflows)!: remove standalone loop workflow type
BREAKING: Standalone `loop:` workflows are no longer supported.
Loop iteration is now exclusively a DAG node type (LoopNode).
Existing loop workflows should be migrated to DAG workflows
with loop nodes — see archon-ralph-dag.yaml for the pattern.
Removed:
- LoopConfig type and LoopWorkflow from WorkflowDefinition union
- executeLoopWorkflow function (~600 lines) from executor.ts
- Loop dispatch in executeWorkflow
- Top-level loop: parsing in loader (now returns clear error message)
- archon-ralph-fresh.yaml, archon-ralph-stateful.yaml, archon-test-loop.yaml
- LoopEditor.tsx and loop mode from WorkflowBuilder UI
- ~900 lines of standalone loop tests
Kept (for DAG loop nodes):
- LoopNodeConfig, LoopNode, isLoopNode
- executeLoopNode in dag-executor.ts
- Loop iteration events in store/event-emitter
- isLoop tracking in web UI workflow store (fires for DAG loop nodes)
* fix: address all review findings for loop-dag-node PR
- Fix missing isDagWorkflow import in command-handler.ts (shipping bug)
- Wrap substituteWorkflowVariables and getAssistantClient in try-catch
with structured error output in executeLoopNode
- Add onTimeout callback for idle timeout (log + user notification + abort)
- Add cancellation user notification before returning failed state
- Differentiate until_bash ENOENT/system errors from expected non-zero exit
- Use logDir for per-iteration AI output logging (logAssistant, logTool,
logStepComplete, tool_called/tool_completed events, sendStructuredEvent)
- Reject retry: on loop nodes at load time (executor doesn't apply it)
- Remove dead isLoop field from WorkflowStartedEvent
- Fix stale error message "DAG/loop dispatch" -> "DAG dispatch"
- Fix stale commitWorkflowArtifacts doc referencing "loop-based"
- Fix archon-ralph-dag.yaml referencing deleted workflows
- Update CLAUDE.md: "Two execution modes", add loop node to DAG description
- Extract parseIdleTimeout helper (3 copies -> 1 in loader.ts)
- Use isLoopNode() type guard in validateDagStructure
- Simplify buildLoopNodeOptions with conditional spread
- Restore loop?: never on StepWorkflow for type safety
- Add tests: AI error mid-iteration, plain signal detection, false positive
- Fix stale test assertion for standalone loop rejection message
2026-03-25 10:37:14 +00:00
) ;
return { nodeId : node.id , output } ;
}
feat: add approval gate node type for human-in-the-loop workflows (#888)
* feat: add approval gate node type for human-in-the-loop workflows
Add `approval` as a fifth DAG node type that pauses workflow execution
until a human approves or rejects. Built on existing resume infrastructure.
- New `approval` node type in YAML workflows with `message` field
- `paused` workflow status (non-terminal, resumable)
- Approve/reject via REST API, CLI, chat commands, and Web UI
- Approval comment available as `$node.output` in downstream nodes
- Dashboard shows amber pulsing badge with approval message for paused runs
- Path guard blocks new workflows on paused worktrees
* fix: resolve 7 bugs in approval gate implementation
Critical:
- Parse metadata JSON on SQLite (returned as TEXT string, not object)
- Suppress spurious "Workflow stopped (paused)" message after approval gates
Medium:
- CLI exits 0 on pause (not error code 1)
- workflow status shows paused runs alongside running
- Docs clarify auto-resume is CLI-only; API/chat mark resumable
Low:
- Skip completed_at when transitioning to failed for approval resume
- Warn on AI fields (model, hooks, etc.) set on approval nodes
- Add rowCount check to updateWorkflowRun
Also: extract ApprovalContext type, add .min(1) to approval.message,
fix stale "Only failed runs" error messages, fix log domain prefix,
add recovery instructions to CLI approve error message.
2026-03-30 14:27:11 +00:00
// 3c. Approval node dispatch — pauses workflow for human review
if ( isApprovalNode ( node ) ) {
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
const output = await executeApprovalNode (
node ,
workflowRun ,
deps ,
platform ,
conversationId ,
workflowProvider ,
workflowModel ,
cwd ,
artifactsDir ,
logDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
nodeOutputs ,
config ,
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
workflowLevelOptions ,
feat(workflows): add capture_response and on_reject retry to approval nodes (#936)
Approval nodes can now capture the reviewer's comment as $nodeId.output
(via capture_response: true) and optionally retry on rejection instead of
cancelling (via on_reject: { prompt, max_attempts }). This enables
iterative human-AI review cycles without needing interactive loop nodes.
Changes:
- Add approvalOnRejectSchema and extend approval node schema with
capture_response and on_reject fields
- Extend ApprovalContext with captureResponse, onRejectPrompt,
onRejectMaxAttempts (stored at pause time for reject handlers)
- Add $REJECTION_REASON variable to substituteWorkflowVariables
- Extract executeApprovalNode function with rejection resume logic
- Update all 4 approve handlers (CLI, command-handler, orchestrator,
server API) to use captureResponse and clear rejection state
- Update all 3 reject handlers (CLI, command-handler, server API) to
check onRejectPrompt and retry instead of cancel when configured
- Add 5 tests for approval node behavior (fresh pause, capture_response,
on_reject resume, max_attempts exhaustion, max_attempts=1)
Fixes #936
2026-04-02 07:36:57 +00:00
configuredCommandFolder ,
issueContext
) ;
return { nodeId : node.id , output } ;
feat: add approval gate node type for human-in-the-loop workflows (#888)
* feat: add approval gate node type for human-in-the-loop workflows
Add `approval` as a fifth DAG node type that pauses workflow execution
until a human approves or rejects. Built on existing resume infrastructure.
- New `approval` node type in YAML workflows with `message` field
- `paused` workflow status (non-terminal, resumable)
- Approve/reject via REST API, CLI, chat commands, and Web UI
- Approval comment available as `$node.output` in downstream nodes
- Dashboard shows amber pulsing badge with approval message for paused runs
- Path guard blocks new workflows on paused worktrees
* fix: resolve 7 bugs in approval gate implementation
Critical:
- Parse metadata JSON on SQLite (returned as TEXT string, not object)
- Suppress spurious "Workflow stopped (paused)" message after approval gates
Medium:
- CLI exits 0 on pause (not error code 1)
- workflow status shows paused runs alongside running
- Docs clarify auto-resume is CLI-only; API/chat mark resumable
Low:
- Skip completed_at when transitioning to failed for approval resume
- Warn on AI fields (model, hooks, etc.) set on approval nodes
- Add rowCount check to updateWorkflowRun
Also: extract ApprovalContext type, add .min(1) to approval.message,
fix stale "Only failed runs" error messages, fix log domain prefix,
add recovery instructions to CLI approve error message.
2026-03-30 14:27:11 +00:00
}
2026-04-01 09:02:57 +00:00
// 3d. Cancel node dispatch — terminates the workflow run
if ( isCancelNode ( node ) ) {
const reason = substituteNodeOutputRefs ( node . cancel , nodeOutputs ) ;
const cancelMsg = ` \ u274c **Workflow cancelled** (node \` ${ node . id } \` ): ${ reason } ` ;
await safeSendMessage ( platform , conversationId , cancelMsg , {
workflowId : workflowRun.id ,
nodeName : node.id ,
} ) ;
deps . store
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'workflow_cancelled' ,
step_name : node.id ,
data : { reason } ,
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'workflow_cancelled' } ,
'workflow.event_persist_failed'
) ;
} ) ;
await deps . store . cancelWorkflowRun ( workflowRun . id ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'workflow_cancelled' ,
runId : workflowRun.id ,
nodeId : node.id ,
reason ,
} ) ;
// Return completed — the between-layer status check will see 'cancelled' and break.
return { nodeId : node.id , output : { state : 'completed' as const , output : reason } } ;
}
2026-04-09 11:48:02 +00:00
// 3e. Script node dispatch — runs via bun or uv
if ( isScriptNode ( node ) ) {
const output = await executeScriptNode (
deps ,
platform ,
conversationId ,
cwd ,
workflowRun ,
node ,
artifactsDir ,
logDir ,
baseBranch ,
docsDir ,
nodeOutputs ,
2026-04-13 12:21:57 +00:00
issueContext ,
config . envVars
2026-04-09 11:48:02 +00:00
) ;
return { nodeId : node.id , output } ;
}
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
// 4. Resolve per-node provider/model/options
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const { provider , options : nodeOptions } = await resolveNodeProviderAndModel (
node ,
workflowProvider ,
workflowModel ,
config ,
platform ,
conversationId ,
feat: add per-node MCP servers for DAG workflows (#445) (#688)
* feat: add per-node MCP servers for DAG workflows (#445)
Add `mcp: path/to/config.json` field to DAG workflow nodes. At execution
time, the executor reads the MCP config JSON, expands $VAR_NAME env
references in env/headers values, and passes the loaded servers to the
Claude Agent SDK via Options.mcpServers. MCP tool wildcards are auto-
added to allowedTools.
- Add mcp field to DagNodeBase type and loader validation
- Add mcpServers + allowedTools to WorkflowAssistantOptions and
AssistantRequestOptions
- Pass mcpServers/allowedTools through ClaudeClient to SDK Options
- Handle system/init message to detect MCP connection failures
- Add Codex warning (per-node MCP not supported)
- Add Haiku warning (tool search not supported)
- Add MCP config path input in Web UI NodeInspector
- Add mcp to WorkflowCanvas reactFlowToDagNodes conversion
- Add 12 unit tests for loadMcpConfig and env var expansion
* fix: address review findings for per-node MCP servers
Type safety:
- Use SDK McpServerConfig type in AssistantRequestOptions (eliminates unsafe cast)
- Update WorkflowAssistantOptions.mcpServers to proper discriminated union
- Remove redundant cast in claude.ts
Error handling:
- Warn users when MCP config references undefined env vars
- Check safeSendMessage return value for MCP connection failures
- Check safeSendMessage return value for Haiku MCP warning
- Log unhandled system messages at debug level in both claude.ts and dag-executor.ts
- Coerce non-string env/header values with warning instead of silent passthrough
Code quality:
- Fix log event naming: dag_node_mcp_* → dag.mcp_* (domain.action_state format)
- Replace IIFE in loader mcp validation with plain if/else
- Extract duplicated env expansion logic into expandEnvVarsInRecord helper
- Merge split import from ./deps into single statement
- Return missingVars from loadMcpConfig/expandEnvVars for caller awareness
Documentation:
- Add mcp field to Node Fields table in docs/authoring-workflows.md
- Add mcp to DAG schema example, allowed_tools section, and summary list
- Add mcp to CLAUDE.md DAG feature list
- Add per-node MCP servers paragraph to README.md tool restrictions section
* docs: add MCP servers guide (docs/mcp-servers.md)
Comprehensive guide covering config file format (stdio/HTTP/SSE), env var
expansion, automatic tool wildcards, MCP-only nodes, connection failure
handling, workflow examples, troubleshooting, and popular server list.
Cross-referenced from authoring-workflows.md and CLAUDE.md.
* feat: add optional ntfy push notification to smart PR review
Add conditional notify node to archon-smart-pr-review workflow that sends
a push notification when the review completes. Gated behind a bash node
that checks for .archon/mcp/ntfy.json — silently skipped if not configured.
- Add check-ntfy bash node + notify MCP node to smart PR review workflow
- Add .archon/mcp/ to .gitignore (per-user MCP configs may contain secrets)
- Add "Push Notifications" setup guide to docs/mcp-servers.md
2026-03-16 18:24:45 +00:00
workflowRun . id ,
feat: expose Claude SDK node options (effort, thinking, maxBudgetUsd, systemPrompt, fallbackModel, betas, sandbox) (#931)
Workflow authors can now configure 7 Claude SDK options per DAG node in YAML.
Five of these (effort, thinking, fallbackModel, betas, sandbox) are also
settable at workflow level as defaults that per-node values override.
Changes:
- Add effortLevelSchema, thinkingConfigSchema, sandboxSettingsSchema to dag-node.ts
- Add 7 optional fields to dagNodeBaseSchema with BASH_NODE_AI_FIELDS + transform
- Add 5 workflow-level defaults to workflowBaseSchema
- Extend WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts)
- Forward all 7 options in claude.ts with systemPrompt conditional override
- Add workflow-level options resolution in dag-executor.ts with per-node override
- Add error_max_budget_usd handling in result handler
- Consolidated Codex warning for all Claude-only options
- Add 16 schema parsing tests for new options
Fixes #931
2026-04-06 15:22:24 +00:00
cwd ,
2026-04-13 13:10:48 +00:00
workflowLevelOptions
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
) ;
feat: visual workflow builder with React Flow (#471)
* feat: add visual workflow builder with React Flow
Replace the "Coming Soon" stub at /workflows/builder with a full visual
workflow editor supporting all three modes:
- DAG mode: React Flow canvas with drag-and-drop from command palette,
edge drawing between nodes, Dagre auto-layout, and full node inspector
- Sequential mode: sortable step list with parallel block grouping
- Loop mode: config panel for prompt/until/max_iterations/fresh_context
Toolbar provides validate, save, and run actions using existing backend
APIs. Existing workflows can be loaded for editing via dropdown or
?edit= URL param. Mode switching with unsaved changes shows confirmation.
Also exports DagNode types from @archon/core, adds 5 new API client
functions (getWorkflow, saveWorkflow, deleteWorkflow, validateWorkflow,
listCommands), and fixes WorkflowDefinitionResponse to use the real
WorkflowDefinition type.
* docs: update docs for visual workflow builder
- Fix directory structure: pages/ → routes/, add workflows to components
- Add visual workflow builder to Web UI features in README
* fix: address review findings in workflow builder
- Move auto-load from render-time side effect to useEffect
- Add fallthrough handling for unrecognized workflow types
- Add promptText as explicit property on DagNodeData, remove double casts
- Consolidate DagFlowNode type alias to single export
- Replace Date.now() node IDs with crypto.randomUUID()
- Use node.id instead of node.data.id in reactFlowToDagNodes
- Remove as WorkflowDefinition casts, inline properties for union safety
- Add try-catch around dagre.layout() and guard undefined pos
- Surface useQuery errors in NodePalette and WorkflowToolbar
- Separate JSON.parse from onUpdate in catch block, show parse details
- Add separate runError state, clear stale errors, handle orphaned conversations
* feat: add parallel block inspector, editing, and ungrouping
- Add ParallelBlockInspector component with sub-step editing
(command, clearContext, allowed/denied tools)
- Add/remove sub-steps within a parallel block
- Auto-ungroup when fewer than 2 sub-steps remain
- Ungroup button in both inspector panel and step row
- Delete block action in inspector
* fix: address PR review findings in workflow builder
- Fix prompt text data loss: map prompt → promptText in dagNodesToReactFlow
- Add key prop to NodeInspector to prevent stale state on node switch
- Log dagre layout errors instead of silently swallowing
- Surface listCommands query errors with visible banner
- Block run when unsaved changes; don't navigate on failure
- Validate before save to avoid raw server error messages
- Add console.error to loadWorkflow and validation catch blocks
- Surface workflow list load error in feedback row
- Differentiate network errors from validation errors
- Add readonly to SequentialEditor steps prop
- Add JSDoc on DagNodeData, ParallelBlockInspectorProps, WorkflowCanvasProps
* feat: add Beta badge to Workflow Builder nav link
* feat: add bash node type and smart PR review DAG workflow
Add a `bash` node type for DAG workflows that runs shell scripts without
AI, capturing stdout as node output. This enables free/deterministic
operations like gathering stats or running git commands within DAG
workflows.
- BashNode type with `bash` script field and optional `timeout`
- Three-way mutual exclusivity in parser (command/prompt/bash)
- executeBashNode with variable substitution, stderr logging, timeout
- Web UI: BASH badge, script editor, timeout input, draggable palette item
Also add archon-smart-pr-review DAG workflow that classifies PR complexity
first (via haiku), then routes to only the relevant review agents based
on the classification. Saves AI calls on trivial/small PRs.
* docs: document bash node type in DAG workflow section
The bash: node type added in this PR was missing from the workflow
documentation. Users writing DAG workflows need to know the three
available node types: command:, prompt:, and bash:.
* fix: address review findings in workflow builder
- Add console.error to handleSave/handleRun catch blocks (was silently swallowing errors)
- Fix allowed_tools/denied_tools using || instead of ?? (empty array [] was converted to undefined, changing semantics)
- Remove unnecessary type assertions in resolveNodeDisplay that bypass TS narrowing
- Add justification comments to as DagNode casts (required by project guidelines)
- Add error details to NodePalette failed commands message
- Use exhaustive switch in buildDefinition with never check
- Fix NodeInspector comments: "AI-only fields" was incomplete, "Output Format" guard was misleading
- Separate serialize/parse try-catch in validate endpoint for clearer error messages
- Classify ENOENT/EACCES errors in executeBashNode for user-friendly messages
- Document intentional Dagre layout fallback per project guidelines
2026-02-25 12:09:53 +00:00
// 5. Determine session — parallel or context:fresh → always fresh
2026-03-25 23:06:23 +00:00
// Parallel layers always get fresh sessions; explicit 'fresh' context also forces it.
// 'shared' forces continuation. Default: fresh for parallel, inherited for sequential.
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const isFresh = isParallelLayer || node . context === 'fresh' ;
const resumeSessionId = isFresh ? undefined : lastSequentialSessionId ;
2026-03-14 16:04:40 +00:00
// 6. Execute with retry for transient failures
const retryConfig = getEffectiveNodeRetryConfig ( node ) ;
2026-04-06 16:39:18 +00:00
let output : NodeExecutionResult = {
state : 'failed' ,
output : '' ,
error : 'Node did not execute' ,
} ;
2026-03-14 16:04:40 +00:00
for ( let attempt = 0 ; attempt <= retryConfig . maxRetries ; attempt ++ ) {
output = await executeNodeInternal (
deps ,
platform ,
conversationId ,
cwd ,
workflowRun ,
node ,
provider ,
nodeOptions ,
artifactsDir ,
logDir ,
baseBranch ,
2026-04-06 13:26:59 +00:00
docsDir ,
2026-03-14 16:04:40 +00:00
nodeOutputs ,
2026-03-25 23:06:23 +00:00
// Always pass the prior session ID — forkSession:true in executeNodeInternal
// ensures the source is never mutated, so retries can safely resume from it.
resumeSessionId ,
2026-03-14 16:04:40 +00:00
configuredCommandFolder ,
issueContext
) ;
if ( output . state !== 'failed' ) break ;
2026-03-14 16:49:30 +00:00
// Check if retryable.
// FATAL errors (auth, permissions, credit balance) are never retried even when on_error:all.
const isFatal = output . error
? classifyError ( new Error ( output . error ) ) === 'FATAL'
: false ;
2026-03-14 16:04:40 +00:00
const isTransient = output . error ? isTransientNodeError ( output . error ) : false ;
const shouldRetry =
2026-03-14 16:49:30 +00:00
! isFatal &&
( retryConfig . onError === 'all' ||
( retryConfig . onError === 'transient' && isTransient ) ) ;
2026-03-14 16:04:40 +00:00
if ( ! shouldRetry || attempt >= retryConfig . maxRetries ) break ;
const delayMs = retryConfig . delayMs * Math . pow ( 2 , attempt ) ;
getLog ( ) . warn (
{
nodeId : node.id ,
attempt : attempt + 1 ,
maxRetries : retryConfig.maxRetries ,
delayMs ,
error : output.error ,
} ,
'dag_node_transient_retry'
) ;
2026-03-14 16:49:30 +00:00
const errorKind = isTransient ? 'transient error' : 'error' ;
2026-03-14 16:04:40 +00:00
await safeSendMessage (
platform ,
conversationId ,
2026-03-14 16:49:30 +00:00
` ⚠️ Node \` ${ node . id } \` failed with ${ errorKind } (attempt ${ String ( attempt + 1 ) } / ${ String ( retryConfig . maxRetries + 1 ) } ). Retrying in ${ String ( Math . round ( delayMs / 1000 ) ) } s... ` ,
2026-03-14 16:04:40 +00:00
{ workflowId : workflowRun.id , nodeName : node.id }
) ;
await new Promise ( resolve = > setTimeout ( resolve , delayMs ) ) ;
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
return { nodeId : node.id , output } ;
} catch ( error ) {
const err = error as Error ;
getLog ( ) . error ( { err , nodeId : node.id } , 'dag_node_pre_execution_failed' ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'node_failed' ,
step_name : node.id ,
data : { error : err.message } ,
} )
. catch ( ( dbErr : Error ) = > {
getLog ( ) . error ( { err : dbErr , nodeId : node.id } , 'workflow_event_persist_failed' ) ;
} ) ;
getWorkflowEventEmitter ( ) . emit ( {
type : 'node_failed' ,
runId : workflowRun.id ,
nodeId : node.id ,
nodeName : node.command ? ? node . id ,
error : err.message ,
} ) ;
await safeSendMessage (
platform ,
conversationId ,
` Node ' ${ node . id } ' failed before execution: ${ err . message } ` ,
{ workflowId : workflowRun.id , nodeName : node.id }
) ;
return {
nodeId : node.id ,
output : { state : 'failed' as const , output : '' , error : err.message } ,
} ;
}
} )
) ;
// Process layer results — store all outputs, track failures
let layerHadFailure = false ;
for ( const result of layerResults ) {
if ( result . status === 'fulfilled' ) {
const { nodeId , output } = result . value ;
2026-04-06 16:39:18 +00:00
if ( output . costUsd !== undefined ) totalCostUsd += output . costUsd ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
nodeOutputs . set ( nodeId , output ) ;
if ( output . state === 'completed' && ! isParallelLayer && output . sessionId !== undefined ) {
lastSequentialSessionId = output . sessionId ;
}
if ( output . state === 'failed' ) layerHadFailure = true ;
} else {
// Should not happen — all errors are caught in the inner try-catch
// Handle defensively: log the unexpected rejection
getLog ( ) . error ( { err : result.reason as Error , layerIdx } , 'dag_node_unexpected_rejection' ) ;
layerHadFailure = true ;
await safeSendMessage (
platform ,
conversationId ,
` An unexpected error occurred executing a node in layer ${ String ( layerIdx ) } . Check server logs. ` ,
{ workflowId : workflowRun.id }
) ;
}
}
if ( layerHadFailure ) {
getLog ( ) . warn ( { layerIdx , nodeCount : layer.length } , 'dag_layer_had_failures' ) ;
}
2026-03-14 14:33:57 +00:00
feat: add approval gate node type for human-in-the-loop workflows (#888)
* feat: add approval gate node type for human-in-the-loop workflows
Add `approval` as a fifth DAG node type that pauses workflow execution
until a human approves or rejects. Built on existing resume infrastructure.
- New `approval` node type in YAML workflows with `message` field
- `paused` workflow status (non-terminal, resumable)
- Approve/reject via REST API, CLI, chat commands, and Web UI
- Approval comment available as `$node.output` in downstream nodes
- Dashboard shows amber pulsing badge with approval message for paused runs
- Path guard blocks new workflows on paused worktrees
* fix: resolve 7 bugs in approval gate implementation
Critical:
- Parse metadata JSON on SQLite (returned as TEXT string, not object)
- Suppress spurious "Workflow stopped (paused)" message after approval gates
Medium:
- CLI exits 0 on pause (not error code 1)
- workflow status shows paused runs alongside running
- Docs clarify auto-resume is CLI-only; API/chat mark resumable
Low:
- Skip completed_at when transitioning to failed for approval resume
- Warn on AI fields (model, hooks, etc.) set on approval nodes
- Add rowCount check to updateWorkflowRun
Also: extract ApprovalContext type, add .min(1) to approval.message,
fix stale "Only failed runs" error messages, fix log domain prefix,
add recovery instructions to CLI approve error message.
2026-03-30 14:27:11 +00:00
// Check for non-running status between DAG layers (cancellation, deletion, pause)
2026-03-14 14:33:57 +00:00
try {
const dagStatus = await deps . store . getWorkflowRunStatus ( workflowRun . id ) ;
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
if ( dagStatus === null || dagStatus !== 'running' ) {
const effectiveStatus = dagStatus ? ? 'deleted' ;
2026-03-14 14:33:57 +00:00
getLog ( ) . info (
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
{
workflowRunId : workflowRun.id ,
layerIdx ,
totalLayers : layers.length ,
status : effectiveStatus ,
} ,
'dag.stop_detected_between_layers'
2026-03-14 14:33:57 +00:00
) ;
feat: add approval gate node type for human-in-the-loop workflows (#888)
* feat: add approval gate node type for human-in-the-loop workflows
Add `approval` as a fifth DAG node type that pauses workflow execution
until a human approves or rejects. Built on existing resume infrastructure.
- New `approval` node type in YAML workflows with `message` field
- `paused` workflow status (non-terminal, resumable)
- Approve/reject via REST API, CLI, chat commands, and Web UI
- Approval comment available as `$node.output` in downstream nodes
- Dashboard shows amber pulsing badge with approval message for paused runs
- Path guard blocks new workflows on paused worktrees
* fix: resolve 7 bugs in approval gate implementation
Critical:
- Parse metadata JSON on SQLite (returned as TEXT string, not object)
- Suppress spurious "Workflow stopped (paused)" message after approval gates
Medium:
- CLI exits 0 on pause (not error code 1)
- workflow status shows paused runs alongside running
- Docs clarify auto-resume is CLI-only; API/chat mark resumable
Low:
- Skip completed_at when transitioning to failed for approval resume
- Warn on AI fields (model, hooks, etc.) set on approval nodes
- Add rowCount check to updateWorkflowRun
Also: extract ApprovalContext type, add .min(1) to approval.message,
fix stale "Only failed runs" error messages, fix log domain prefix,
add recovery instructions to CLI approve error message.
2026-03-30 14:27:11 +00:00
// Paused is intentional (approval gate) — the approval message was already sent
if ( effectiveStatus !== 'paused' ) {
await safeSendMessage (
platform ,
conversationId ,
` ⚠️ **Workflow stopped** ( ${ effectiveStatus } ): DAG execution stopped after layer ${ String ( layerIdx + 1 ) } / ${ String ( layers . length ) } ` ,
{ workflowId : workflowRun.id }
) ;
}
2026-03-14 14:33:57 +00:00
break ;
}
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
} catch ( statusErr ) {
// Non-fatal — status check failure should not crash the workflow
getLog ( ) . warn (
{ err : statusErr as Error , workflowRunId : workflowRun.id } ,
'dag.status_check_failed'
) ;
2026-03-14 14:33:57 +00:00
}
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
// Helper: bail out if the run was transitioned externally (cancelled, deleted, etc.)
async function skipIfStatusChanged ( logEvent : string ) : Promise < boolean > {
const status = await deps . store . getWorkflowRunStatus ( workflowRun . id ) ;
if ( status === null || status !== 'running' ) {
getLog ( ) . info ( { workflowRunId : workflowRun.id , status : status ? ? 'deleted' } , logEvent ) ;
getWorkflowEventEmitter ( ) . unregisterRun ( workflowRun . id ) ;
return true ;
}
return false ;
}
2026-04-01 11:36:12 +00:00
// Single-pass: compute node outcome counts and derive success/failure booleans
const nodeCounts = { completed : 0 , failed : 0 , skipped : 0 , total : workflow.nodes.length } ;
for ( const o of nodeOutputs . values ( ) ) {
if ( o . state === 'completed' ) nodeCounts . completed ++ ;
else if ( o . state === 'failed' ) nodeCounts . failed ++ ;
else if ( o . state === 'skipped' ) nodeCounts . skipped ++ ;
}
const anyCompleted = nodeCounts . completed > 0 ;
const anyFailed = nodeCounts . failed > 0 ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
getLog ( ) . info (
{ nodeCount : workflow.nodes.length , anyCompleted , anyFailed } ,
'dag_workflow_finished'
) ;
if ( ! anyCompleted ) {
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
if ( await skipIfStatusChanged ( 'dag.skip_fail_status_changed' ) ) return ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const failMsg =
` DAG workflow ' ${ workflow . name } ' completed with no successful nodes. ` +
'Check node conditions, trigger rules, and upstream failures.' ;
2026-04-01 11:36:12 +00:00
// Note: nodeCounts not stored for failed runs — failWorkflowRun only stores { error }.
// Frontend guards with isValidNodeCounts so missing node_counts is safe.
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
await deps . store . failWorkflowRun ( workflowRun . id , failMsg ) . catch ( ( dbErr : Error ) = > {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
getLog ( ) . error ( { err : dbErr , workflowRunId : workflowRun.id } , 'dag_db_fail_failed' ) ;
} ) ;
refactor(workflows)!: remove sequential execution mode, DAG becomes sole format (#805)
* refactor(workflows)!: remove sequential execution mode, DAG becomes sole format
Remove the steps-based (sequential) workflow execution mode entirely.
All workflows now use the nodes-based (DAG) format exclusively.
- Convert 8 sequential default workflows to DAG format
- Delete archon-fix-github-issue sequential (DAG version absorbs triggers)
- Remove SingleStep, ParallelBlock, StepWorkflow types and guards
- Gut executor.ts from ~2200 to ~730 lines (remove sequential loop)
- Remove step_started/completed/failed and parallel_agent_* events
- Remove logStepStart/Complete and logParallelBlockStart/Complete
- Delete SequentialEditor, StepProgress, ParallelBlockView components
- Remove sequential mode from workflow builder and execution views
- Delete executor.test.ts (4395 lines), update ~45 test fixtures
- Update CLAUDE.md and docs to reflect DAG-only format
BREAKING CHANGE: Workflows using `steps:` format are no longer supported.
Convert to `nodes:` (DAG) format. The loader provides a clear error message
directing users to the migration guide.
* fix: address review findings — guard errors, remove dead code, add tests
- Guard logNodeSkip/logWorkflowError against filesystem errors in dag-executor
- Move mkdir(artifactsDir) inside try-catch with user-friendly error
- Remove startFromStep dead parameter from executeWorkflow signature
- Remove isDagWorkflow() tautology and all callers (20+ sites)
- Remove dead BuilderMode/mode state from frontend components
- Remove vestigial isLoop, selectedStep, stepIndex, step_index fields
- Remove "DAG" prefix from user-facing resume/error messages
- Fix 5 stale docs (README, getting-started, authoring-commands, web adapter)
- Update event-emitter tests to use node events instead of removed step events
- Add executor-shared.test.ts (12 tests) for substituteWorkflowVariables
- Add executor.test.ts (11 tests) for concurrent-run, model resolution, resume
* fix(workflows): add migration guide, port preamble tests, improve error message
- Add docs/sequential-dag-migration-guide.md with 3 conversion patterns
(single step, chain with clearContext, parallel block) and a Claude Code
migration command for automated conversion
- Update loader error message to point to migration guide and include
ready-to-run claude command
- Port 8 preamble tests from deleted executor.test.ts to new
executor-preamble.test.ts: staleness detection (3), concurrent-run
guard (3), DAG resume (2)
Addresses review feedback from #805.
* fix(workflows): update loader test to match new error message wording
* fix: address review findings — fail stuck runs, remove dead code, fix docs
- Mark workflow run as failed when artifacts mkdir fails (prevents
15-min concurrent-run guard block)
- Remove vestigial totalSteps from WorkflowStartedEvent and executor
- Delete dead WorkflowToolbar.tsx (369 lines, no importers)
- Remove stepIndex prop from StepLogs (always 0, label now "Node logs")
- Restore cn() in StatusBar for consistent conditional classes
- Promote resume-check log to error, add errorType to failure logs
- Remove ghost $PLAN/$IMPLEMENTATION_SUMMARY from docs (never implemented)
- Update workflows.md rules to DAG-only format
- Fix migration guide trigger_rule example
- Clean up blank-line residues and stale comments
* fix: resolve rebase conflicts with #729 (forkSession) and #730 (dashboard)
- Remove sequential forkSession/persistSession code from #729 (dead after
sequential removal)
- Fix loader type narrowing for DagNode context field
- Update dashboard components from #730 to use dagNodes instead of steps
- Remove WorkflowStepEvent/ParallelAgentEvent from dashboard SSE hook
2026-03-26 09:27:34 +00:00
await logWorkflowError ( logDir , workflowRun . id , failMsg ) . catch ( ( logErr : Error ) = > {
getLog ( ) . error (
{ err : logErr , workflowRunId : workflowRun.id } ,
'dag.workflow_error_log_write_failed'
) ;
} ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const emitterForFail = getWorkflowEventEmitter ( ) ;
emitterForFail . emit ( {
type : 'workflow_failed' ,
runId : workflowRun.id ,
workflowName : workflow.name ,
error : failMsg ,
} ) ;
emitterForFail . unregisterRun ( workflowRun . id ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
await safeSendMessage ( platform , conversationId , ` \ u274c ${ failMsg } ` , {
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
workflowId : workflowRun.id ,
} ) ;
// DO NOT throw — outer executor.ts catch would duplicate workflow_failed events
return ;
}
if ( anyFailed ) {
2026-04-16 16:40:55 +00:00
if ( await skipIfStatusChanged ( 'dag.skip_fail_status_changed' ) ) return ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const failedNodes = [ . . . nodeOutputs . entries ( ) ]
. filter ( ( [ , o ] ) = > o . state === 'failed' )
. map ( ( [ id , o ] ) = > ` ' ${ id } ': ${ o . state === 'failed' ? o . error : 'unknown' } ` )
. join ( '; ' ) ;
2026-04-16 16:40:55 +00:00
const failMsg = ` DAG workflow ' ${ workflow . name } ' completed with failures: ${ failedNodes } ` ;
await deps . store . failWorkflowRun ( workflowRun . id , failMsg ) . catch ( ( dbErr : Error ) = > {
getLog ( ) . error ( { err : dbErr , workflowRunId : workflowRun.id } , 'dag_db_fail_failed' ) ;
} ) ;
await logWorkflowError ( logDir , workflowRun . id , failMsg ) . catch ( ( logErr : Error ) = > {
getLog ( ) . error (
{ err : logErr , workflowRunId : workflowRun.id } ,
'dag.workflow_error_log_write_failed'
) ;
} ) ;
const emitterForFail = getWorkflowEventEmitter ( ) ;
emitterForFail . emit ( {
type : 'workflow_failed' ,
runId : workflowRun.id ,
workflowName : workflow.name ,
error : failMsg ,
} ) ;
emitterForFail . unregisterRun ( workflowRun . id ) ;
await safeSendMessage ( platform , conversationId , ` \ u274c ${ failMsg } ` , {
workflowId : workflowRun.id ,
} ) ;
// DO NOT throw — outer executor.ts catch would duplicate workflow_failed events
return ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}
feat: workflow lifecycle overhaul — path-based guards, interrupted status, resume/abandon (#871)
* feat: add interrupted to WorkflowRunStatus schema
Implements US-001 from PRD.
Changes:
- Add 'interrupted' to workflowRunStatusSchema z.enum in packages/workflows/src/schemas/workflow-run.ts
- Add 'interrupted' to workflowRunStatusSchema in packages/server/src/routes/schemas/workflow.schemas.ts
- Add interrupted: z.number() to dashboardRunsResponseSchema counts object
- Add 'interrupted' to dashboardValidStatuses in API handler
- Add interrupted: 0 to DashboardRunsResult counts interface and runtime object in packages/core/src/db/workflows.ts
* feat: update IWorkflowStore interface & DB query implementations
Implements US-002 from PRD.
Changes:
- IWorkflowStore: rename getActiveWorkflowRun → getActiveWorkflowRunByPath(workingPath)
- IWorkflowStore: drop conversationId from findResumableRun signature
- IWorkflowStore: add interruptOrphanedRuns() method
- db/workflows: add getActiveWorkflowRunByPath querying status IN ('running', 'interrupted')
- db/workflows: update findResumableRun to query by workflow_name + working_path only, include 'interrupted' status
- db/workflows: add interruptOrphanedRuns() UPDATE SET status='interrupted' WHERE status='running'
- store-adapter: wire all three new/modified methods
- executor: update call sites to use renamed methods (type-check requirement)
- tests: update all mock stores and add new tests for getActiveWorkflowRunByPath and interruptOrphanedRuns
* feat: replace staleness guard with path-based lifecycle
Implements US-003 from PRD.
Changes:
- executor.ts: remove STALE_MINUTES staleness auto-kill; replace with
status-based guard — 'running' blocks, 'interrupted' offers resume/abandon
- server/src/index.ts: replace failStaleWorkflowRuns() with
createWorkflowStore().interruptOrphanedRuns() on startup
- executor-preamble.test.ts: replace staleness detection tests with
concurrent run guard tests covering 'running' and 'interrupted' cases
* feat: command handler — /workflow status, resume, and abandon
Implements US-004. Replaces time-based stale heuristics with explicit
lifecycle commands for workflow management.
Changes:
- Remove WORKFLOW_SLOW_THRESHOLD_MS and WORKFLOW_STALE_THRESHOLD_MS constants
- Replace /workflow status with global view: lists all running+interrupted runs
across all worktrees (ID, name, working path, status, started-at)
- Add /workflow resume <id>: validates state then calls resumeWorkflowRun
- Add /workflow abandon <id>: validates state then calls failWorkflowRun
- Add statuses[] filter to listWorkflowRuns for IN (...) queries
- Update /workflow help text and default case usage string
- Update /status command to remove stale warning that referenced removed constants
- Replace old /workflow status tests with new behavior coverage
- Add /workflow resume and /workflow abandon test coverage
* feat: CLI workflow status, resume, and abandon subcommands
Implements US-005 from PRD.
Changes:
- Implement workflowStatusCommand: lists all running+interrupted runs with ID, name, path, status, age; supports --json flag
- Add workflowResumeCommand: validates run state then calls resumeWorkflowRun
- Add workflowAbandonCommand: validates run state then calls failWorkflowRun('Abandoned by user')
- Replace findLastFailedRun usage in --resume path with findResumableRun(workflowName, cwd)
- Wire resume/abandon subcommands in cli.ts
- Update tests: replace "not implemented" test with status/resume/abandon coverage
* feat: Web UI interrupted status badge and dashboard support
Implements US-006 from PRD.
Changes:
- api.generated.d.ts: add 'interrupted' to WorkflowRunStatus enum and DashboardRunsResponse.counts
- api.ts: add interrupted field to DashboardCounts interface
- WorkflowExecution.tsx: add 'interrupted' to TERMINAL_STATUSES; add amber color to StatusBadge
- WorkflowRunCard.tsx: add amber dot and badge for interrupted status
- StatusSummaryBar.tsx: add 'interrupted' to STATUS_CHIPS filter list
- DashboardPage.tsx: include interrupted in activeRuns filter and counts default
* refactor: remove dead timer-based workflow staleness code
Implements US-007 from PRD.
Changes:
- Remove findLastFailedRun() from db/workflows.ts (CLI path unified on findResumableRun in US-005)
- Remove failStaleWorkflowRuns() from db/workflows.ts (replaced by interruptOrphanedRuns in US-002)
- Remove IDatabase import from db/workflows.ts (no longer needed)
- Remove failStaleWorkflowRuns tests from db/workflows.test.ts
grep -r 'STALE' packages/ (workflow-timer variant), grep -r 'findLastFailedRun' and
grep -r 'failStaleWorkflowRuns' all return zero matches.
* fix: address review feedback — truncated IDs, resume semantics, type safety
- Use full UUIDs in resume/abandon command suggestions (was .slice(0, 8))
- Add completed_at to interruptOrphanedRuns for correct duration metrics
- Fix resume commands: mark interrupted→failed to unblock path guard,
let next workflow invocation auto-resume via findResumableRun
- Collapse dual status/statuses fields into status?: T | T[]
- Extract TERMINAL/RESUMABLE/ACTIVE_WORKFLOW_STATUSES constants
- Add explicit else-if for interrupted status in executor path guard
- Add structured logging to CLI workflow commands
- Restore conversationId to cmd.workflow_status_failed log
- Add tests: listWorkflowRuns statuses filter, interrupted auto-resume,
DB error handling for resume/abandon
- Update docs: commands-reference, cli-user-guide, authoring-workflows, CLAUDE.md
* refactor: simplify CLI commands and status filter logic
- Extract getRunOrThrow helper for shared run lookup pattern
- Use WorkflowRun[] instead of Awaited<ReturnType<...>>
- Remove single-item special case in listWorkflowRuns (IN works for all)
- Use ?? instead of || for null-coalescing consistency
- Remove unused ACTIVE_WORKFLOW_STATUSES constant
- Add inline comment on completed_at for interrupted runs
* fix: handle SQLite string dates in formatAge
SQLite returns started_at as a string, not a Date object.
formatAge now accepts Date | string and converts accordingly.
Found during E2E testing against real SQLite database.
* feat: UX improvements — real resume, dashboard actions, cleanup command
- CLI resume now actually re-executes the workflow (calls workflowRunCommand
with --resume internally instead of just flipping DB status)
- Remove truncated IDs from executor guard messages (full ID in commands only)
- Add Resume/Abandon/Delete buttons to dashboard workflow run cards
- Add Delete button to history table rows
- Add API endpoints: POST resume, POST abandon, DELETE workflow run
- Add CLI workflow cleanup command (deletes terminal runs older than N days)
- Add deleteWorkflowRun and deleteOldWorkflowRuns DB functions
* refactor: simplify API handlers, dashboard actions, and log conventions
- Use RESUMABLE/TERMINAL_WORKFLOW_STATUSES constants in API handlers
(was inline string checks diverging from CLI/command-handler)
- Extract makeRunAction helper in DashboardPage (4 identical handlers → 1)
- Fix log event names to use domain prefix convention (api.workflow_run_*)
- Use Ban icon for Abandon to distinguish from Cancel's XCircle
- Use instanceof Date guard in formatAge for clarity
- Add comment on delete handler's active-status guard
* refactor: simplify workflow lifecycle — remove interrupted, single resume path
Rework the primitives for a clean foundation:
Status model: 5 statuses (pending, running, completed, failed, cancelled).
Remove 'interrupted' entirely — server restart now marks orphaned runs
as 'failed' directly (with metadata.failure_reason = 'server_restart').
Resume model: one path. The executor's implicit findResumableRun
detects prior failed runs and skips completed nodes. The CLI --resume
flag reuses the prior run's worktree but lets the executor handle
node-skipping (no more preCreatedRun bypass). Chat /workflow resume
tells the user to re-invoke (auto-resume kicks in).
Path guard: only blocks 'running' status (was running + interrupted).
Guards always run regardless of preCreatedRun.
Cancellation: generalized from status === 'cancelled' to
status !== 'running' at all 3 check points (streaming, loop iterations,
DAG layers). Ready for future 'paused' status with zero changes.
- interruptOrphanedRuns → failOrphanedRuns
- Remove preCreatedRun bypass from CLI --resume path
- Generalize 3 cancellation check points in dag-executor
- Update all API endpoints, command handlers, UI components
- Update all tests and documentation
* fix: cancel/complete race, abandon semantics, UTC dates
- Fix cancel/complete race condition: dag-executor now checks DB status
before calling completeWorkflowRun or failWorkflowRun, preventing a
cancel during the final layer from being overwritten to completed
- Abandon uses cancelWorkflowRun instead of failWorkflowRun, so
abandoned runs don't get auto-resumed by findResumableRun
- Fix formatAge UTC bug: SQLite dates without Z suffix now parsed as UTC
* fix: address PR review — SQL safety, transactions, error handling, docs, tests
- Validate olderThanDays before SQL interpolation in deleteOldWorkflowRuns
- Wrap multi-statement deletes in transactions (deleteOldWorkflowRuns, deleteWorkflowRun)
- Fix deleteWorkflowRun error double-wrap (don't re-wrap "not found" errors)
- Handle null getWorkflowRunStatus in DAG executor (treat as deleted, abort)
- Fix mock name mismatch: interruptOrphanedRuns → failOrphanedRuns in 3 test files
- Fix default mock getWorkflowRunStatus to return 'running' instead of null
- Add NaN guard to formatAge (returns 'unknown' on unparseable dates)
- Fix stale 'interrupted' references in route summary and delete comment
- Include working path in /workflow resume response
- Align deleteOldWorkflowRuns return type to { count } for consistency
- Document workflow cleanup command in CLAUDE.md, CLI user guide, commands reference
- Document new API endpoints (resume, abandon, delete) in CLAUDE.md
- Add tests for deleteOldWorkflowRuns, deleteWorkflowRun, workflowCleanupCommand
- Fix workflowAbandonCommand test to assert cancelWorkflowRun call
* refactor: simplify code per review — extract helper, cleaner date parsing, consistent guards
- Extract duplicated status-check blocks into skipIfStatusChanged helper in dag-executor
- Simplify formatAge to single-pass date parsing with Z suffix (ISO 8601)
- Use TERMINAL_WORKFLOW_STATUSES constant in delete route guard
- Rename cancelError → actionError in DashboardPage (covers 4 actions now)
- Fix merge conflict: add IDatabase import, getRunningWorkflows from dev
- Fix api.conversations.test.ts: add missing workflow mocks, fix Hono → OpenAPIHono
* fix: address review findings — double-rollback, missing guards, log context, tests
- Fix double-rollback in deleteWorkflowRun by removing inner rollback()
call and letting the outer catch handle it
- Add terminal-status guard inside deleteWorkflowRun itself, not just in
the route handler, to prevent deletion of running workflows
- Add rollback failure logging to the rollback() helper
- Add runId to error logs in resume/abandon/delete API route handlers
- Add workingPath to getActiveWorkflowRunByPath error log
- Add workflowRunId to dag-executor status-check warn logs
- Wrap workflowRunCommand in try/catch in workflowResumeCommand with
structured logging and null guard for user_message
- Clean up stale 'interrupted' references in JSDoc
- Fix missing / prefix on workflow cleanup in commands-reference.md
- Add API route tests for POST /resume, POST /abandon, DELETE /:runId
* refactor: apply code simplifications from review
- Replace fragile startsWith string matching in deleteWorkflowRun catch
with a typed WorkflowRunGuardError class
- Reorder listWorkflowRuns placeholder generation: capture startIdx
before pushing values for clarity
- Replace curried makeRunAction factory in DashboardPage with a plain
runAction helper function
- Move skipIfStatusChanged helper definition before its call sites in
dag-executor to match reading order
2026-03-30 10:36:53 +00:00
// Check if status was changed externally (e.g. cancelled) before marking complete.
if ( await skipIfStatusChanged ( 'dag.skip_complete_status_changed' ) ) return ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
// Update DB and emit completion
try {
2026-04-06 16:39:18 +00:00
await deps . store . completeWorkflowRun ( workflowRun . id , {
node_counts : nodeCounts ,
2026-04-06 17:03:47 +00:00
// totalCostUsd starts at 0; only write metadata when at least one node reported cost
2026-04-06 16:39:18 +00:00
. . . ( totalCostUsd > 0 ? { total_cost_usd : totalCostUsd } : { } ) ,
} ) ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} catch ( dbErr ) {
getLog ( ) . error (
{ err : dbErr as Error , workflowRunId : workflowRun.id } ,
'dag_db_complete_failed'
) ;
await safeSendMessage (
platform ,
conversationId ,
'Warning: workflow completed but the run status could not be saved. The workflow result may appear inconsistent.' ,
{ workflowId : workflowRun.id }
) ;
}
await logWorkflowComplete ( logDir , workflowRun . id ) ;
2026-03-12 16:56:29 +00:00
const duration = Date . now ( ) - dagStartTime ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
const emitter = getWorkflowEventEmitter ( ) ;
emitter . emit ( {
type : 'workflow_completed' ,
runId : workflowRun.id ,
workflowName : workflow.name ,
2026-03-12 16:56:29 +00:00
duration ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} ) ;
refactor: extract @archon/workflows package from @archon/core (#507)
* refactor: extract @archon/workflows package from @archon/core
Move the workflow engine (~14K lines) from @archon/core into a standalone
@archon/workflows package. The engine (loader, router, executor, DAG executor,
event emitter, JSONL logger, bundled defaults, variable substitution) now lives
in packages/workflows/ with dependencies only on @archon/git and @archon/paths.
Database operations, AI client creation, and config loading are injected via a
WorkflowDeps object and IWorkflowStore trait interface — mirroring the
IIsolationStore pattern from the @archon/isolation extraction.
Key changes:
- CREATE packages/workflows/ with all workflow engine source files
- CREATE IWorkflowStore trait + WorkflowDeps dependency injection
- CREATE store-adapter.ts in core to bridge DB modules to IWorkflowStore
- UPDATE executeWorkflow signature to accept WorkflowDeps as first param
- UPDATE all consumers (orchestrator, CLI) to construct and pass WorkflowDeps
- UPDATE discoverWorkflows to use options object instead of positional args
- DELETE original workflow files from core (~14,800 lines removed)
- UPDATE all imports across packages to use @archon/workflows
- ADD text-imports.d.ts references to downstream tsconfigs
Zero behavior changes. All existing tests pass. @archon/core re-exports all
workflow symbols for backward compatibility.
* docs: update CLAUDE.md for @archon/workflows package extraction
Add the new @archon/workflows package to the directory structure,
architecture layers, and import patterns sections. Update @archon/core
description to reflect it now re-exports from @archon/workflows rather
than housing the workflow engine directly.
* fix: address PR review - restore tests, remove compat shims, tighten types
- Move 8 test files (373 tests) to @archon/workflows with WorkflowDeps
mock injection replacing direct DB/client module mocks
- Remove backward compat re-exports from @archon/core — all consumers
now import workflow symbols directly from @archon/workflows
- Fix loadDefaultWorkflows config: callers now load config and pass
loadDefaults option to discoverWorkflows (was silently ignored)
- Add .catch() with logging to all 18 fire-and-forget createWorkflowEvent
calls in executor.ts, matching dag-executor.ts pattern
- Narrow AssistantClientFactory provider param: string → 'claude' | 'codex'
- Type createWorkflowEvent event_type with WorkflowEventType union
- Remove optional marker from WorkflowConfig.assistants.claude
- Strengthen IWorkflowStore.createWorkflowEvent JSDoc contract
- Fix log level inconsistencies (resolveProjectPaths, loadCommandPrompt)
- Update CLAUDE.md to reflect no-shim architecture
* fix: address review - extract helper, add tests, tighten types and docs
- Extract discoverWorkflowsWithConfig() helper in @archon/workflows to
eliminate 8 duplicate loadConfig+discoverWorkflows try/catch blocks
across command-handler, orchestrator-agent, cli/workflow, and api.ts.
Config load failures now logged at warn level in one place.
- Add createWorkflowDeps() factory in store-adapter.ts to consolidate 4
inline WorkflowDeps construction sites into a single point.
- Add compile-time assertion that MergedConfig satisfies WorkflowConfig
to catch structural drift between the two interfaces.
- Wrap createWorkflowEvent in store adapter with try/catch to enforce
the non-throwing contract at the boundary.
- Add store-adapter.test.ts (7 tests) covering method wiring, the
status cast, and the non-throwing wrapper.
- Add globalSearchPath integration tests (3) and
discoverWorkflowsWithConfig tests (3) to loader.test.ts.
- Fix 4 failing orchestrator.test.ts assertions for new signature.
- Fix stale module docstring in @archon/core index.ts.
- Fix inaccurate createWorkflowEvent JSDoc in store.ts.
- Improve comments in deps.ts (circular dep rationale, IWorkflowPlatform
exclusions, WorkflowConfig scope) and store-adapter.ts (SQL column).
- Fix emitValidationResults to log non-ENOENT access errors instead of
silently swallowing them.
* chore: Auto-commit workflow artifacts (archon-test-loop)
2026-02-26 10:51:29 +00:00
deps . store
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
. createWorkflowEvent ( {
workflow_run_id : workflowRun.id ,
event_type : 'workflow_completed' ,
2026-03-12 16:56:29 +00:00
data : { duration_ms : duration } ,
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
} )
. catch ( ( err : Error ) = > {
getLog ( ) . error (
{ err , workflowRunId : workflowRun.id , eventType : 'workflow_completed' } ,
'workflow_event_persist_failed'
) ;
} ) ;
emitter . unregisterRun ( workflowRun . id ) ;
2026-03-31 00:49:40 +00:00
// Return the first terminal node's output (nodes with no dependents) for the parent
// conversation summary. For the common single-terminal case this is unambiguous; for
// multi-terminal DAGs the first completed node in definition order is used.
const allDependencies = new Set ( workflow . nodes . flatMap ( n = > n . depends_on ? ? [ ] ) ) ;
const terminalOutput = workflow . nodes
. filter ( n = > ! allDependencies . has ( n . id ) )
. map ( n = > nodeOutputs . get ( n . id ) )
. find ( o = > o ? . state === 'completed' && o . output . trim ( ) . length > 0 ) ? . output ;
return terminalOutput ;
feat: DAG workflow engine with parallel execution and conditional branching (#450)
* feat: add DAG workflow engine with parallel execution and conditional branching
Adds a third workflow execution mode (`nodes:`) alongside `steps:` and `loop:`.
DAG workflows support explicit dependency edges, parallel layer execution via
Promise.allSettled, conditional branching with `when:` expressions, join semantics
via `trigger_rule`, structured JSON output via Claude SDK `outputFormat`, and
upstream output capture via `$node_id.output` substitution.
- New: `DagNode`, `DagWorkflow`, `TriggerRule`, `NodeOutput`, `NodeState` types
- New: `condition-evaluator.ts` — pure `evaluateCondition` for `when:` expressions
- New: `dag-executor.ts` — topological sort, Promise.allSettled parallel layers,
output capture, trigger rule evaluation, per-node provider/model resolution
- Updated: `loader.ts` — detect `nodes:` key, validate node graph, Kahn cycle detection
- Updated: `executor.ts` — route DAG workflows to dag-executor before loop dispatch
- Updated: `logger.ts` / `event-emitter.ts` — node_start/complete/skip/error events
- Updated: `workflow-bridge.ts` — SSE events for dag_node state changes
- Updated: `AssistantRequestOptions` — added `outputFormat` for Claude structured output
- Updated: `claude.ts` — thread `outputFormat` into SDK Options
- Tests: 37 new tests (condition-evaluator + dag-executor topological sort, trigger
rules, loader cycle detection, invalid DAG rejection, valid DAG parsing)
* docs: document DAG workflow mode (nodes:) added in phase 1
Add full documentation for the new `nodes:` execution mode:
- docs/authoring-workflows.md: add third workflow type section,
full DAG schema reference (node fields, trigger_rule, when:
conditions, output_format, $nodeId.output substitution), a
DAG example workflow, and update the variable table and summary
- CLAUDE.md: add nodes:/DAG bullet points to the Workflows section
- README.md: add nodes: example alongside steps: and loop:, update
key design patterns to mention DAG mode
* fix: address DAG workflow engine review findings
Critical bugs:
- DB workflow status never updated after DAG completion (completeWorkflowRun/failWorkflowRun now called)
- resolveNodeProviderAndModel throws silently swallowed by Promise.allSettled — now caught and returned as failed node outputs
- substituteNodeOutputRefs JSON parse failure was silent — now logged as warn
Important fixes:
- Surface unparseable when: conditions to user via safeSendMessage (fail-open preserved)
- Missing upstream nodes treated as failed in checkTriggerRule instead of silently filtered out
- Config load failure in loadCommandPrompt upgraded from warn to error
- Circular import executor ↔ dag-executor broken via new command-validation.ts module
- Remove defensive "should never happen" else branch in executeNodeInternal (DagNode discriminated union guarantees it)
Type improvements:
- DagNode → CommandNode | PromptNode discriminated union (command/prompt mutually exclusive at type level)
- NodeOutput → discriminated union (error: string required on failed, absent on others)
- TRIGGER_RULES constant and isTriggerRule() added to types.ts, deduplicating loader.ts local definitions
- isDagWorkflow simplified to Array.isArray(workflow.nodes)
- output_format array guard in parseDagNode now rejects arrays and null values
Code quality:
- Replace all void workflowEventDb.createWorkflowEvent() with .catch() error logging
- Fix o.error ?? 'unknown' to o.state === 'failed' ? o.error : 'unknown' (type-safe)
- Export substituteNodeOutputRefs for unit testing
Tests (7 new):
- condition-evaluator: number and boolean JSON field coercion
- dag-executor: none_failed_min_one_success with all-skipped deps, nodes+loop conflict,
invalid trigger_rule rejection, substituteNodeOutputRefs (3 cases), all-nodes-skipped mechanism
* docs: fix two inaccuracies in DAG workflow documentation
- README: "Nodes without depends_on run in parallel" was misleading —
root nodes run concurrently with each other in the same layer, but a
single root node doesn't run "in parallel" with anything. Reworded to
"are in the first layer and run concurrently with each other".
- authoring-workflows.md: Variable Substitution section intro said
"Loop prompts and DAG node prompts/commands support these variables"
but step-based workflows also support the same variables via
substituteWorkflowVariables in executor.ts. Updated to say all
workflow types.
* fix: address PR #450 review findings in DAG workflow engine
Correctness:
- Remove throw from !anyCompleted path to prevent double workflow_failed
emission; add safeSendMessage and return instead
- Guard lastSequentialSessionId assignment against undefined overwrite
Type safety:
- Narrow workflowProvider from string to 'claude' | 'codex' in
resolveNodeProviderAndModel and executeDagWorkflow signatures
- Remove unsafe 'as claude | codex' cast
- Add compile-time assertion that NodeOutput covers all NodeState values
Silent failure surfacing:
- Pre-execution node failure now notifies user via safeSendMessage
- Unexpected Promise.allSettled rejection notifies user and logs layerIdx
- completeWorkflowRun DB failure notifies user of potential inconsistency
- Codex node with output_format now warns user (not just server log)
- Make resolveNodeProviderAndModel async to support the above
Dead code:
- Remove unused 'export type { MergedConfig }' re-export
Comments:
- Update safeSendMessage/substituteWorkflowVariables/loadCommandPrompt
TODOs to reflect Rule of Three is now met
- Fix executeNodeInternal docstring to mention context:'fresh' nodes
- Fix evaluateCondition @param: "settled" not "completed" upstreams
- Fix NodeOutput doc: "JSON-encoded string from the SDK"
Tests (7 new):
- substituteNodeOutputRefs: unknown node ref resolves to empty string
- checkTriggerRule: absent upstream synthesised as failed (x2)
- buildTopologicalLayers: two independent chains share layers correctly
- evaluateCondition: valid expression returns parsed: true
2026-02-18 13:13:22 +00:00
}