Archon/packages/workflows/src/dag-executor.ts

2913 lines
104 KiB
TypeScript
Raw Normal View History

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';
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,
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,
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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,
EffortLevel,
ThinkingConfig,
SandboxSettings,
refactor(workflows): eliminate types.ts re-export shim (step 2.1) (#844) * refactor(workflows): eliminate types.ts re-export shim (step 2.1) Move the 4 non-schema types (LoadCommandResult, WorkflowExecutionResult, WorkflowLoadError, WorkflowLoadResult) into schemas/workflow.ts where WorkflowDefinition already lives. Relocate the compile-time NodeOutput/ NodeState assertion to schemas/workflow-run.ts. Add DagWorkflow alias and all 4 types to schemas/index.ts. Update index.ts barrel to re-export from ./schemas directly. Update all 21 import lines across 12 source files and 9 test files. Rename types.test.ts → schemas.test.ts. Delete types.ts. The import chain is now one level shallower: caller → ./schemas → specific schema file (was: caller → ./types → ./schemas → schema file). External consumers of @archon/workflows are unaffected. Fixes #842 * docs: address review findings from PR #844 - Update schemas/workflow.ts file header to reflect that it now also contains non-Zod hand-written result types (LoadCommandResult, WorkflowExecutionResult, WorkflowLoadError, WorkflowLoadResult) - Remove stale types.ts entry from CLAUDE.md directory structure listing (file was deleted in this PR) * fix(docs): update stale references after types.ts deletion - CLAUDE.md: wrong-example comment now notes the subpath no longer exists (not just "don't do this in web") - LoadCommandResult JSDoc: clarify non-empty content is enforced at load time in executor-shared.ts, not by the type itself
2026-03-26 22:07:46 +00:00
} from './schemas';
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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';
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';
refactor: extract shared executor infrastructure and split workflow discovery (#685) * refactor: extract shared executor infrastructure and split workflow discovery Three targeted simplifications from the architectural assessment: 1. Extract executor-shared.ts: FATAL_PATTERNS, TRANSIENT_PATTERNS, classifyError(), loadCommandPrompt(), substituteWorkflowVariables(), buildPromptWithContext(), and resolveProjectPaths() were duplicated verbatim between executor.ts and dag-executor.ts (Rule of Three explicitly noted in TODO comments). Both files now import from the shared module. 2. Extract workflow-discovery.ts: discoverWorkflows() and loadWorkflowsFromDir() mixed filesystem-walking concerns into loader.ts alongside YAML parsing. Moving them to a dedicated module reduces loader.ts by ~268 lines and gives it a single focus. 3. Fix emitRetract missing in batch mode: orchestrator-agent.ts called platform.emitRetract() in stream mode but silently skipped it in batch mode for both workflowInvocation and projectRegistration branches. Added the same optional-chained call in handleBatchMode to match. * fix: address review findings from PR #685 - Delete dead execution-utils.ts (superseded by executor-shared.ts) - Restore $BASE_BRANCH fail-fast guard accidentally dropped in extraction - Fix logger domain in workflow-discovery.ts ('workflow.loader' → 'workflow.discovery') - Remove redundant access() calls before readFile() (TOCTOU elimination) - Inline single-use contextValue variable - Update CLAUDE.md directory tree (loader.ts annotation, new files) - Update cli-developer-guide.md discoverWorkflows location
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,
detectCreditExhaustion,
refactor: extract shared executor infrastructure and split workflow discovery (#685) * refactor: extract shared executor infrastructure and split workflow discovery Three targeted simplifications from the architectural assessment: 1. Extract executor-shared.ts: FATAL_PATTERNS, TRANSIENT_PATTERNS, classifyError(), loadCommandPrompt(), substituteWorkflowVariables(), buildPromptWithContext(), and resolveProjectPaths() were duplicated verbatim between executor.ts and dag-executor.ts (Rule of Three explicitly noted in TODO comments). Both files now import from the shared module. 2. Extract workflow-discovery.ts: discoverWorkflows() and loadWorkflowsFromDir() mixed filesystem-walking concerns into loader.ts alongside YAML parsing. Moving them to a dedicated module reduces loader.ts by ~268 lines and gives it a single focus. 3. Fix emitRetract missing in batch mode: orchestrator-agent.ts called platform.emitRetract() in stream mode but silently skipped it in batch mode for both workflowInvocation and projectRegistration branches. Added the same optional-chained call in handleBatchMode to match. * fix: address review findings from PR #685 - Delete dead execution-utils.ts (superseded by executor-shared.ts) - Restore $BASE_BRANCH fail-fast guard accidentally dropped in extraction - Fix logger domain in workflow-discovery.ts ('workflow.loader' → 'workflow.discovery') - Remove redundant access() calls before readFile() (TOCTOU elimination) - Inline single-use contextValue variable - Update CLAUDE.md directory tree (loader.ts annotation, new files) - Update cli-developer-guide.md discoverWorkflows location
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,
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
2026-04-09 11:48:02 +00:00
isInlineScript,
refactor: extract shared executor infrastructure and split workflow discovery (#685) * refactor: extract shared executor infrastructure and split workflow discovery Three targeted simplifications from the architectural assessment: 1. Extract executor-shared.ts: FATAL_PATTERNS, TRANSIENT_PATTERNS, classifyError(), loadCommandPrompt(), substituteWorkflowVariables(), buildPromptWithContext(), and resolveProjectPaths() were duplicated verbatim between executor.ts and dag-executor.ts (Rule of Three explicitly noted in TODO comments). Both files now import from the shared module. 2. Extract workflow-discovery.ts: discoverWorkflows() and loadWorkflowsFromDir() mixed filesystem-walking concerns into loader.ts alongside YAML parsing. Moving them to a dedicated module reduces loader.ts by ~268 lines and gives it a single focus. 3. Fix emitRetract missing in batch mode: orchestrator-agent.ts called platform.emitRetract() in stream mode but silently skipped it in batch mode for both workflowInvocation and projectRegistration branches. Added the same optional-chained call in handleBatchMode to match. * fix: address review findings from PR #685 - Delete dead execution-utils.ts (superseded by executor-shared.ts) - Restore $BASE_BRANCH fail-fast guard accidentally dropped in extraction - Fix logger domain in workflow-discovery.ts ('workflow.loader' → 'workflow.discovery') - Remove redundant access() calls before readFile() (TOCTOU elimination) - Inline single-use contextValue variable - Update CLAUDE.md directory tree (loader.ts annotation, new files) - Update cli-developer-guide.md discoverWorkflows location
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;
}
/** Workflow-level Claude SDK options — per-node overrides take precedence via ?? */
interface WorkflowLevelOptions {
effort?: EffortLevel;
thinking?: ThinkingConfig;
fallbackModel?: string;
betas?: string[];
sandbox?: SandboxSettings;
}
/** Internal node execution result — extends NodeOutput with cost data for aggregation. */
type NodeExecutionResult = NodeOutput & { costUsd?: number };
/** 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) */
const lastNodeActivityUpdate = new Map<string, number>();
const ACTIVITY_HEARTBEAT_INTERVAL_MS = 60_000;
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;
}
/** 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',
};
}
/**
* 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.
*/
function isTransientNodeError(errorMessage: string): boolean {
return classifyError(new Error(errorMessage)) === 'TRANSIENT';
}
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;
}
}
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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.
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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,
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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,
Fix: warn on unknown $nodeId.output refs instead of silently returning empty string (#593) * Fix: warn on unknown \$nodeId.output refs instead of silently returning empty string (#565) Typos in $nodeId.output references (e.g. $clasify.output vs $classify.output) were silently replaced with empty strings with no log output, making them impossible to diagnose. Load-time validation also skipped these refs, so broken workflows loaded without error. Changes: - substituteNodeOutputRefs(): log warn 'dag_node_output_ref_unknown_node' for unknown nodeId - resolveOutputRef(): log warn 'condition_output_ref_unknown_node' for unknown nodeId (distinguishes "node doesn't exist" from "node has empty output") - validateDagStructure(): validate $nodeId.output refs in when: and prompt: fields at load time - Update tests to assert warning is logged for unknown node refs - Move condition-evaluator.test.ts to its own bun test invocation (uses mock.module) Fixes #565 * fix: address review findings from PR #593 - Add loader.test.ts coverage for load-time $nodeId.output ref validation (5 tests: bad when: ref, bad prompt: ref, valid refs, multi-source lastIndex reset, bash: excluded from load-time validation) - Rename _match → match in dag-executor substituteNodeOutputRefs callback - Tighten warn count assertion to .toBe(2) in condition-evaluator.test.ts - Update validateDagStructure JSDoc to list fourth validation check - Update resolveOutputRef JSDoc to document warn-vs-silent distinction - Add inline comment on lastIndex = 0 explaining stateful g-flag regex reset
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);
Fix: warn on unknown $nodeId.output refs instead of silently returning empty string (#593) * Fix: warn on unknown \$nodeId.output refs instead of silently returning empty string (#565) Typos in $nodeId.output references (e.g. $clasify.output vs $classify.output) were silently replaced with empty strings with no log output, making them impossible to diagnose. Load-time validation also skipped these refs, so broken workflows loaded without error. Changes: - substituteNodeOutputRefs(): log warn 'dag_node_output_ref_unknown_node' for unknown nodeId - resolveOutputRef(): log warn 'condition_output_ref_unknown_node' for unknown nodeId (distinguishes "node doesn't exist" from "node has empty output") - validateDagStructure(): validate $nodeId.output refs in when: and prompt: fields at load time - Update tests to assert warning is logged for unknown node refs - Move condition-evaluator.test.ts to its own bun test invocation (uses mock.module) Fixes #565 * fix: address review findings from PR #593 - Add loader.test.ts coverage for load-time $nodeId.output ref validation (5 tests: bad when: ref, bad prompt: ref, valid refs, multi-source lastIndex reset, bash: excluded from load-time validation) - Rename _match → match in dag-executor substituteNodeOutputRefs callback - Tighten warn count assertion to .toBe(2) in condition-evaluator.test.ts - Update validateDagStructure JSDoc to list fourth validation check - Update resolveOutputRef JSDoc to document warn-vs-silent distinction - Add inline comment on lastIndex = 0 explaining stateful g-flag regex reset
2026-03-13 07:31:21 +00:00
if (!nodeOutput) {
getLog().warn({ nodeId, match }, 'dag_node_output_ref_unknown_node');
return escapedForBash ? "''" : '';
}
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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];
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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);
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
2026-03-13 07:29:39 +00:00
return escapedForBash ? "''" : ''; // objects, null, undefined, symbol, bigint → empty
feat: add per-node SDK hooks to DAG workflows (#634) * feat: add per-node SDK hooks to DAG workflows Thread hooks field through the full options pipeline: DagNodeBase.hooks → WorkflowAssistantOptions.hooks → AssistantRequestOptions.hooks → SDK Options.hooks YAML workflows define hooks declaratively with response objects that map 1:1 to the Claude Agent SDK's SyncHookJSONOutput. At runtime, each YAML hook is wrapped in async () => response. Supports all 21 SDK hook events with optional matcher and timeout fields. - Add WorkflowHookEvent union, WorkflowHookMatcher, WorkflowNodeHooks types - Add parseNodeHooks() validator in loader with full error reporting - Add buildSDKHooksFromYAML() converter in dag-executor - Forward hooks through claude.ts to SDK Options - Add hooks JSON editor in Web UI NodeInspector (hidden for bash/Codex) - Add archon-architect example workflow showcasing hooks patterns - 22 tests covering parsing, SDK conversion, and integration - Documentation in docs/hooks.md * fix: address review findings for per-node hooks - Sync VALID_HOOK_EVENTS with WorkflowHookEvent via typed constant (mirrors TRIGGER_RULES pattern, prevents future drift) - Guard buildSDKHooksFromYAML against returning empty {} silently - Add Array.isArray guard to response validation in parseNodeHooks - Fix Codex warning log levels from .error to .warn (non-fatal) - Bind catch error in substituteNodeOutputRefs for better diagnostics - Add integration tests: hooks→sendQuery, Codex+hooks warning - Add array response rejection test - Fix parseNodeHooks docstring to document all return cases - Remove unverified SDK version comment on hooks - Add Claude-only note to AssistantRequestOptions.hooks docstring - Add hooks field to authoring-workflows.md node fields table - Fix broken code.claude.com URL in hooks.md - Clarify timeout unit ambiguity in WorkflowHookMatcher JSDoc
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(
feat: add per-node SDK hooks to DAG workflows (#634) * feat: add per-node SDK hooks to DAG workflows Thread hooks field through the full options pipeline: DagNodeBase.hooks → WorkflowAssistantOptions.hooks → AssistantRequestOptions.hooks → SDK Options.hooks YAML workflows define hooks declaratively with response objects that map 1:1 to the Claude Agent SDK's SyncHookJSONOutput. At runtime, each YAML hook is wrapped in async () => response. Supports all 21 SDK hook events with optional matcher and timeout fields. - Add WorkflowHookEvent union, WorkflowHookMatcher, WorkflowNodeHooks types - Add parseNodeHooks() validator in loader with full error reporting - Add buildSDKHooksFromYAML() converter in dag-executor - Forward hooks through claude.ts to SDK Options - Add hooks JSON editor in Web UI NodeInspector (hidden for bash/Codex) - Add archon-architect example workflow showcasing hooks patterns - 22 tests covering parsing, SDK conversion, and integration - Documentation in docs/hooks.md * fix: address review findings for per-node hooks - Sync VALID_HOOK_EVENTS with WorkflowHookEvent via typed constant (mirrors TRIGGER_RULES pattern, prevents future drift) - Guard buildSDKHooksFromYAML against returning empty {} silently - Add Array.isArray guard to response validation in parseNodeHooks - Fix Codex warning log levels from .error to .warn (non-fatal) - Bind catch error in substituteNodeOutputRefs for better diagnostics - Add integration tests: hooks→sendQuery, Codex+hooks warning - Add array response rejection test - Fix parseNodeHooks docstring to document all return cases - Remove unverified SDK version comment on hooks - Add Claude-only note to AssistantRequestOptions.hooks docstring - Add hooks field to authoring-workflows.md node fields table - Fix broken code.claude.com URL in hooks.md - Clarify timeout unit ambiguity in WorkflowHookMatcher JSDoc
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'
);
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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,
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}"`
);
}
// 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],
feat(workflows): inline sub-agent definitions on DAG nodes (#1276) * feat(workflows): inline sub-agent definitions on DAG nodes Add `agents:` node field letting workflow YAML define Claude Agent SDK sub-agents inline, keyed by kebab-case ID. The main agent can spawn them via the Task tool — useful for map-reduce patterns where a cheap model briefs items and a stronger model reduces. Authors no longer need standalone `.claude/agents/*.md` files for workflow-scoped helpers; the definitions live with the workflow. Claude only. Codex and community providers without the capability emit a capability warning and ignore the field. Merges with the internal `dag-node-skills` wrapper when `skills:` is also set — user-defined agents win on ID collision. * fix(workflows): address PR #1276 review feedback Critical: - Re-export agentDefinitionSchema + AgentDefinition from schemas/index.ts (matches the "schemas/index.ts re-exports all" convention). Important: - Surface user-override of internal 'dag-node-skills' wrapper: warn-level provider log + platform message to the user when agents: redefines the reserved ID alongside skills:. User-wins behavior preserved (by design) but silent capability removal is now observable. - Add validator test coverage for the agents-capability warning (codex node with agents: → warning; claude node → no warning; no-agents field → no warning). - Strengthen NodeConfig.agents duplicate-type comment explaining the intentional circular-dep avoidance and pointing at the Zod schema as authoritative source. Actual extraction is follow-up work. Simplifications: - Drop redundant typeof check in validator (schema already enforces). - Drop unreachable Object.keys(...).length > 0 check in dag-executor. - Drop rot-prone "(out of v1 scope)" parenthetical. - Drop WHAT-only comment on AGENT_ID_REGEX. - Tighten AGENT_ID_REGEX to reject trailing/double hyphens (/^[a-z0-9]+(-[a-z0-9]+)*$/). Tests: - parseWorkflow strips agents on script: and loop: nodes (parallel to the existing bash: coverage). - provider emits warn log on dag-node-skills collision; no warn on non-colliding inline agents. Docs: - Renumber authoring-workflows Summary section (12b → 13; bump 13-19). - Add Pi capability-table row for inline agents (❌, Claude-only). - Add when-to-use guidance (agents: vs .claude/agents/*.md) in the new "Inline sub-agents" section. - Cross-link skills.md Related → inline-sub-agents. - CHANGELOG [Unreleased] Added entry for #1276.
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],
['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');
feat: add per-node SDK hooks to DAG workflows (#634) * feat: add per-node SDK hooks to DAG workflows Thread hooks field through the full options pipeline: DagNodeBase.hooks → WorkflowAssistantOptions.hooks → AssistantRequestOptions.hooks → SDK Options.hooks YAML workflows define hooks declaratively with response objects that map 1:1 to the Claude Agent SDK's SyncHookJSONOutput. At runtime, each YAML hook is wrapped in async () => response. Supports all 21 SDK hook events with optional matcher and timeout fields. - Add WorkflowHookEvent union, WorkflowHookMatcher, WorkflowNodeHooks types - Add parseNodeHooks() validator in loader with full error reporting - Add buildSDKHooksFromYAML() converter in dag-executor - Forward hooks through claude.ts to SDK Options - Add hooks JSON editor in Web UI NodeInspector (hidden for bash/Codex) - Add archon-architect example workflow showcasing hooks patterns - 22 tests covering parsing, SDK conversion, and integration - Documentation in docs/hooks.md * fix: address review findings for per-node hooks - Sync VALID_HOOK_EVENTS with WorkflowHookEvent via typed constant (mirrors TRIGGER_RULES pattern, prevents future drift) - Guard buildSDKHooksFromYAML against returning empty {} silently - Add Array.isArray guard to response validation in parseNodeHooks - Fix Codex warning log levels from .error to .warn (non-fatal) - Bind catch error in substituteNodeOutputRefs for better diagnostics - Add integration tests: hooks→sendQuery, Codex+hooks warning - Add array response rejection test - Fix parseNodeHooks docstring to document all return cases - Remove unverified SDK version comment on hooks - Add Claude-only note to AssistantRequestOptions.hooks docstring - Add hooks field to authoring-workflows.md node fields table - Fix broken code.claude.com URL in hooks.md - Clarify timeout unit ambiguity in WorkflowHookMatcher JSDoc
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.`,
feat: add per-node SDK hooks to DAG workflows (#634) * feat: add per-node SDK hooks to DAG workflows Thread hooks field through the full options pipeline: DagNodeBase.hooks → WorkflowAssistantOptions.hooks → AssistantRequestOptions.hooks → SDK Options.hooks YAML workflows define hooks declaratively with response objects that map 1:1 to the Claude Agent SDK's SyncHookJSONOutput. At runtime, each YAML hook is wrapped in async () => response. Supports all 21 SDK hook events with optional matcher and timeout fields. - Add WorkflowHookEvent union, WorkflowHookMatcher, WorkflowNodeHooks types - Add parseNodeHooks() validator in loader with full error reporting - Add buildSDKHooksFromYAML() converter in dag-executor - Forward hooks through claude.ts to SDK Options - Add hooks JSON editor in Web UI NodeInspector (hidden for bash/Codex) - Add archon-architect example workflow showcasing hooks patterns - 22 tests covering parsing, SDK conversion, and integration - Documentation in docs/hooks.md * fix: address review findings for per-node hooks - Sync VALID_HOOK_EVENTS with WorkflowHookEvent via typed constant (mirrors TRIGGER_RULES pattern, prevents future drift) - Guard buildSDKHooksFromYAML against returning empty {} silently - Add Array.isArray guard to response validation in parseNodeHooks - Fix Codex warning log levels from .error to .warn (non-fatal) - Bind catch error in substituteNodeOutputRefs for better diagnostics - Add integration tests: hooks→sendQuery, Codex+hooks warning - Add array response rejection test - Fix parseNodeHooks docstring to document all return cases - Remove unverified SDK version comment on hooks - Add Claude-only note to AssistantRequestOptions.hooks docstring - Add hooks field to authoring-workflows.md node fields table - Fix broken code.claude.com URL in hooks.md - Clarify timeout unit ambiguity in WorkflowHookMatcher JSDoc
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');
feat: add per-node SDK hooks to DAG workflows (#634) * feat: add per-node SDK hooks to DAG workflows Thread hooks field through the full options pipeline: DagNodeBase.hooks → WorkflowAssistantOptions.hooks → AssistantRequestOptions.hooks → SDK Options.hooks YAML workflows define hooks declaratively with response objects that map 1:1 to the Claude Agent SDK's SyncHookJSONOutput. At runtime, each YAML hook is wrapped in async () => response. Supports all 21 SDK hook events with optional matcher and timeout fields. - Add WorkflowHookEvent union, WorkflowHookMatcher, WorkflowNodeHooks types - Add parseNodeHooks() validator in loader with full error reporting - Add buildSDKHooksFromYAML() converter in dag-executor - Forward hooks through claude.ts to SDK Options - Add hooks JSON editor in Web UI NodeInspector (hidden for bash/Codex) - Add archon-architect example workflow showcasing hooks patterns - 22 tests covering parsing, SDK conversion, and integration - Documentation in docs/hooks.md * fix: address review findings for per-node hooks - Sync VALID_HOOK_EVENTS with WorkflowHookEvent via typed constant (mirrors TRIGGER_RULES pattern, prevents future drift) - Guard buildSDKHooksFromYAML against returning empty {} silently - Add Array.isArray guard to response validation in parseNodeHooks - Fix Codex warning log levels from .error to .warn (non-fatal) - Bind catch error in substituteNodeOutputRefs for better diagnostics - Add integration tests: hooks→sendQuery, Codex+hooks warning - Add array response rejection test - Fix parseNodeHooks docstring to document all return cases - Remove unverified SDK version comment on hooks - Add Claude-only note to AssistantRequestOptions.hooks docstring - Add hooks field to authoring-workflows.md node fields table - Fix broken code.claude.com URL in hooks.md - Clarify timeout unit ambiguity in WorkflowHookMatcher JSDoc
2026-03-16 09:08:35 +00:00
}
}
feat(workflows): inline sub-agent definitions on DAG nodes (#1276) * feat(workflows): inline sub-agent definitions on DAG nodes Add `agents:` node field letting workflow YAML define Claude Agent SDK sub-agents inline, keyed by kebab-case ID. The main agent can spawn them via the Task tool — useful for map-reduce patterns where a cheap model briefs items and a stronger model reduces. Authors no longer need standalone `.claude/agents/*.md` files for workflow-scoped helpers; the definitions live with the workflow. Claude only. Codex and community providers without the capability emit a capability warning and ignore the field. Merges with the internal `dag-node-skills` wrapper when `skills:` is also set — user-defined agents win on ID collision. * fix(workflows): address PR #1276 review feedback Critical: - Re-export agentDefinitionSchema + AgentDefinition from schemas/index.ts (matches the "schemas/index.ts re-exports all" convention). Important: - Surface user-override of internal 'dag-node-skills' wrapper: warn-level provider log + platform message to the user when agents: redefines the reserved ID alongside skills:. User-wins behavior preserved (by design) but silent capability removal is now observable. - Add validator test coverage for the agents-capability warning (codex node with agents: → warning; claude node → no warning; no-agents field → no warning). - Strengthen NodeConfig.agents duplicate-type comment explaining the intentional circular-dep avoidance and pointing at the Zod schema as authoritative source. Actual extraction is follow-up work. Simplifications: - Drop redundant typeof check in validator (schema already enforces). - Drop unreachable Object.keys(...).length > 0 check in dag-executor. - Drop rot-prone "(out of v1 scope)" parenthetical. - Drop WHAT-only comment on AGENT_ID_REGEX. - Tighten AGENT_ID_REGEX to reject trailing/double hyphens (/^[a-z0-9]+(-[a-z0-9]+)*$/). Tests: - parseWorkflow strips agents on script: and loop: nodes (parallel to the existing bash: coverage). - provider emits warn log on dag-node-skills collision; no warn on non-colliding inline agents. Docs: - Renumber authoring-workflows Summary section (12b → 13; bump 13-19). - Add Pi capability-table row for inline agents (❌, Claude-only). - Add when-to-use guidance (agents: vs .claude/agents/*.md) in the new "Inline sub-agents" section. - Cross-link skills.md Related → inline-sub-agents. - CHANGELOG [Unreleased] Added entry for #1276.
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 };
feat: add per-node skills for DAG workflows (#446) (#689) * feat: add per-node skills for DAG workflows (#446) Add `skills: [name, ...]` field to DAG workflow nodes. When a node has skills, the executor wraps it in an AgentDefinition that preloads those skills into the subagent context. 'Skill' is auto-added to allowedTools. - Add skills field to DagNodeBase type and loader validation - Add agents + agent fields to WorkflowAssistantOptions and AssistantRequestOptions for AgentDefinition wrapping - Pass agents/agent through ClaudeClient to SDK Options - Build AgentDefinition with preloaded skills in dag-executor - Add Codex warning (per-node skills not supported) - Add skills comma-separated input in Web UI NodeInspector - Add skills to WorkflowCanvas reactFlowToDagNodes conversion - Add 5 unit tests for skills parsing validation * feat: add Remotion video generation workflow and skills/MCP docs Add archon-remotion-generate as a bundled default workflow that uses per-node skills (remotion-best-practices) to generate Remotion video compositions with AI, then renders preview stills and full video via deterministic bash nodes. - Add archon-remotion-generate.yaml to defaults - Add docs/remotion-workflow.md with setup guide and examples - Add docs/skills.md documenting per-node skills feature (#446) - Register workflow in bundled-defaults.ts for binary builds * chore: gitignore skills-lock.json * fix: address review feedback for per-node skills (#446) - Always pass ['Skill'] in AgentDefinition.tools instead of undefined - Surface buildPromptWithContext errors to user via safeSendMessage - Use AgentDefinition from SDK in AssistantRequestOptions (remove cast) - Warn and skip non-object MCP server configs instead of silent passthrough - Include JSON parse error detail in loadMcpConfig error messages - Add 3 executor-level tests for skills options construction - Update bundled-defaults test count for remotion workflow - Add skills to CLAUDE.md, authoring-workflows.md, README.md, hooks.md - Clean up loader skills validation with type predicate
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,
feat(workflows): inline sub-agent definitions on DAG nodes (#1276) * feat(workflows): inline sub-agent definitions on DAG nodes Add `agents:` node field letting workflow YAML define Claude Agent SDK sub-agents inline, keyed by kebab-case ID. The main agent can spawn them via the Task tool — useful for map-reduce patterns where a cheap model briefs items and a stronger model reduces. Authors no longer need standalone `.claude/agents/*.md` files for workflow-scoped helpers; the definitions live with the workflow. Claude only. Codex and community providers without the capability emit a capability warning and ignore the field. Merges with the internal `dag-node-skills` wrapper when `skills:` is also set — user-defined agents win on ID collision. * fix(workflows): address PR #1276 review feedback Critical: - Re-export agentDefinitionSchema + AgentDefinition from schemas/index.ts (matches the "schemas/index.ts re-exports all" convention). Important: - Surface user-override of internal 'dag-node-skills' wrapper: warn-level provider log + platform message to the user when agents: redefines the reserved ID alongside skills:. User-wins behavior preserved (by design) but silent capability removal is now observable. - Add validator test coverage for the agents-capability warning (codex node with agents: → warning; claude node → no warning; no-agents field → no warning). - Strengthen NodeConfig.agents duplicate-type comment explaining the intentional circular-dep avoidance and pointing at the Zod schema as authoritative source. Actual extraction is follow-up work. Simplifications: - Drop redundant typeof check in validator (schema already enforces). - Drop unreachable Object.keys(...).length > 0 check in dag-executor. - Drop rot-prone "(out of v1 scope)" parenthetical. - Drop WHAT-only comment on AGENT_ID_REGEX. - Tighten AGENT_ID_REGEX to reject trailing/double hyphens (/^[a-z0-9]+(-[a-z0-9]+)*$/). Tests: - parseWorkflow strips agents on script: and loop: nodes (parallel to the existing bash: coverage). - provider emits warn log on dag-node-skills collision; no warn on non-colliding inline agents. Docs: - Renumber authoring-workflows Summary section (12b → 13; bump 13-19). - Add Pi capability-table row for inline agents (❌, Claude-only). - Add when-to-use guidance (agents: vs .claude/agents/*.md) in the new "Inline sub-agents" section. - Cross-link skills.md Related → inline-sub-agents. - CHANGELOG [Unreleased] Added entry for #1276.
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,
};
feat: add per-node skills for DAG workflows (#446) (#689) * feat: add per-node skills for DAG workflows (#446) Add `skills: [name, ...]` field to DAG workflow nodes. When a node has skills, the executor wraps it in an AgentDefinition that preloads those skills into the subagent context. 'Skill' is auto-added to allowedTools. - Add skills field to DagNodeBase type and loader validation - Add agents + agent fields to WorkflowAssistantOptions and AssistantRequestOptions for AgentDefinition wrapping - Pass agents/agent through ClaudeClient to SDK Options - Build AgentDefinition with preloaded skills in dag-executor - Add Codex warning (per-node skills not supported) - Add skills comma-separated input in Web UI NodeInspector - Add skills to WorkflowCanvas reactFlowToDagNodes conversion - Add 5 unit tests for skills parsing validation * feat: add Remotion video generation workflow and skills/MCP docs Add archon-remotion-generate as a bundled default workflow that uses per-node skills (remotion-best-practices) to generate Remotion video compositions with AI, then renders preview stills and full video via deterministic bash nodes. - Add archon-remotion-generate.yaml to defaults - Add docs/remotion-workflow.md with setup guide and examples - Add docs/skills.md documenting per-node skills feature (#446) - Register workflow in bundled-defaults.ts for binary builds * chore: gitignore skills-lock.json * fix: address review feedback for per-node skills (#446) - Always pass ['Skill'] in AgentDefinition.tools instead of undefined - Surface buildPromptWithContext errors to user via safeSendMessage - Use AgentDefinition from SDK in AssistantRequestOptions (remove cast) - Warn and skip non-object MCP server configs instead of silent passthrough - Include JSON parse error detail in loadMcpConfig error messages - Add 3 executor-level tests for skills options construction - Update bundled-defaults test count for remotion workflow - Add skills to CLAUDE.md, authoring-workflows.md, README.md, hooks.md - Clean up loader skills validation with type predicate
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] ?? {};
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;
}
/**
* 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,
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
): 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
fix: require explicit base branch config for worktree creation (#686) * fix: require explicit base branch config for worktree creation When no `worktree.baseBranch` is configured in .archon/config.yaml, the system silently fell back to `getDefaultBranch()` which always resolved to `main`. This caused worktrees to branch from main instead of the intended branch (e.g., dev), producing merge conflicts. Now errors with actionable message when no base branch is set: "Set worktree.baseBranch in .archon/config.yaml or use --from flag" The --from flag continues to work as an override for one-off cases. Cleanup service still uses getDefaultBranch for merge checks. Also adds .archon/config.yaml with baseBranch: dev for this project. * fix: scope $BASE_BRANCH validation and improve error handling - Make $BASE_BRANCH resolution lazy: only fail when a workflow step actually references the variable, not on every workflow execution - Surface config load errors distinctly from missing-value errors in worktree provider (was masking real cause behind generic message) - Add 'no base branch configured' to known isolation error patterns so the error is classified and shown to users properly - Add tests for baseBranch/fromBranch precedence and non-task workflowType behavior - Update stale docs referencing removed auto-detection fallback across CLAUDE.md, README.md, and docs/ * fix: configure isolation loader in CLI workflow command The CLI was calling getIsolationProvider() without first calling configureIsolation(), so the worktree provider had no way to load .archon/config.yaml — all worktree creation failed with "No base branch configured" even when config existed.
2026-03-16 13:05:16 +00:00
let substitutedPrompt: string;
try {
substitutedPrompt = buildPromptWithContext(
rawPrompt,
workflowRun.id,
workflowRun.user_message,
artifactsDir,
baseBranch,
docsDir,
fix: require explicit base branch config for worktree creation (#686) * fix: require explicit base branch config for worktree creation When no `worktree.baseBranch` is configured in .archon/config.yaml, the system silently fell back to `getDefaultBranch()` which always resolved to `main`. This caused worktrees to branch from main instead of the intended branch (e.g., dev), producing merge conflicts. Now errors with actionable message when no base branch is set: "Set worktree.baseBranch in .archon/config.yaml or use --from flag" The --from flag continues to work as an override for one-off cases. Cleanup service still uses getDefaultBranch for merge checks. Also adds .archon/config.yaml with baseBranch: dev for this project. * fix: scope $BASE_BRANCH validation and improve error handling - Make $BASE_BRANCH resolution lazy: only fail when a workflow step actually references the variable, not on every workflow execution - Surface config load errors distinctly from missing-value errors in worktree provider (was masking real cause behind generic message) - Add 'no base branch configured' to known isolation error patterns so the error is classified and shown to users properly - Add tests for baseBranch/fromBranch precedence and non-task workflowType behavior - Update stale docs referencing removed auto-detection fallback across CLAUDE.md, README.md, and docs/ * fix: configure isolation loader in CLI workflow command The CLI was calling getIsolationProvider() without first calling configureIsolation(), so the worktree provider had no way to load .archon/config.yaml — all worktree creation failed with "No base branch configured" even when config existed.
2026-03-16 13:05:16 +00:00
issueContext,
`dag node '${node.id}' prompt`
);
} catch (error) {
const err = error as Error;
feat: add per-node skills for DAG workflows (#446) (#689) * feat: add per-node skills for DAG workflows (#446) Add `skills: [name, ...]` field to DAG workflow nodes. When a node has skills, the executor wraps it in an AgentDefinition that preloads those skills into the subagent context. 'Skill' is auto-added to allowedTools. - Add skills field to DagNodeBase type and loader validation - Add agents + agent fields to WorkflowAssistantOptions and AssistantRequestOptions for AgentDefinition wrapping - Pass agents/agent through ClaudeClient to SDK Options - Build AgentDefinition with preloaded skills in dag-executor - Add Codex warning (per-node skills not supported) - Add skills comma-separated input in Web UI NodeInspector - Add skills to WorkflowCanvas reactFlowToDagNodes conversion - Add 5 unit tests for skills parsing validation * feat: add Remotion video generation workflow and skills/MCP docs Add archon-remotion-generate as a bundled default workflow that uses per-node skills (remotion-best-practices) to generate Remotion video compositions with AI, then renders preview stills and full video via deterministic bash nodes. - Add archon-remotion-generate.yaml to defaults - Add docs/remotion-workflow.md with setup guide and examples - Add docs/skills.md documenting per-node skills feature (#446) - Register workflow in bundled-defaults.ts for binary builds * chore: gitignore skills-lock.json * fix: address review feedback for per-node skills (#446) - Always pass ['Skill'] in AgentDefinition.tools instead of undefined - Surface buildPromptWithContext errors to user via safeSendMessage - Use AgentDefinition from SDK in AssistantRequestOptions (remove cast) - Warn and skip non-object MCP server configs instead of silent passthrough - Include JSON parse error detail in loadMcpConfig error messages - Add 3 executor-level tests for skills options construction - Update bundled-defaults test count for remotion workflow - Add skills to CLAUDE.md, authoring-workflows.md, README.md, hooks.md - Clean up loader skills validation with type predicate
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
);
fix: require explicit base branch config for worktree creation (#686) * fix: require explicit base branch config for worktree creation When no `worktree.baseBranch` is configured in .archon/config.yaml, the system silently fell back to `getDefaultBranch()` which always resolved to `main`. This caused worktrees to branch from main instead of the intended branch (e.g., dev), producing merge conflicts. Now errors with actionable message when no base branch is set: "Set worktree.baseBranch in .archon/config.yaml or use --from flag" The --from flag continues to work as an override for one-off cases. Cleanup service still uses getDefaultBranch for merge checks. Also adds .archon/config.yaml with baseBranch: dev for this project. * fix: scope $BASE_BRANCH validation and improve error handling - Make $BASE_BRANCH resolution lazy: only fail when a workflow step actually references the variable, not on every workflow execution - Surface config load errors distinctly from missing-value errors in worktree provider (was masking real cause behind generic message) - Add 'no base branch configured' to known isolation error patterns so the error is classified and shown to users properly - Add tests for baseBranch/fromBranch precedence and non-task workflowType behavior - Update stale docs referencing removed auto-detection fallback across CLAUDE.md, README.md, and docs/ * fix: configure isolation loader in CLI workflow command The CLI was calling getIsolationProvider() without first calling configureIsolation(), so the worktree provider had no way to load .archon/config.yaml — all worktree creation failed with "No base branch configured" even when config existed.
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);
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
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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;
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[] = [];
// Create per-node abort controller for idle timeout cleanup
const nodeAbortController = new AbortController();
feat: safe session continuity across workflow steps with forkSession-based retries (#729) * feat: safe session continuity across workflow steps with forkSession-based retries (#691) Retries previously discarded all prior conversation context by passing resumeSessionId=undefined, because naive session resume would append to the original session file and corrupt it on a crash. This adds forkSession:true support so the SDK copies the prior session history into a new file before appending — leaving the source untouched — making it safe to always pass the prior session ID on retry. Changes: - Add forkSession?: boolean to WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts) - Pass forkSession to SDK Options in ClaudeClient with enhanced resuming_session log - Sequential executor: set shouldForkSession=true when resuming; inject into stepOptions; fix retry to always pass resumeSessionId - DAG executor: set shouldForkSession=true when resuming in executeNodeInternal; fix retry to always pass resumeSessionId; update isFresh comment - Fix resolvedOptions to explicitly set persistSession:false for Claude workflows (was silently defaulting to SDK's true) - Add context:'shared' union to DagNodeBase.context type; update clearContext docstring Fixes #691 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove persistSession:false from workflow resolvedOptions to preserve cross-step session continuity SDK docs confirm that persistSession:false prevents session files from being written to disk, making resume impossible for subsequent steps. Removing this option restores the SDK default (true), so step 1's session ID can be used by step 2 to resume. Also clarify context:'shared' docstring to note it has no effect on parallel layer nodes, and add unit tests asserting that retries pass forkSession:true when a prior session exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add persistSession: false to Claude resolvedOptions Implements the missing persistSession fix claimed in the PR description. Without this, the SDK defaults to true and writes session transcripts to disk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(loader): preserve context: 'shared' in DAG node YAML parsing The PR added 'shared' to DagNodeBase.context type but the loader only preserved 'fresh', silently dropping 'shared' during YAML parsing. This meant context: shared in workflow YAML was ignored at load time. 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 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 = {
...nodeOptions,
abortSignal: nodeAbortController.signal,
feat: safe session continuity across workflow steps with forkSession-based retries (#729) * feat: safe session continuity across workflow steps with forkSession-based retries (#691) Retries previously discarded all prior conversation context by passing resumeSessionId=undefined, because naive session resume would append to the original session file and corrupt it on a crash. This adds forkSession:true support so the SDK copies the prior session history into a new file before appending — leaving the source untouched — making it safe to always pass the prior session ID on retry. Changes: - Add forkSession?: boolean to WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts) - Pass forkSession to SDK Options in ClaudeClient with enhanced resuming_session log - Sequential executor: set shouldForkSession=true when resuming; inject into stepOptions; fix retry to always pass resumeSessionId - DAG executor: set shouldForkSession=true when resuming in executeNodeInternal; fix retry to always pass resumeSessionId; update isFresh comment - Fix resolvedOptions to explicitly set persistSession:false for Claude workflows (was silently defaulting to SDK's true) - Add context:'shared' union to DagNodeBase.context type; update clearContext docstring Fixes #691 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove persistSession:false from workflow resolvedOptions to preserve cross-step session continuity SDK docs confirm that persistSession:false prevents session files from being written to disk, making resume impossible for subsequent steps. Removing this option restores the SDK default (true), so step 1's session ID can be used by step 2 to resume. Also clarify context:'shared' docstring to note it has no effect on parallel layer nodes, and add unit tests asserting that retries pass forkSession:true when a prior session exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add persistSession: false to Claude resolvedOptions Implements the missing persistSession fix claimed in the PR description. Without this, the SDK defaults to true and writes session transcripts to disk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(loader): preserve context: 'shared' in DAG node YAML parsing The PR added 'shared' to DagNodeBase.context type but the loader only preserved 'fresh', silently dropping 'shared' during YAML parsing. This meant context: shared in workflow YAML was ignored at load time. 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 23:06:23 +00:00
...(shouldForkSession ? { forkSession: true } : {}),
};
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;
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 {
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,
() => {
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 },
'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
}
)) {
const tickNow = Date.now();
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.
if (tickNow - (lastNodeCancelCheck.get(nodeKey) ?? 0) > CANCEL_CHECK_INTERVAL_MS) {
lastNodeCancelCheck.set(nodeKey, tickNow);
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')) {
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'
);
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'
);
}
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
}
// Activity heartbeat — write, throttled to every 60s (only for stale/zombie detection)
if (tickNow - (lastNodeActivityUpdate.get(nodeKey) ?? 0) > ACTIVITY_HEARTBEAT_INTERVAL_MS) {
lastNodeActivityUpdate.set(nodeKey, tickNow);
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);
// 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 ?? {});
// 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'
);
});
} 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;
if (msg.cost !== undefined) nodeCostUsd = msg.cost;
if (msg.stopReason !== undefined) nodeStopReason = msg.stopReason;
if (msg.numTurns !== undefined) nodeNumTurns = msg.numTurns;
if (msg.modelUsage) nodeModelUsage = msg.modelUsage;
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
2026-03-12 10:38:21 +00:00
if (msg.structuredOutput !== undefined) structuredOutput = msg.structuredOutput;
// Fail the node if the SDK reports a cost cap exceeded error
if (msg.isError && msg.errorSubtype === 'error_max_budget_usd') {
const cap = nodeOptions?.maxBudgetUsd;
getLog().warn(
{ nodeId: node.id, maxBudgetUsd: cap, durationMs: Date.now() - nodeStartTime },
'dag.node_budget_cap_exceeded'
);
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'
);
}
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
2026-03-12 10:38:21 +00:00
}
// rate_limit chunks: already log.warn'd in claude.ts; not surfaced to SSE per design
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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.
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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.
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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.`,
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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
}
}
// 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 },
'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.`,
nodeContext
);
}
// 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',
});
// Clean up throttle entries
lastNodeCancelCheck.delete(`${workflowRun.id}:${node.id}`);
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) {
fix: use SDK structured_output for output_format DAG nodes (#546) * Investigate issue #497: output_format mixed prose+JSON breaking conditions * fix: use SDK structured_output for output_format nodes (#497) When a DAG node uses output_format, the Claude SDK streams reasoning prose before the structured JSON. executeNodeInternal concatenated all assistant text, producing mixed content that broke JSON.parse() in downstream when: conditions. The SDK already provides validated JSON via structured_output on the result message — we just never read it. Now we forward it through WorkflowMessageChunk and use it to override nodeOutputText when output_format is set. Changes: - Add structuredOutput field to WorkflowMessageChunk result variant - Extract structured_output from SDK result in ClaudeClient.sendQuery - Override nodeOutputText with structuredOutput in executeNodeInternal - Add tests for structured output extraction and condition evaluation Fixes #497 * fix: move structured output override before post-loop side effects Address self-review finding: the nodeOutputText override should happen before logging/events, not after, so the canonical value is established before any downstream use. Also remove redundant assertion. * Archive investigation for issue #497 * fix: address review findings for structured output PR - Fix batch-mode sending raw prose+JSON to user when structuredOutput overrides nodeOutputText (use clean JSON for batch content instead) - Add warn log + user message when output_format is set but SDK returns no structured_output (prevents silent fallback to broken behavior) - Wrap JSON.stringify(structuredOutput) in try-catch with node context - Add debug log at structured output override point - Fix pre-existing domain prefix: activity_update_failed → dag.activity_update_failed - Add ClaudeClient tests for structuredOutput propagation (present/absent) - Add DAG executor test for output_format absent guard - Update docs/authoring-workflows.md to describe structured_output mechanism
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
}
// Detect credit exhaustion: SDK returns it as assistant text, not a thrown error.
const creditError = detectCreditExhaustion(nodeOutputText);
if (creditError) {
const duration = Date.now() - nodeStartTime;
getLog().warn({ nodeId: node.id, durationMs: duration }, 'dag.node_credit_exhausted');
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,
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,
...(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
});
// Clean up throttle entries on completion
lastNodeCancelCheck.delete(`${workflowRun.id}:${node.id}`);
lastNodeActivityUpdate.delete(`${workflowRun.id}:${node.id}`);
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;
// Clean up throttle entries on failure
lastNodeCancelCheck.delete(`${workflowRun.id}:${node.id}`);
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');
return {
state: 'failed',
output: nodeOutputText,
error: 'Cancelled by user',
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
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,
});
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
}
}
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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,
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>,
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,
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
);
Fix: shell injection via $nodeId.output in bash nodes (#591) * Fix: shell injection via \$nodeId.output in bash nodes (#566) Node output from AI or bash nodes was inlined unescaped into bash scripts before passing to `bash -c`, allowing shell metacharacters in upstream output to execute arbitrary commands. Changes: - Add `shellQuote()` helper that single-quotes values for safe shell embedding - Add `escapedForBash` parameter to `substituteNodeOutputRefs()` (default false) - Pass `escapedForBash = true` in `executeBashNode()` call site - Add unit tests covering metacharacter injection, single-quote escaping, missing refs, JSON field escaping, and numeric field passthrough Fixes #566 * test: add edge case coverage for shell-escaped node output refs Add 6 unit tests and 1 integration test addressing review findings: Unit tests (substituteNodeOutputRefs -- shell escaping): - Boolean JSON field is not quoted when escapedForBash=true (same branch as numeric) - Empty string output becomes quoted empty string when escapedForBash=true - Embedded newline in output is safe inside single-quoted strings - Object JSON field becomes quoted empty string when escapedForBash=true - Invalid JSON with field ref returns quoted empty string when escapedForBash=true Integration test (executeDagWorkflow -- bash nodes): - Upstream bash output with shell metacharacters does not inject into downstream bash script, confirming the call-site fix at dag-executor.ts:884 is effective Also add inline comment explaining why the number/boolean branch safely omits the escapedForBash check (JSON.parse guarantees no shell metacharacters).
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
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
2026-04-09 11:48:02 +00:00
const timeout = node.timeout ?? SUBPROCESS_DEFAULT_TIMEOUT;
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,
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 };
}
}
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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>,
issueContext?: string,
envVars?: Record<string, string>
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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;
const subprocessEnv =
envVars && Object.keys(envVars).length > 0 ? { ...process.env, ...envVars } : undefined;
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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';
// --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];
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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 };
}
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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/`;
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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';
args = ['--no-env-file', 'run', scriptDef.path];
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
2026-04-09 11:48:02 +00:00
}
}
const { stdout, stderr } = await execFileAsync(cmd, args, {
cwd,
timeout,
env: subprocessEnv,
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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:
* - 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,
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
): 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
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 {
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 };
}
// Detect interactive loop resume — check if workflowRun.metadata has loop gate state for this node
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:17:56 +00:00
const rawApproval = workflowRun.metadata?.approval;
const loopGateMeta = isApprovalContext(rawApproval) ? rawApproval : undefined;
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 = '';
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');
};
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
refactor(workflows): migrate engine types to Zod schemas (step 2) (#841) * refactor(workflows): migrate engine types to Zod schemas (step 2) - Add packages/workflows/src/schemas/ with dag-node, workflow, workflow-run, retry, loop, hooks schemas - Update types.ts to re-export inferred types from schemas (backward compatible) - Replace parseDagNode/parseSingleStep/parseStep/parseRetryConfig/parseToolList/parseNodeHooks/parseIdleTimeout in loader.ts with dagNodeSchema.safeParse() - Derive TRIGGER_RULES and WORKFLOW_HOOK_EVENTS from schema .options - Update CLAUDE.md with engine schema conventions Closes #825 * fix(docs): address review findings from comprehensive-pr-review - Fix backward-compat comment in loader.ts: loader.test.ts → hooks.test.ts - Fix schemas/index.ts docstring: "defined here" → "re-exported from this index" - Update CLAUDE.md: types.ts description, add schemas/ entry, add @hono/zod-openapi to @archon/workflows deps * fix(workflows): address review findings for Zod schema migration - Derive WorkflowDefinition from schema (Omit + intersection) instead of hand-written interface - Use dagNodeBaseSchema.extend() to eliminate 15 duplicated field declarations - Move mcp/skills non-empty constraints into base schema - Split aiBase into shared + aiOnly to clarify retry ownership - Restore valid: field in modelReasoningEffort/webSearchMode warning logs - Move imports to top of loader.ts - Fix stale isTriggerRule JSDoc, DagNodeSchema capitalization, and several comment inaccuracies - Add zod to @archon/workflows dependency list in CLAUDE.md * refactor(workflows): simplify loader.ts - Merge duplicate imports from ./types - Deduplicate AI-field warning blocks for bash/loop nodes - Use modelReasoningEffortSchema/webSearchModeSchema directly instead of workflowBaseSchema.shape accessor - Consistent path formatting in formatNodeIssue * revert: undo unrelated auth-service formatting changes
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
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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,
docsDir,
issueContext,
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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;
if (msg.cost !== undefined) {
loopTotalCostUsd = (loopTotalCostUsd ?? 0) + msg.cost;
}
if (msg.stopReason !== undefined) loopFinalStopReason = msg.stopReason;
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) {
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);
}
// 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);
});
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;
// 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,
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,
});
// 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,
data: {
duration_ms: Date.now() - iterationStart,
node_output: lastIterationOutput,
...(loopTotalCostUsd !== undefined ? { cost_usd: loopTotalCostUsd } : {}),
...(loopFinalStopReason ? { stop_reason: loopFinalStopReason } : {}),
...(loopTotalNumTurns !== undefined ? { num_turns: loopTotalNumTurns } : {}),
},
})
.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,
...(loopTotalCostUsd !== undefined ? { costUsd: loopTotalCostUsd } : {}),
...(loopFinalStopReason ? { stopReason: loopFinalStopReason } : {}),
...(loopTotalNumTurns !== undefined ? { numTurns: loopTotalNumTurns } : {}),
});
return {
state: 'completed',
output: lastIterationOutput,
sessionId: currentSessionId,
costUsd: loopTotalCostUsd,
};
}
// 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.
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}\``;
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 16:17:56 +00:00
const gateSent = await safeSendMessage(platform, conversationId, gateMsg, {
workflowId: workflowRun.id,
nodeName: node.id,
});
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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`,
};
}
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,
});
fix: address PR #938 review findings — interactive loop correctness and coverage Fixed: - H1: Return 'completed' (not 'failed') from interactive loop gate to prevent false "Some DAG nodes failed" warnings in multi-node workflows - H2: Check safeSendMessage return value before pausing — fail the node with a clear error if the gate message failed to deliver, preventing orphaned paused runs - H3: Extend isApprovalTransition guard in updateWorkflowRun to cover loop_user_input metadata key, preventing completed_at from being stamped on resumable loop runs - M1: Add isApprovalContext() type guard in workflow-run.ts; replace unsafe casts in dag-executor.ts and command-handler.ts - M4/L3: Update comments to accurately reflect completed-return semantics and metadata merge requirement - L1: Pass '' instead of undefined for $LOOP_USER_INPUT on iterations after first - L4: Update $LOOP_USER_INPUT docstring to clarify first-iteration-only scoping - Gap6: Add archon-piv-loop to bundled-defaults.ts so it's available in binary builds Tests added: - H4: /workflow approve interactive_loop branch tests in command-handler.test.ts (routing, approval_received event, no node_completed, error cases) - M2: superRefine validation tests in loader.test.ts (reject interactive without gate_message; accept valid interactive loop) - M3: loader warning test for interactive loop in non-interactive workflow Docs updated: - H5: docs/loop-nodes.md — add interactive/gate_message fields, $LOOP_USER_INPUT variable, and interactive loop pattern section - M5: docs/authoring-workflows.md — note interactive loops require workflow-level interactive: true - L5: README.md — add archon-piv-loop row, update count 16→17 - L6: docs/authoring-workflows.md — update count 16→17 - L7: CLAUDE.md — add $LOOP_USER_INPUT to variable substitution table Bundled defaults test updated: count 10→11 to reflect archon-piv-loop addition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
return { state: 'completed', output: lastIterationOutput, 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
}
// 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);
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
}
/**
* 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,
workflowModel: string | undefined,
cwd: string,
artifactsDir: string,
logDir: string,
baseBranch: string,
docsDir: string,
nodeOutputs: Map<string, NodeOutput>,
config: WorkflowConfig,
workflowLevelOptions: WorkflowLevelOptions,
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
if (rejectionReason !== '' && node.approval.on_reject) {
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);
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`,
});
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,
docsDir,
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,
cwd,
workflowLevelOptions
);
const output = await executeNodeInternal(
deps,
platform,
conversationId,
cwd,
workflowRun,
syntheticNode,
provider,
nodeOptions,
artifactsDir,
logDir,
baseBranch,
docsDir,
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,
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,
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>
fix(workflows): restore post-workflow summary in parent conversation (#907) * fix(workflows): restore post-workflow summary in parent conversation (#900) After PR #805 removed sequential execution and made DAG the sole format, the terminal node output was never returned from executeDagWorkflow (void return), so executor.ts always returned {success:true} with no summary field, and orchestrator.ts silently skipped the summary send. Changes: - dag-executor.ts: Change executeDagWorkflow return type void → string|undefined - dag-executor.ts: Compute terminal nodes (no dependents) and return first completed non-empty output after emitter.unregisterRun - executor.ts: Capture dagSummary from executeDagWorkflow and pass as summary field in the completed WorkflowExecutionResult Fixes #900 * test(isolation): update syncWorkspace spy assertions for resetAfterFetch arg Five WorktreeProvider tests were failing because syncWorkspace gained a third `{ resetAfterFetch: boolean }` argument; test assertions expected only two arguments. Updated assertions to include `{ resetAfterFetch: false }` (test paths are not managed clones under ~/.archon/workspaces). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address review findings from PR #907 - Add doc comment on terminal node selection explaining first-match semantics for multi-terminal DAGs (code-review LOW finding) - Fix mock type signature for mockExecuteDagWorkflow from Promise<void> to Promise<string | undefined> to match updated return type (test-coverage LOW) - Add result.summary wiring tests in executor.test.ts: verifies that when executeDagWorkflow returns a string it flows through to WorkflowExecutionResult.summary, and that undefined is passed through correctly (test-coverage MEDIUM) - Add terminal node selection return-value tests in dag-executor.test.ts: linear DAG returns terminal output, empty terminal returns undefined, fan-in DAG returns only the true terminal node (test-coverage MEDIUM + rating-5 gap) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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();
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,
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;
// 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.
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(
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) {
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'\`.`;
Fix: DAG when: parse errors now fail-closed (skip node) instead of fail-open (#590) * Fix: DAG when: parse errors now fail-closed (skip node) instead of fail-open (#563) A typo in a when: expression (e.g. using = instead of ==) caused the condition evaluator to return { result: true, parsed: false }, silently bypassing the gate and running the node unconditionally. Changes: - condition-evaluator.ts: return result: false on parse failure (fail-closed); upgrade log from warn to error; update doc comments - dag-executor.ts: add early-return skip on !conditionParsed with distinct reason 'when_condition_parse_error', emitting the same observability events (workflow event, emitter, logfile) as the regular condition-skip path; update platform message to say "node skipped" instead of "node ran" - event-emitter.ts: add 'when_condition_parse_error' to NodeSkippedEvent reason union - condition-evaluator.test.ts: update assertion to expect result: false on invalid expression Fixes #563 * fix: address review findings for DAG when: parse-error fix - Downgrade condition-evaluator.ts parse-failure logs from error to debug to avoid double ERROR logging (dag-executor already emits the ERROR with full node context) - Add executor-level integration tests for the !conditionParsed skip path: - verifies node is skipped (not run) when when: expression is unparseable - verifies platform warning message names the node and says "skipped" - verifies workflow completes without throwing when all nodes parse-error skip
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');
});
Fix: DAG when: parse errors now fail-closed (skip node) instead of fail-open (#590) * Fix: DAG when: parse errors now fail-closed (skip node) instead of fail-open (#563) A typo in a when: expression (e.g. using = instead of ==) caused the condition evaluator to return { result: true, parsed: false }, silently bypassing the gate and running the node unconditionally. Changes: - condition-evaluator.ts: return result: false on parse failure (fail-closed); upgrade log from warn to error; update doc comments - dag-executor.ts: add early-return skip on !conditionParsed with distinct reason 'when_condition_parse_error', emitting the same observability events (workflow event, emitter, logfile) as the regular condition-skip path; update platform message to say "node skipped" instead of "node ran" - event-emitter.ts: add 'when_condition_parse_error' to NodeSkippedEvent reason union - condition-evaluator.test.ts: update assertion to expect result: false on invalid expression Fixes #563 * fix: address review findings for DAG when: parse-error fix - Downgrade condition-evaluator.ts parse-failure logs from error to debug to avoid double ERROR logging (dag-executor already emits the ERROR with full node context) - Add executor-level integration tests for the !conditionParsed skip path: - verifies node is skipped (not run) when when: expression is unparseable - verifies platform warning message names the node and says "skipped" - verifies workflow completes without throwing when all nodes parse-error skip
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,
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,
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)) {
// 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 =
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 =
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));
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,
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,
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 };
}
// 3c. Approval node dispatch — pauses workflow for human review
if (isApprovalNode(node)) {
const output = await executeApprovalNode(
node,
workflowRun,
deps,
platform,
conversationId,
workflowProvider,
workflowModel,
cwd,
artifactsDir,
logDir,
baseBranch,
docsDir,
nodeOutputs,
config,
workflowLevelOptions,
configuredCommandFolder,
issueContext
);
return { nodeId: node.id, output };
}
// 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 } };
}
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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,
issueContext,
config.envVars
feat: script node type for DAG workflows (bun/uv runtimes) (#999) * feat: add ScriptNode schema and type guards (US-001) Implements US-001 from the script-nodes PRD. Changes: - Add scriptNodeSchema with script, runtime (bun|uv), deps, and timeout fields - Add ScriptNode type with never fields for mutual exclusivity - Add isScriptNode type guard - Add SCRIPT_NODE_AI_FIELDS constant (same as BASH_NODE_AI_FIELDS) - Update dagNodeSchema superRefine and transform to handle script: nodes - Update DagNode union type to include ScriptNode - Add script node dispatch stub in dag-executor.ts (fails fast until US-003) - Export all new types and values from schemas/index.ts - Add comprehensive schema tests for ScriptNode parsing and validation * feat: script discovery from .archon/scripts/ Implements US-002 from PRD. Changes: - Add ScriptDefinition type and discoverScripts() in script-discovery.ts - Auto-detect runtime from file extension (.ts/.js->bun, .py->uv) - Handle duplicate script name conflicts across extensions - Add bundled defaults infrastructure (empty) for scripts - Add tests for discovery, naming, and runtime detection * feat: script execution engine (inline + named) Implements US-003 from PRD. Changes: - Add executeScriptNode() in dag-executor.ts following executeBashNode pattern - Support inline bun (-e) and uv (run python -c) execution - Support named scripts via bun run / uv run - Wire ScriptNode dispatch replacing 'not yet implemented' stub - Capture stdout as node output, stderr as warning - Handle timeout and non-zero exit - Pass env vars for variable substitution - Add tests for inline/named/timeout/failure cases * feat: runtime availability validation at load time Implements US-004 from PRD. Changes: - Add checkRuntimeAvailable() utility for bun/uv binary detection - Extend validator.ts with script file and runtime validation - Integrate script validation into parseWorkflow flow in loader.ts - Add tests for runtime availability detection * feat: dependency installation for script nodes Implements US-005 from PRD. Changes: - Support deps field for uv nodes: uvx --with dep1... for inline - Support uv run --with dep1... for named uv scripts - Bun deps are auto-installed at runtime via bun's native mechanism - Empty/omitted deps field produces no extra flags - Add tests for dep injection into both runtimes * test: integration tests and validation for script nodes Implements US-006 from PRD. Changes: - Fill test coverage gaps for script node feature - Add script + command mutual exclusivity schema test - Add env var substitution tests ($WORKFLOW_ID, $ARTIFACTS_DIR in scripts) - Add stderr handling test (stderr sent to user as platform message) - Add missing named script file validation tests to validator.test.ts - Full bun run validate passes * fix: address review findings in script nodes - Extract isInlineScript to executor-shared.ts (was duplicated in dag-executor.ts and validator.ts) - Remove dead warnMissingScriptRuntimes from loader.ts (validator already covers runtime checks) - Remove path traversal fallback in executeScriptNode — error when named script not found instead of executing arbitrary file paths - Memoize checkRuntimeAvailable to avoid repeated subprocess spawns - Add min(1) to scriptNodeSchema.script field for consistency - Replace dynamic import with static import in validator.ts * fix(workflows): address review findings for script node implementation Critical fixes: - Wrap discoverScripts() in try-catch inside executeScriptNode to prevent unhandled rejections when script discovery fails (e.g. duplicate names) - Add isScriptNode to isNonAiNode check in loader.ts so AI-specific fields on script nodes emit warnings (activates SCRIPT_NODE_AI_FIELDS) Important fixes: - Surface script stderr in user-facing error messages on non-zero exit - Replace uvx with uv run --with for inline uv scripts with deps - Add z.string().min(1) validation on deps array items - Remove unused ScriptDefinition.content field and readFile I/O - Add logging in discoverAvailableScripts catch block - Warn when deps is specified with bun runtime (silently ignored) Simplifications: - Merge BASH_DEFAULT_TIMEOUT and SCRIPT_DEFAULT_TIMEOUT into single SUBPROCESS_DEFAULT_TIMEOUT constant - Use scriptDef.runtime instead of re-deriving from extname() - Extract shared formatValidationResult helper, deduplicate section comments Tests: - Add isInlineScript unit tests to executor-shared.test.ts - Add named-script-not-found executor test to dag-executor.test.ts - Update deps tests to expect uv instead of uvx Docs: - Add script: node type to CLAUDE.md node types and directory structure - Add script: to .claude/rules/workflows.md DAG Node Types section
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,
cwd,
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
feat: safe session continuity across workflow steps with forkSession-based retries (#729) * feat: safe session continuity across workflow steps with forkSession-based retries (#691) Retries previously discarded all prior conversation context by passing resumeSessionId=undefined, because naive session resume would append to the original session file and corrupt it on a crash. This adds forkSession:true support so the SDK copies the prior session history into a new file before appending — leaving the source untouched — making it safe to always pass the prior session ID on retry. Changes: - Add forkSession?: boolean to WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts) - Pass forkSession to SDK Options in ClaudeClient with enhanced resuming_session log - Sequential executor: set shouldForkSession=true when resuming; inject into stepOptions; fix retry to always pass resumeSessionId - DAG executor: set shouldForkSession=true when resuming in executeNodeInternal; fix retry to always pass resumeSessionId; update isFresh comment - Fix resolvedOptions to explicitly set persistSession:false for Claude workflows (was silently defaulting to SDK's true) - Add context:'shared' union to DagNodeBase.context type; update clearContext docstring Fixes #691 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove persistSession:false from workflow resolvedOptions to preserve cross-step session continuity SDK docs confirm that persistSession:false prevents session files from being written to disk, making resume impossible for subsequent steps. Removing this option restores the SDK default (true), so step 1's session ID can be used by step 2 to resume. Also clarify context:'shared' docstring to note it has no effect on parallel layer nodes, and add unit tests asserting that retries pass forkSession:true when a prior session exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add persistSession: false to Claude resolvedOptions Implements the missing persistSession fix claimed in the PR description. Without this, the SDK defaults to true and writes session transcripts to disk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(loader): preserve context: 'shared' in DAG node YAML parsing The PR added 'shared' to DagNodeBase.context type but the loader only preserved 'fresh', silently dropping 'shared' during YAML parsing. This meant context: shared in workflow YAML was ignored at load time. 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 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;
// 6. Execute with retry for transient failures
const retryConfig = getEffectiveNodeRetryConfig(node);
let output: NodeExecutionResult = {
state: 'failed',
output: '',
error: 'Node did not execute',
};
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
output = await executeNodeInternal(
deps,
platform,
conversationId,
cwd,
workflowRun,
node,
provider,
nodeOptions,
artifactsDir,
logDir,
baseBranch,
docsDir,
nodeOutputs,
feat: safe session continuity across workflow steps with forkSession-based retries (#729) * feat: safe session continuity across workflow steps with forkSession-based retries (#691) Retries previously discarded all prior conversation context by passing resumeSessionId=undefined, because naive session resume would append to the original session file and corrupt it on a crash. This adds forkSession:true support so the SDK copies the prior session history into a new file before appending — leaving the source untouched — making it safe to always pass the prior session ID on retry. Changes: - Add forkSession?: boolean to WorkflowAssistantOptions (deps.ts) and AssistantRequestOptions (types/index.ts) - Pass forkSession to SDK Options in ClaudeClient with enhanced resuming_session log - Sequential executor: set shouldForkSession=true when resuming; inject into stepOptions; fix retry to always pass resumeSessionId - DAG executor: set shouldForkSession=true when resuming in executeNodeInternal; fix retry to always pass resumeSessionId; update isFresh comment - Fix resolvedOptions to explicitly set persistSession:false for Claude workflows (was silently defaulting to SDK's true) - Add context:'shared' union to DagNodeBase.context type; update clearContext docstring Fixes #691 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove persistSession:false from workflow resolvedOptions to preserve cross-step session continuity SDK docs confirm that persistSession:false prevents session files from being written to disk, making resume impossible for subsequent steps. Removing this option restores the SDK default (true), so step 1's session ID can be used by step 2 to resume. Also clarify context:'shared' docstring to note it has no effect on parallel layer nodes, and add unit tests asserting that retries pass forkSession:true when a prior session exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add persistSession: false to Claude resolvedOptions Implements the missing persistSession fix claimed in the PR description. Without this, the SDK defaults to true and writes session transcripts to disk. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(loader): preserve context: 'shared' in DAG node YAML parsing The PR added 'shared' to DagNodeBase.context type but the loader only preserved 'fresh', silently dropping 'shared' during YAML parsing. This meant context: shared in workflow YAML was ignored at load time. 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 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,
configuredCommandFolder,
issueContext
);
if (output.state !== 'failed') break;
// 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;
const isTransient = output.error ? isTransientNodeError(output.error) : false;
const shouldRetry =
!isFatal &&
(retryConfig.onError === 'all' ||
(retryConfig.onError === 'transient' && isTransient));
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'
);
const errorKind = isTransient ? 'transient error' : 'error';
await safeSendMessage(
platform,
conversationId,
`⚠️ Node \`${node.id}\` failed with ${errorKind} (attempt ${String(attempt + 1)}/${String(retryConfig.maxRetries + 1)}). Retrying in ${String(Math.round(delayMs / 1000))}s...`,
{ 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;
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');
}
// Check for non-running status between DAG layers (cancellation, deletion, pause)
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';
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'
);
// 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 }
);
}
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'
);
}
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;
}
// 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.';
// 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) {
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('; ');
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 {
await deps.store.completeWorkflowRun(workflowRun.id, {
node_counts: nodeCounts,
// totalCostUsd starts at 0; only write metadata when at least one node reported cost
...(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);
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,
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',
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);
fix(workflows): restore post-workflow summary in parent conversation (#907) * fix(workflows): restore post-workflow summary in parent conversation (#900) After PR #805 removed sequential execution and made DAG the sole format, the terminal node output was never returned from executeDagWorkflow (void return), so executor.ts always returned {success:true} with no summary field, and orchestrator.ts silently skipped the summary send. Changes: - dag-executor.ts: Change executeDagWorkflow return type void → string|undefined - dag-executor.ts: Compute terminal nodes (no dependents) and return first completed non-empty output after emitter.unregisterRun - executor.ts: Capture dagSummary from executeDagWorkflow and pass as summary field in the completed WorkflowExecutionResult Fixes #900 * test(isolation): update syncWorkspace spy assertions for resetAfterFetch arg Five WorktreeProvider tests were failing because syncWorkspace gained a third `{ resetAfterFetch: boolean }` argument; test assertions expected only two arguments. Updated assertions to include `{ resetAfterFetch: false }` (test paths are not managed clones under ~/.archon/workspaces). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address review findings from PR #907 - Add doc comment on terminal node selection explaining first-match semantics for multi-terminal DAGs (code-review LOW finding) - Fix mock type signature for mockExecuteDagWorkflow from Promise<void> to Promise<string | undefined> to match updated return type (test-coverage LOW) - Add result.summary wiring tests in executor.test.ts: verifies that when executeDagWorkflow returns a string it flows through to WorkflowExecutionResult.summary, and that undefined is passed through correctly (test-coverage MEDIUM) - Add terminal node selection return-value tests in dag-executor.test.ts: linear DAG returns terminal output, empty terminal returns undefined, fan-in DAG returns only the true terminal node (test-coverage MEDIUM + rating-5 gap) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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
}