Skip to content

feat: workflow execution lifecycle + editor improvements#1058

Merged
Aureliolo merged 15 commits intomainfrom
feat/workflow-execution-lifecycle
Apr 4, 2026
Merged

feat: workflow execution lifecycle + editor improvements#1058
Aureliolo merged 15 commits intomainfrom
feat/workflow-execution-lifecycle

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

Implements workflow execution COMPLETED/FAILED status transitions (#1042) and workflow editor improvements (#1029) including list page, YAML import, conditionals with AND/OR operators, minimap, and copy/paste.

Backend -- Workflow Execution Lifecycle (#1042)

  • TaskEngine observer mechanism: register_observer() accepts async callbacks for TaskStateChanged events. WorkflowExecutionObserver bridges TaskEngine events to WorkflowExecutionService
  • COMPLETED transition: When all TASK nodes complete, execution transitions to COMPLETED with completed_at set
  • FAILED transition: When any task fails or is cancelled, execution transitions to FAILED with error message and completed_at
  • Atomic saves: Node status update + execution status transition combined into a single save (no double-save race condition)
  • New enum values: TASK_COMPLETED and TASK_FAILED in WorkflowNodeExecutionStatus
  • Event constants: WORKFLOW_EXEC_COMPLETED, WORKFLOW_EXEC_FAILED, WORKFLOW_EXEC_NODE_TASK_COMPLETED, WORKFLOW_EXEC_NODE_TASK_FAILED used in structured logging

Frontend -- Workflow Editor Improvements (#1029)

  • Workflow list page (/workflows): Card grid view with search, type filter, create/duplicate/delete actions
  • Bidirectional YAML editing: Toggle between visual and YAML modes, parse YAML back to nodes with inline error display, position preservation
  • Multi-workflow support: Workflow selector dropdown, quick-switch, "Save as New"
  • Structured condition builder: AND/OR logical operators with multi-row field/operator/value builder, free-text Advanced mode fallback
  • Minimap: @xyflow/react MiniMap with per-node-type color coding
  • Copy/paste: Ctrl+C/V for selected node groups, preserving internal edges

Code Quality

  • Extracted execution_activation_helpers.py (graph walking, conditional processing, task config parsing) from execution_service.py to stay under 800-line limit
  • Extracted task_engine_events.py (event building, snapshot publishing) from task_engine.py
  • Fixed 22 TypeScript errors (SegmentedControl label prop, ConfirmDialog API, SelectField onChange, ReactFlow reset changes, yaml-to-nodes guards)
  • Safe YAML parsing with yaml.CORE_SCHEMA
  • Click-outside dismiss + Escape key handling on workflow card dropdown
  • Timer cleanup on unmount in YAML editor

Test Plan

  • 13,490 Python tests pass (10 new: observer mechanism, lifecycle transitions, model validation)
  • 2,342 web dashboard tests pass
  • All linters clean (ruff, mypy, ESLint, TypeScript)

Review Coverage

Pre-reviewed by 16 specialized agents (docs-consistency, code-reviewer, python-reviewer, pr-test-analyzer, silent-failure-hunter, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, conventions-enforcer, security-reviewer, frontend-reviewer, design-token-audit, api-contract-drift, test-quality-reviewer, async-concurrency-reviewer, issue-resolution-verifier). 46 findings addressed.

Closes #1029
Closes #1042

Copilot AI review requested due to automatic review settings April 4, 2026 01:01
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a74922e6-0af9-4eb8-95a4-c21ba72d42b3

📥 Commits

Reviewing files that changed from the base of the PR and between 45d4dc0 and ae2b183.

📒 Files selected for processing (2)
  • docs/design/engine.md
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (6)
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Always read the relevant docs/design/ page before implementing any feature or planning any issue. The design spec is the starting point for architecture, data models, and behavior.

Files:

  • docs/design/engine.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones
Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: A PostToolUse hook (scripts/check_web_design_system.py) enforces design system rules on every Edit/Write to web/src/
Use property-based testing with fast-check (fc.assert + fc.property) for React component tests

Files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,js,jsx}: Always use createLogger from @/lib/logger -- never bare console.warn/console.error/console.debug in application code
Variable name for logger: always log (e.g. const log = createLogger('module-name'))
Use log.debug() (DEV-only, stripped in production), log.warn(), log.error() logging levels
Pass dynamic/untrusted values as separate args to logger (not interpolated into the message string) so they go through sanitizeArg
Attacker-controlled fields inside structured objects must be wrapped in sanitizeForLog() before embedding in log messages

Files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/pages/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones -- do not recreate status dots inline, use <StatusBadge>
Do NOT build card-with-header layouts from scratch -- use <SectionCard>
Do NOT create metric displays with text-metric font-bold -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>

Files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Colors: use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in .tsx/.ts files
Typography: use font-sans or font-mono (maps to Geist tokens). NEVER set fontFamily directly
Spacing: use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Shadows/Borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright)
Do NOT use rgba() with hardcoded values -- use design token variables
Hardcoded hex colors and rgba values in web/src/ files are automatically caught by a PostToolUse hook and must be fixed
Hardcoded font-family declarations in web/src/ files are automatically caught by a PostToolUse hook and must be fixed
Hardcoded Framer Motion transition durations (should use @/lib/motion presets) are automatically caught by a PostToolUse hook and must be fixed
Duplicate patterns that should use existing shared components in web/src/ are automatically caught by a PostToolUse hook and must be fixed
Complex .map() blocks that should be extracted to components in web/src/ are automatically caught by a PostToolUse hook and must be fixed
CSS side-effect imports need type declarations (Vite's /// <reference types="vite/client" /> covers this) as noUncheckedSideEffectImports defaults to true in TypeScript 6

Files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...

Applied to files:

  • docs/design/engine.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)

Applied to files:

  • docs/design/engine.md
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • docs/design/engine.md
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/engine/coordination/**/*.py : Task coordination uses multi-agent pipeline with 4 dispatchers (SAS/centralized/decentralized/context-dependent), wave execution, and workspace lifecycle integration.

Applied to files:

  • docs/design/engine.md
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
🔇 Additional comments (3)
web/src/pages/workflows/WorkflowCreateDrawer.tsx (1)

1-123: LGTM!

The component is well-structured and follows all coding guidelines:

  • UI components properly reused from @/components/ui/
  • Design tokens correctly applied (no hardcoded colors or spacing)
  • Form validation and error handling are clean
  • State properly reset on both successful submission and drawer close
  • Accessibility addressed with role="alert" on the error message
  • formatLabel utility used for consistent label formatting

Previous review feedback has been addressed (error state reset, accessible error announcement, shared utility usage).

docs/design/engine.md (2)

262-263: LGTM: Status enum documentation matches implementation.

The new TASK_COMPLETED and TASK_FAILED values accurately reflect the split between control-node completion (COMPLETED) and terminal task outcomes, consistent with the enum definition in src/synthorg/core/enums.py.


417-419: The review comment is incorrect. The syntax except MemoryError, RecursionError: is the correct PEP 758 exception syntax for Python 3.14 (which is the project's target version per requires-python = ">=3.14" and target-version = "py314"). This syntax is enforced by ruff and will not cause a SyntaxError. The implementation at line 752 in _notify_observers correctly implements the exception handling behavior documented in the design spec.

			> Likely an incorrect or invalid review comment.

Walkthrough

Adds an in-process observer API to TaskEngine (register_observer) and extracts task-state event construction/publishing into a new task_engine_events module. Introduces TaskStateChanged publication helpers and a WorkflowExecutionObserver that forwards task state-change events to WorkflowExecutionService. Extends per-node statuses with TASK_COMPLETED and TASK_FAILED, implements complete_execution, fail_execution, and handle_task_state_changed to update node statuses and transition executions, and factors activation-time graph walking, conditional processing, and task config parsing into execution_activation_helpers. Updates validation, observability event constants, unit tests, and multiple frontend workflow-list/editor features.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: workflow execution lifecycle + editor improvements' accurately summarizes the main changes across both backend (execution lifecycle transitions) and frontend (editor enhancements).
Description check ✅ Passed The description is comprehensive and relates directly to the changeset, detailing backend execution lifecycle implementation and frontend editor improvements with concrete feature descriptions and test coverage.
Linked Issues check ✅ Passed The changeset addresses all primary coding requirements from linked issues #1029 (workflow list, YAML editing, conditionals, minimap, copy/paste) and #1042 (execution lifecycle, COMPLETED/FAILED transitions, event-driven observer mechanism).
Out of Scope Changes check ✅ Passed All code changes align with stated objectives: backend observer mechanism, execution transitions, new enum values, event constants, plus frontend list page, YAML editor, conditionals builder, minimap, and copy/paste. Documentation updates (CLAUDE.md, design docs) and test additions support the implementation scope.
Docstring Coverage ✅ Passed Docstring coverage is 44.37% which is sufficient. The required threshold is 40.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 4, 2026 01:02 — with GitHub Actions Inactive
gemini-code-assist[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA ae2b183.
Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.

Scanned Files

None

This comment was marked as resolved.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 86.71587% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.74%. Comparing base (4a8adfe) to head (ae2b183).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...rg/engine/workflow/execution_activation_helpers.py 71.29% 21 Missing and 10 partials ⚠️
src/synthorg/engine/task_engine_events.py 90.32% 2 Missing and 1 partial ⚠️
src/synthorg/engine/task_engine.py 88.88% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1058      +/-   ##
==========================================
+ Coverage   90.72%   90.74%   +0.02%     
==========================================
  Files         707      710       +3     
  Lines       40254    40396     +142     
  Branches     4014     4035      +21     
==========================================
+ Hits        36522    36659     +137     
- Misses       3033     3038       +5     
  Partials      699      699              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/engine/task_engine.py`:
- Around line 703-714: The handler is awaiting observer delivery inline in
_process_one() via _notify_observers(event), which stalls the single-writer hot
path; instead create or use a dedicated background dispatcher/queue (e.g., an
_observer_dispatcher with an enqueue() method or an asyncio task runner) and
push the built event to it without awaiting delivery here — leave
publish_snapshot awaited when _config.publish_snapshots is true but replace
direct calls to await self._notify_observers(event) (and the similar occurrence
around the other build_state_changed_event usage) with non-blocking
enqueue/dispatch to the background dispatcher so observer delivery happens
asynchronously and best-effort.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py`:
- Around line 256-284: The code currently coerces unknown config values into
defaults (using _TASK_TYPE_MAP/_PRIORITY_MAP/_COMPLEXITY_MAP) which masks
invalid workflow definitions; change this to validate and reject invalid values
instead: for each of raw_type/raw_priority/raw_complexity, check membership in
the respective map and if missing call logger.error(WORKFLOW_EXEC_TASK_CREATED,
execution_id=execution_id, node_id=nid, note=...) and raise an exception (e.g.,
ValueError or the service's workflow validation exception) instead of falling
back to TaskType.DEVELOPMENT/Priority.MEDIUM/Complexity.MEDIUM so
activation/validation stops and the caller can surface the error. Ensure
references to _TASK_TYPE_MAP, _PRIORITY_MAP, _COMPLEXITY_MAP, TaskType,
Priority, Complexity, WORKFLOW_EXEC_TASK_CREATED, execution_id, nid and config
are used to locate the exact checks to change.
- Around line 185-196: Replace the parenthesized multi-except with the
repo-standard PEP 758 form: in the try/except that calls
evaluate_condition(expr, ctx) (the block that currently catches (ValueError,
TypeError, KeyError) as exc), change the except clause to use comma-separated
exception types (except ValueError, TypeError, KeyError as exc:) and keep the
existing logging (logger.exception(...)) and raise
WorkflowConditionEvalError(msg) from exc unchanged; this touches the
evaluate_condition call and the WorkflowConditionEvalError raise in the same
block.

In `@src/synthorg/engine/workflow/execution_observer.py`:
- Around line 1-58: This module lacks the required module-level logger; add
"from synthorg.observability import get_logger" and create logger =
get_logger(__name__) at top-level, then use that logger in
WorkflowExecutionObserver (e.g., log entry/exit or important info in __init__
and async __call__ when delegating to
WorkflowExecutionService.handle_task_state_changed) to satisfy the guideline
that every business-logic module must set up a logger for observability.

In `@src/synthorg/engine/workflow/execution_service.py`:
- Around line 695-711: The _find_execution_by_task function currently scans all
RUNNING executions via _execution_repo.list_by_status and nested
node_executions, causing O(running×nodes) work; replace this with a
repository-level lookup keyed by task_id (e.g., add and call a new method like
_execution_repo.get_by_task_id or _execution_repo.find_execution_for_task) so
the TaskEngine observer uses a constant-time indexed lookup; update the
WorkflowExecutionRepository interface/implementation to maintain the task_id ->
execution_id index (or query) and switch _find_execution_by_task to call that
new repository method instead of iterating.
- Around line 540-544: The WORKFLOW_EXEC_FAILED log is emitted after the state
is already persisted and should be an informational state transition, not a
warning; update the two logging calls that currently use logger.warning (the
call that passes WORKFLOW_EXEC_FAILED with execution_id and error around the
block using execution_id, and the analogous call around lines 612-616) to use
logger.info instead, keeping the same message key and structured fields
(execution_id, error) so the transition remains logged at INFO level.

In `@tests/unit/engine/workflow/test_execution_lifecycle.py`:
- Around line 457-493: Parametrize the two tests that only exercise
TaskStatus.FAILED to also assert behavior for TaskStatus.CANCELLED by converting
them to use `@pytest.mark.parametrize` over statuses [TaskStatus.FAILED,
TaskStatus.CANCELLED]; call service.handle_task_state_changed with
_make_task_event(task_id, status) and assert the same terminal outcomes
(stored.status is WorkflowExecutionStatus.FAILED in
test_task_failed_transitions_to_failed and nmap["task-a"].status is
WorkflowNodeExecutionStatus.TASK_FAILED in
test_task_failed_updates_node_to_task_failed) so the cancel branch is covered;
keep references to _activate_simple/_activate_parallel, _get_task_ids,
_make_task_event, WorkflowExecutionStatus.FAILED, and
WorkflowNodeExecutionStatus.TASK_FAILED when locating changes.

In `@tests/unit/engine/workflow/test_execution_models.py`:
- Around line 278-331: Replace the four nearly-identical tests in
TestTaskLinkedStatusOnNonTaskNodes with a single parametrized test using
pytest.mark.parametrize: create one test (e.g.,
test_task_linked_status_on_non_task_node_rejected) that takes node_type and
status parameters and asserts WorkflowNodeExecution raises ValidationError for
those combinations; reference the existing symbols WorkflowNodeExecution,
WorkflowNodeType, WorkflowNodeExecutionStatus, and ValidationError and supply
the tuples [(WorkflowNodeType.START,
WorkflowNodeExecutionStatus.TASK_COMPLETED), (WorkflowNodeType.START,
WorkflowNodeExecutionStatus.TASK_FAILED), (WorkflowNodeType.END,
WorkflowNodeExecutionStatus.TASK_COMPLETED), (WorkflowNodeType.CONDITIONAL,
WorkflowNodeExecutionStatus.TASK_CREATED)] so the behavior stays identical but
avoids duplication.

In `@web/src/hooks/useWorkflowsData.ts`:
- Around line 33-34: Remove the dead pollingRef by deleting the declaration
"const pollingRef = useRef(polling)" and the subsequent assignment
"pollingRef.current = polling" in useWorkflowsData; also remove any now-unused
import of useRef if it becomes unused after this change so there are no
lingering unused variables or imports.

In `@web/src/pages/workflow-editor/condition-expression-types.ts`:
- Around line 59-68: The serializer currently maps expr.conditions and joins
parts without handling an empty ConditionGroup, causing an empty string output;
update the code that builds parts (the block calling serializeCondition(c) and
joining with expr.logicalOperator) to guard against empty groups: when expr.kind
=== 'group' and expr.conditions.length === 0 either return a safe default (e.g.,
an explicit empty-group token or empty string) or throw a validation error so
downstream consumers don't receive malformed output—apply this check in the
function that handles ConditionGroup/serializeCondition so empty expr.conditions
are handled before mapping and joining.
- Around line 102-119: The loop that splits `str` by `token` uses a `depth`
counter but doesn't guard against unbalanced parentheses; modify the logic in
the function that contains this loop to detect when `depth` goes negative during
iteration (e.g., when encountering a ')' with `depth === 0`) and immediately
return null (or otherwise signal an error), and after the loop check that `depth
=== 0` before returning `parts`—if not, return null; use the existing
identifiers (`str`, `token`, `depth`, `i`, `current`, `parts`) to implement
these checks so callers can handle malformed input.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 211-217: The builder only syncs builder→parent; add logic to
detect external changes to the incoming value and reset builder state
accordingly: create a lastSyncedRef to track the last value we pushed, add an
effect that runs on value changes which computes serialized =
serializeCondition(buildExpression(comparisons, logicalOperator)) and returns
early if value === serialized or value === lastSyncedRef.current, otherwise
parse the new value with parseForBuilder and, if parse succeeds call
setEntries(toEntries(...)), setLogicalOperator(...), setMode('builder'), else
setFreeText(value) and setMode('advanced'); finally update lastSyncedRef.current
= value. Use these symbols to locate code: buildExpression, serializeCondition,
parseForBuilder, setEntries, setLogicalOperator, setMode, setFreeText, and
lastSyncedRef.
- Around line 286-299: The SegmentedControl placed between rows currently
mutates the shared logicalOperator state (logicalOperator, setLogicalOperator)
which can confuse users into thinking each row has its own operator; update the
UI to clarify this by either (a) showing the SegmentedControl only once above
the condition rows with a helper label/tooltip that states "Applies to all rows"
or (b) keep the per-row control but add a persistent tooltip/info icon (adjacent
to SegmentedControl) that explains "This operator is global across all
conditions in this builder" and make the per-row controls visually muted (or
disabled) to indicate they are not independent; adjust the JSX in
ConditionExpressionBuilder around the SegmentedControl usage and ensure the
tooltip text references the flat-builder behavior so users understand the global
scope of logicalOperator.
- Around line 252-267: The handler handleModeChange currently switches to
'builder' even if parseForBuilder(freeText) returns null, leaving stale entries;
change it so when newMode === 'builder' you call parseForBuilder(freeText) and
if it returns null you do NOT call setMode('builder') but instead set a
validation flag (e.g., setParseError / setValidationError) to surface the
failure to the user; only when parseForBuilder returns a valid flat object
should you call setEntries(toEntries(flat.comparisons)),
setLogicalOperator(flat.logicalOperator) and setMode('builder'); keep the
existing advanced->freeText path (buildExpression/serializeCondition) as-is.
- Around line 196-207: Compute parseForBuilder(value) once during initialization
and reuse the result for all initial state values: call parseForBuilder(value)
once (e.g., const parsed = parseForBuilder(value)) and then use
parsed?.comparisons (fallback to [createComparison()]) for entries via
toEntries, parsed?.logicalOperator ?? 'AND' for logicalOperator, and parsed for
getInitialMode/freeText; update the entries/setEntries,
logicalOperator/setLogicalOperator, and freeText initializers to reference that
single parsed value and remove the extra parseForBuilder calls.
- Around line 102-157: Replace the native <select> for the operator in
ConditionExpressionBuilder with the project's SelectField component: use
options={OPERATORS} (OPERATORS is already in {value,label} shape), bind value to
comparison.operator and call onUpdate(index, { ...comparison, operator: newValue
}) from SelectField's onChange, preserve the existing className/aria-label and
width (w-24) so layout doesn't shift, and remove the inner <option> mapping and
related fragments.

In `@web/src/pages/workflow-editor/WorkflowSelector.tsx`:
- Around line 20-46: The WorkflowSelector component is using raw <select>
elements instead of the shared UI primitives; refactor the JSX in
WorkflowSelector to use the dashboard's shared form/select components from
web/src/components/ui/ (e.g., the Select, SelectTrigger, SelectValue,
SelectContent, SelectItem primitives) while preserving behavior tied to
props/current state (currentId, onChange, workflows) and the loading state
branch—map workflows to SelectItem using w.id and w.name and ensure the onChange
handler calls onChange(value) only when a non-empty value is chosen.

In `@web/src/pages/workflow-editor/WorkflowYamlEditor.tsx`:
- Around line 63-71: When replacing the entire graph in
useWorkflowEditorStore.setState (the block that sets nodes to mappedNodes, edges
to result.edges, dirty/yamlPreview/undoStack/redoStack), also clear the stale
selection by resetting selectedNodeId (and optionally selectedEdgeId) to
null/undefined so the inspector won't point at a removed/renamed node; update
the setState call to include selectedNodeId: null (and selectedEdgeId: null if
present).
- Around line 21-31: yamlText is only set once from initialYaml so the editor
doesn't update when initialYaml changes; add a useEffect that watches
initialYaml and calls setYamlText(initialYaml) (and optionally reset
setParseErrors([]), setParseWarnings([]), and setApplied(false)) so the local
editor state resyncs whenever the parent-provided initialYaml changes; reference
the existing state hooks yamlText, setYamlText, parseErrors, parseWarnings,
applied, and appliedTimerRef and implement the effect using initialYaml as the
dependency.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 146-169: The YAML import currently collapses branch and
conditional semantics because inferEdgeType() is always resulting in
'sequential' and branch edges are hard-coded to type 'sequential'; update the
import to preserve/derive the correct edge type by (1) fixing inferEdgeType() to
inspect the step metadata (e.g., step.condition, step.type, or any saved
edgeType) and return 'conditional' or 'sequential' accordingly, (2) when
building edges from step.depends_on push edges with type set to the returned
value (not forced to 'sequential') and keep data.edgeType in sync, and (3) when
building edges from step.branches emit a dedicated parallel edge type (e.g.,
'parallel' or 'parallel_branch') and set data.edgeType to 'parallel_branch' (and
apply the same change for the other similar block around lines 238-244) so
round-tripping preserves true/false and parallel-split semantics; refer to
inferEdgeType(), the edges array construction for step.depends_on, and the
branch-edge construction for step.branches.
- Around line 103-105: The code currently sets firstStepId and emits edges while
iterating YAML order, which can create edges to skipped/invalid steps; instead,
first pass: collect and validate all steps into stepIds and a map of
validStepIds (use existing firstStepId only after validation), create nodes for
them; second pass: iterate steps' depends_on and branches to emit edges only
when both source and target exist in the validated map, and finally connect
"start-1" to the computed root steps (those without incoming validated edges)
rather than using the original file order; update logic around firstStepId,
stepIds, and edge emission to follow this two-pass approach and reference
depends_on, branches, and "start-1" when implementing checks.

In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 336-340: The WorkflowYamlEditor initializes its internal yamlText
from the initialYaml prop but doesn't update when initialYaml (e.g.,
yamlPreview) changes while the editor is mounted, causing stale content on
undo/redo; add a useEffect in WorkflowYamlEditor that watches initialYaml and
calls setYamlText(initialYaml) (useEffect(() => { setYamlText(initialYaml) },
[initialYaml])) to auto-sync, or alternatively document in the component
props/readme that yamlText intentionally preserves unsaved edits and provide a
Revert control for manual reset.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 48-131: The card shell in WorkflowCard is hand-rolled (the outer
div with rounded-lg border bg-card p-card etc.)—replace that with the shared
SectionCard component to avoid duplicating card-with-header layout; wrap the
existing inner content (title, labels, description, stats, version/updated) as
SectionCard children and move the header elements (workflow.name +
workflow.workflow_type badge) into SectionCard's header slot or props if it has
one, retain the Link to editorUrl and keep the menu toggle logic (menuRef,
menuOpen, setMenuOpen, onDuplicate, setConfirmDelete) but adjust
positioning/classes so the menu button remains absolutely positioned (e.g.,
ensure SectionCard is relatively positioned or pass a className to SectionCard).

In `@web/src/pages/workflows/WorkflowCreateDrawer.tsx`:
- Around line 61-63: After a successful create (where you call
useToastStore.getState().add and setForm(INITIAL_FORM)), also clear any
validation state before closing the drawer: reset the errors and submitError
state via their setters (e.g., call setErrors(...) and setSubmitError(...) — or
the equivalent state updater used in this component) immediately before
setForm(INITIAL_FORM) or at least before calling onClose(), then call onClose()
so stale validation messages won't appear if the drawer is reopened.

In `@web/src/pages/workflows/WorkflowFilters.tsx`:
- Around line 19-35: The JSX in WorkflowFilters.tsx uses raw <input> and
<select> elements for searchQuery and workflowTypeFilter (handlers
setSearchQuery, setWorkflowTypeFilter) which duplicates styling/behavior;
replace them with the shared UI components from web/src/components/ui (e.g., the
project's Input/TextInput and Select components) and wire their value and
onChange props to searchQuery/setSearchQuery and
workflowTypeFilter/setWorkflowTypeFilter (mapping empty string to null as
before) so styling, accessibility and behavior are centralized.

In `@web/src/stores/workflows.ts`:
- Around line 74-79: The deleteWorkflow handler is unconditionally decrementing
totalWorkflows even when no workflow matched the id; update deleteWorkflow to
call deleteWorkflowApi(id) as before but compute the filtered list and compare
lengths (or check whether any item was removed) before updating state: set
workflows to the filtered array and only adjust totalWorkflows when the new
length is less than the previous length (or set totalWorkflows to Math.max(0,
newLength)) so counts cannot desync or go negative; reference the deleteWorkflow
function, deleteWorkflowApi call, the workflows array and totalWorkflows field
when making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 120e7afd-e2ee-48f3-9b41-3b7a0d3d917e

📥 Commits

Reviewing files that changed from the base of the PR and between 67650c5 and fd11b59.

📒 Files selected for processing (39)
  • CLAUDE.md
  • docs/design/engine.md
  • docs/design/page-structure.md
  • src/synthorg/api/app.py
  • src/synthorg/core/enums.py
  • src/synthorg/engine/task_engine.py
  • src/synthorg/engine/task_engine_events.py
  • src/synthorg/engine/task_engine_models.py
  • src/synthorg/engine/workflow/execution_activation_helpers.py
  • src/synthorg/engine/workflow/execution_models.py
  • src/synthorg/engine/workflow/execution_observer.py
  • src/synthorg/engine/workflow/execution_service.py
  • src/synthorg/observability/events/task_engine.py
  • src/synthorg/observability/events/workflow_execution.py
  • tests/unit/engine/test_task_engine_observers.py
  • tests/unit/engine/workflow/test_execution_lifecycle.py
  • tests/unit/engine/workflow/test_execution_models.py
  • tests/unit/engine/workflow/test_execution_observer.py
  • web/CLAUDE.md
  • web/src/components/layout/Sidebar.tsx
  • web/src/hooks/useWorkflowsData.ts
  • web/src/pages/WorkflowEditorPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx
  • web/src/pages/workflow-editor/WorkflowSelector.tsx
  • web/src/pages/workflow-editor/WorkflowToolbar.tsx
  • web/src/pages/workflow-editor/WorkflowYamlEditor.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
  • web/src/pages/workflow-editor/copy-paste.ts
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowGridView.tsx
  • web/src/pages/workflows/WorkflowsSkeleton.tsx
  • web/src/router/index.tsx
  • web/src/stores/workflow-editor.ts
  • web/src/stores/workflows.ts

Comment on lines +703 to +714
if result.success and (self._config.publish_snapshots or self._observers):
event = build_state_changed_event(mutation, result)
if self._config.publish_snapshots:
await publish_snapshot(
mutation,
event,
message_bus=self._message_bus,
sender=self._SNAPSHOT_SENDER,
channel=self._SNAPSHOT_CHANNEL,
)
if self._observers:
await self._notify_observers(event)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move observer delivery off the single-writer hot path.

_process_one() now awaits every registered observer after a successful mutation. One slow database/network observer will therefore stall subsequent task mutations, grow queue pressure, and stretch shutdown drain time even though observer failures are treated as best-effort. Hand these events off to a dedicated background dispatcher/queue instead of awaiting them inline here.

Also applies to: 739-760

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/task_engine.py` around lines 703 - 714, The handler is
awaiting observer delivery inline in _process_one() via
_notify_observers(event), which stalls the single-writer hot path; instead
create or use a dedicated background dispatcher/queue (e.g., an
_observer_dispatcher with an enqueue() method or an asyncio task runner) and
push the built event to it without awaiting delivery here — leave
publish_snapshot awaited when _config.publish_snapshots is true but replace
direct calls to await self._notify_observers(event) (and the similar occurrence
around the other build_state_changed_event usage) with non-blocking
enqueue/dispatch to the background dispatcher so observer delivery happens
asynchronously and best-effort.

Comment on lines +185 to +196
try:
result = evaluate_condition(expr, ctx)
except (ValueError, TypeError, KeyError) as exc:
logger.exception(
WORKFLOW_EXEC_CONDITION_EVAL_FAILED,
execution_id=execution_id,
node_id=nid,
expression=expr,
error=str(exc),
)
msg = f"Failed to evaluate condition on node {nid!r}: {exc}"
raise WorkflowConditionEvalError(msg) from exc
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine lines 185-196
if [ -f "src/synthorg/engine/workflow/execution_activation_helpers.py" ]; then
  echo "=== File exists ==="
  wc -l "src/synthorg/engine/workflow/execution_activation_helpers.py"
  echo ""
  echo "=== Lines 185-196 ==="
  sed -n '185,196p' "src/synthorg/engine/workflow/execution_activation_helpers.py" | cat -n
else
  echo "File not found"
fi

Repository: Aureliolo/synthorg

Length of output: 692


🏁 Script executed:

# Check Python version and ruff configuration
echo "=== pyproject.toml (ruff config) ==="
if [ -f "pyproject.toml" ]; then
  cat pyproject.toml | head -100
fi
echo ""
echo "=== Looking for Python version target ==="
grep -i "python" pyproject.toml | head -20

Repository: Aureliolo/synthorg

Length of output: 3123


🏁 Script executed:

# Check for any other except clauses in the file to understand the pattern
echo "=== All except clauses in the file ==="
rg "except.*:" "src/synthorg/engine/workflow/execution_activation_helpers.py" -A 1

Repository: Aureliolo/synthorg

Length of output: 179


Use the repo's Python 3.14 multi-except form here.

This project standardizes on PEP 758 syntax, so the parenthesized multi-except is out of policy for src/**/*.py.

🔧 Suggested change
-    except (ValueError, TypeError, KeyError) as exc:
+    except ValueError, TypeError, KeyError as exc:

As per coding guidelines, "PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
185 - 196, Replace the parenthesized multi-except with the repo-standard PEP 758
form: in the try/except that calls evaluate_condition(expr, ctx) (the block that
currently catches (ValueError, TypeError, KeyError) as exc), change the except
clause to use comma-separated exception types (except ValueError, TypeError,
KeyError as exc:) and keep the existing logging (logger.exception(...)) and
raise WorkflowConditionEvalError(msg) from exc unchanged; this touches the
evaluate_condition call and the WorkflowConditionEvalError raise in the same
block.

Comment on lines +1 to +58
"""Bridge between TaskEngine events and workflow execution lifecycle.

Registered as a ``TaskEngine`` observer at application startup.
When a task transitions to COMPLETED or FAILED, delegates to
``WorkflowExecutionService`` to update the parent workflow
execution accordingly.
"""

from typing import TYPE_CHECKING

from synthorg.engine.workflow.execution_service import (
WorkflowExecutionService,
)

if TYPE_CHECKING:
from synthorg.engine.task_engine import TaskEngine
from synthorg.engine.task_engine_models import TaskStateChanged
from synthorg.persistence.workflow_definition_repo import (
WorkflowDefinitionRepository,
)
from synthorg.persistence.workflow_execution_repo import (
WorkflowExecutionRepository,
)


class WorkflowExecutionObserver:
"""Bridges TaskEngine events to WorkflowExecutionService.

Constructed once at application startup and registered via
``TaskEngine.register_observer()``.

Args:
definition_repo: Repository for reading workflow definitions.
execution_repo: Repository for persisting execution state.
task_engine: Engine for creating concrete tasks.
"""

def __init__(
self,
*,
definition_repo: WorkflowDefinitionRepository,
execution_repo: WorkflowExecutionRepository,
task_engine: TaskEngine,
) -> None:
self._service = WorkflowExecutionService(
definition_repo=definition_repo,
execution_repo=execution_repo,
task_engine=task_engine,
)

async def __call__(self, event: TaskStateChanged) -> None:
"""Delegate a task state change to the execution service.

Called by ``TaskEngine`` after every successful mutation.
Forwards the event to ``WorkflowExecutionService.handle_task_state_changed``,
which filters for terminal task transitions and updates execution state.
"""
await self._service.handle_task_state_changed(event)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding logger setup for observability consistency.

This module bridges task state changes to workflow execution lifecycle updates. While the actual event handling and logging occurs in WorkflowExecutionService, adding a logger here would allow tracing entry/exit at the observer level if needed for debugging.

♻️ Optional: Add logger setup
 """Bridge between TaskEngine events and workflow execution lifecycle.
 
 Registered as a ``TaskEngine`` observer at application startup.
 When a task transitions to COMPLETED or FAILED, delegates to
 ``WorkflowExecutionService`` to update the parent workflow
 execution accordingly.
 """
 
 from typing import TYPE_CHECKING
 
+from synthorg.observability import get_logger
 from synthorg.engine.workflow.execution_service import (
     WorkflowExecutionService,
 )
 
 if TYPE_CHECKING:
     from synthorg.engine.task_engine import TaskEngine
     from synthorg.engine.task_engine_models import TaskStateChanged
     from synthorg.persistence.workflow_definition_repo import (
         WorkflowDefinitionRepository,
     )
     from synthorg.persistence.workflow_execution_repo import (
         WorkflowExecutionRepository,
     )
 
+logger = get_logger(__name__)

As per coding guidelines: "Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_observer.py` around lines 1 - 58, This
module lacks the required module-level logger; add "from synthorg.observability
import get_logger" and create logger = get_logger(__name__) at top-level, then
use that logger in WorkflowExecutionObserver (e.g., log entry/exit or important
info in __init__ and async __call__ when delegating to
WorkflowExecutionService.handle_task_state_changed) to satisfy the guideline
that every business-logic module must set up a logger for observability.

Comment on lines +48 to +131
<div className="relative rounded-lg border border-border bg-card p-card transition-shadow hover:shadow-[var(--so-shadow-card-hover)]">
<Link to={editorUrl} className="block">
<div className="mb-2 flex items-center gap-2">
<span className="truncate text-sm font-semibold text-foreground">
{workflow.name}
</span>
<span className="rounded-md bg-accent/10 px-1.5 py-0.5 text-xs font-medium text-accent">
{formatLabel(workflow.workflow_type)}
</span>
</div>

{workflow.description && (
<p className="mb-3 line-clamp-2 text-xs text-muted-foreground">
{workflow.description}
</p>
)}

<div className="mb-2 flex flex-wrap items-center gap-2">
<StatPill label="Nodes" value={workflow.nodes.length} />
<StatPill label="Edges" value={workflow.edges.length} />
</div>

<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>v{workflow.version}</span>
<span>Updated {formatRelativeTime(workflow.updated_at)}</span>
</div>
</Link>

<div ref={menuRef} className="absolute right-3 top-3">
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setMenuOpen(!menuOpen)
}}
className="rounded p-1 text-muted-foreground hover:bg-surface hover:text-foreground"
aria-label="Workflow actions"
>
<MoreHorizontal className="size-4" />
</button>

{menuOpen && (
<div
className="absolute right-0 top-full z-10 mt-1 w-36 rounded-lg border border-border bg-card py-1 shadow-lg"
role="menu"
>
<Link
to={editorUrl}
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-foreground hover:bg-surface"
role="menuitem"
onClick={() => setMenuOpen(false)}
>
<Pencil className="size-3.5" />
Edit
</Link>
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-foreground hover:bg-surface"
role="menuitem"
onClick={() => {
setMenuOpen(false)
onDuplicate(workflow.id)
}}
>
<Copy className="size-3.5" />
Duplicate
</button>
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-danger hover:bg-surface"
role="menuitem"
onClick={() => {
setMenuOpen(false)
setConfirmDelete(true)
}}
>
<Trash2 className="size-3.5" />
Delete
</button>
</div>
)}
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use SectionCard instead of a bespoke card shell.

This recreates a card-with-header layout the shared UI layer already owns, so border/shadow/spacing behavior will drift here over time. Wrapping this content in SectionCard keeps the page aligned with the design system and avoids duplicating the container chrome.

Based on learnings, "Do NOT build card-with-header layouts from scratch -- use <SectionCard> component."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 48 - 131, The card
shell in WorkflowCard is hand-rolled (the outer div with rounded-lg border
bg-card p-card etc.)—replace that with the shared SectionCard component to avoid
duplicating card-with-header layout; wrap the existing inner content (title,
labels, description, stats, version/updated) as SectionCard children and move
the header elements (workflow.name + workflow.workflow_type badge) into
SectionCard's header slot or props if it has one, retain the Link to editorUrl
and keep the menu toggle logic (menuRef, menuOpen, setMenuOpen, onDuplicate,
setConfirmDelete) but adjust positioning/classes so the menu button remains
absolutely positioned (e.g., ensure SectionCard is relatively positioned or pass
a className to SectionCard).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

♻️ Duplicate comments (4)
src/synthorg/engine/workflow/execution_observer.py (1)

9-25: 🛠️ Refactor suggestion | 🟠 Major

Add the required module logger in this bridge module.

WorkflowExecutionObserver is business logic, so this file should follow the repo-wide logger contract even if it only emits DEBUG traces around delegation.

♻️ Suggested change
 from typing import TYPE_CHECKING
 
+from synthorg.observability import get_logger
 from synthorg.engine.workflow.execution_service import (
     WorkflowExecutionService,
 )
@@
 if TYPE_CHECKING:
     from synthorg.engine.task_engine import TaskEngine
@@
         WorkflowExecutionRepository,
     )
 
+
+logger = get_logger(__name__)
+
 
 class WorkflowExecutionObserver:

As per coding guidelines, Every module with business logic MUST have: from synthorg.observability import get_logger then logger = get_logger(__name__).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_observer.py` around lines 9 - 25, The
module is missing the required module logger; add the repo-standard logger by
importing get_logger from synthorg.observability and creating logger =
get_logger(__name__) near the top of the file so WorkflowExecutionObserver (and
any top-level delegation/tracing) can emit DEBUG traces using logger.debug(...)
per the repo contract.
src/synthorg/engine/task_engine.py (1)

703-714: ⚠️ Potential issue | 🟠 Major

Don't await observers on the single-writer path.

_process_one() now blocks on every registered observer after the mutation is already persisted. One slow DB/network observer will stall later task mutations, extend shutdown drain time, and make the new observer semantics effectively blocking in practice. Queue observer delivery onto a lifecycle-owned background dispatcher/worker instead of awaiting _notify_observers() inline here.

Based on learnings, TaskEngine is the project's centralized single-writer task state engine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/task_engine.py` around lines 703 - 714, The code
currently awaits self._notify_observers(event) inside _process_one(), blocking
the single-writer path; change this to enqueue or schedule observer delivery on
a TaskEngine-owned background dispatcher so observer handlers run asynchronously
and don't stall later mutations. Implement or reuse a lifecycle-owned
queue/worker (e.g., create a self._observer_queue and a background coroutine
worker or a helper method like _enqueue_observer_event(event) that uses
asyncio.create_task or puts the event on the queue) and have the worker call
_notify_observers(event); ensure the dispatcher is started when TaskEngine
initializes and drained/stopped cleanly on shutdown so observer delivery is
eventual and not awaited inline in _process_one().
src/synthorg/engine/workflow/execution_activation_helpers.py (1)

187-196: ⚠️ Potential issue | 🟡 Minor

Use the repo's Python 3.14 multi-except form here.

This clause is still using the parenthesized syntax, which is out of policy for src/**/*.py and likely to be re-flagged by linting.

🔧 Suggested change
-    except (ValueError, TypeError, KeyError) as exc:
+    except ValueError, TypeError, KeyError as exc:

Verification: inspect this block and confirm the helper still contains the parenthesized multi-except form.

#!/bin/bash
sed -n '185,196p' src/synthorg/engine/workflow/execution_activation_helpers.py
rg -n 'except \(' src/synthorg/engine/workflow/execution_activation_helpers.py

As per coding guidelines, PEP 758 except syntax: use except A, B: (no parentheses) — ruff enforces this on Python 3.14.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
187 - 196, Replace the parenthesized multi-except "except (ValueError,
TypeError, KeyError) as exc:" with the Python 3.14 multi-except form (use the
exception union operator) so it becomes "except ValueError | TypeError |
KeyError as exc:"; keep the body unchanged (the logger.exception call that uses
WORKFLOW_EXEC_CONDITION_EVAL_FAILED and the raise of WorkflowConditionEvalError
that references execution_id, nid, expr should remain intact).
web/src/pages/workflows/WorkflowCard.tsx (1)

25-50: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for the workflow card shell.

This re-implements a card-with-header layout the shared UI layer already owns, so spacing and chrome will drift here over time.

Based on learnings, "Do NOT build card-with-header layouts from scratch -- use <SectionCard> component."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 25 - 50, Replace the
custom outer div shell in WorkflowCard (the rounded-lg border block wrapping the
Link) with the shared SectionCard component: move the workflow name,
workflow_type badge (formatLabel(workflow.workflow_type)) and optional
description into SectionCard's header/title area (use SectionCard props or
header slot as appropriate), keep the Link around the clickable header/body
content, and place the StatPill cluster (Nodes/Edges) and the footer row
(version and Updated formatRelativeTime(workflow.updated_at)) into the
SectionCard body/footer; remove the handcrafted border/spacing classes and rely
on SectionCard for chrome so spacing and header styling are consistent across
the app.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/engine/workflow/execution_activation_helpers.py`:
- Around line 175-184: The current code treats a missing or blank
node.config["condition_expression"] as false; instead, detect if
condition_expression is absent or empty (e.g., expr == "" or
node.config.get("condition_expression") is None/blank) before calling
evaluate_condition(), log using WORKFLOW_EXEC_CONDITION_EVAL_FAILED with
execution_id, node_id (nid) and expression (expr), and raise
WorkflowConditionEvalError so activation is rejected; update the block around
expr/evaluate_condition() to perform this validation and short-circuit by
logging and raising the WorkflowConditionEvalError rather than logging a warning
and defaulting to false.

In `@src/synthorg/engine/workflow/execution_service.py`:
- Around line 493-545: The lifecycle transition methods (e.g., fail_execution
and the similar complete/cancel handlers) currently read an execution via
_load_running, set status and version = execution.version + 1, then call
_execution_repo.save, which will raise VersionConflictError on races; refactor
these to use a conflict-aware retry pattern (extract or add a helper like
_save_with_conflict_retry) that on VersionConflictError reloads the execution
from storage, checks if its status is already a terminal state
(WorkflowExecutionStatus.FAILED/COMPLETED/CANCELLED) and if so returns that
reloaded execution as a benign no-op, otherwise applies the
status/update/version increment against the fresh model and retries the save
until success or a limited retry count; replace direct calls to
_execution_repo.save in fail_execution (and the same logic around
complete_execution and cancel_execution referenced) with this helper and ensure
the logger uses the final persisted entity.
- Around line 744-746: Before raising the ValueError when "found" is false, emit
a log at WARNING or ERROR with structured context including execution_id
(execution.id) and task_id; e.g., call the module/class logger (use the existing
logger object or self.logger used elsewhere in execution_service.py) to log a
message like "task/execution correlation failure" with execution_id and task_id
fields, then raise the ValueError as currently written to preserve behavior.

In `@tests/unit/engine/workflow/test_execution_lifecycle.py`:
- Around line 93-105: list_by_definition() and list_by_status() currently return
live references from self._store; change them to return deep copies so the fake
repo mirrors get() behavior and prevents in-place mutation from leaking into
tests. Use copy.deepcopy to return copies of the stored WorkflowExecution
objects (e.g., return tuple(copy.deepcopy(e) for e in self._store.values() if
...)) and add the necessary import for the copy module; update both
list_by_definition and list_by_status accordingly.

In `@web/src/pages/workflow-editor/condition-expression-types.ts`:
- Around line 174-180: The NOT parsing currently flattens an inner group by
using inner.conditions, dropping the group's operator (so NOT (a==1 OR b==2)
becomes NOT a==1 AND b==2 after serialization); change the logic in the NOT
handling inside parseConditionString so that if
parseConditionString(notMatch[1]) returns a group you do not extract its
.conditions but instead treat the whole group as a single child (i.e., children
= [inner] for inner.kind === 'group', otherwise children = [inner]) and then
call createGroup('NOT', children) so the inner operator is preserved when
serialized.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 58-64: parseForBuilder currently calls flattenExpression() which
returns null for nested ConditionGroup nodes produced by buildExpression(),
causing any expression created with "Add group" to be unreadable by the builder;
fix parseForBuilder (which calls parseConditionString) to recursively handle
ConditionGroup nodes instead of flattening them: traverse the parsed AST and
convert nested ConditionGroup nodes into the builder's representation (or extend
flattenExpression to support nested groups), preserving nested logical operators
and comparisons so parseForBuilder returns a non-null structure for grouped
expressions; reference parseForBuilder, parseConditionString, flattenExpression,
buildExpression and ConditionGroup when implementing the recursive conversion.
- Around line 217-241: The guard that prevents re-parsing only compares value
against a serialization built from entries.map(e => e.comparison) and
logicalOperator, missing per-entry negate and subGroups so builder-originated
updates that toggle NOT or add subgroups get treated as external; update the
guard to compute currentSerialized using the full entry objects (including
negate and subGroups) instead of only e.comparison — i.e., change
currentComparisons to use the entire entry (or adjust
buildExpression/serializeCondition to accept entries), then call
serializeCondition(buildExpression(currentEntries, logicalOperator)) so
lastSyncedRef correctly matches builder-originated serializations and avoid
needless reparsing; keep the rest of the effect (parseForBuilder, setEntries,
setLogicalOperator, setMode, setFreeText) unchanged.
- Around line 351-362: handleModeChange currently drops or leaks builder-only
state: when switching builder→advanced you serialize without applying the
group's negate and subgroup structure, and advanced→builder only restores
comparisons and logicalOperator so previous negate/subGroups can bleed through.
Fix by, in the 'advanced' branch, building the full expression via
buildExpression(comparisons, logicalOperator, subGroups) and ensure you apply
any group-level negate before calling serializeCondition and setFreeText (e.g.,
wrap or mark the expr as negated); in the 'builder' branch, after
parseForBuilder(freeText) succeeds, restore all builder state returned by the
parser — call setEntries(toEntries(flat.comparisons)),
setLogicalOperator(flat.logicalOperator), setNegate(flat.negate) and
setSubGroups(flat.subGroups) (or clear subGroups if parser returns none) so
negate and subgroup state are correctly synced; keep the existing early-return
if parse fails.

In `@web/src/pages/workflow-editor/WorkflowYamlEditor.tsx`:
- Around line 57-62: The handler currently updates the editor state whenever
result.errors is empty, which lets warn-only parses (e.g., unsupported step
types or missing ids) drop nodes silently; modify parseYamlToNodesEdges to
return a concrete indicator of dropped/unsupported nodes (e.g.,
result.droppedNodes or result.unsupportedSteps array) and then change the
early-return condition to: if (result.errors.length > 0 || result.droppedNodes)
return; keep setting parse errors/warnings with setParseErrors/setParseWarnings
but only call setNodes/setEdges (or otherwise commit the parse) when there are
no errors and no droppedNodes/unsupportedSteps. Use the function and state names
parseYamlToNodesEdges, setParseErrors, setParseWarnings, setNodes, setEdges to
locate the code to change.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 81-89: The import is guessing conditional edges by YAML order in
inferDependsOnEdgeType, which breaks round-trip because workflow-to-yaml only
emits plain depends_on ids; change the YAML serialization (workflow-to-yaml.ts)
to emit depends_on entries with explicit edge type metadata (e.g., object { id,
edge: "conditional_true" | "conditional_false" | "sequential" }) when source
step is conditional, and update the parser (yaml-to-nodes.ts) and
inferDependsOnEdgeType to prefer that explicit edge field instead of deriving
from branch index; keep a fallback to the old string form for backward
compatibility (preserve current behavior if depends_on is a string) and update
both places mentioned (the inferDependsOnEdgeType function and the serializer
around the block referenced at 103-129 and the other spots around 213-223) so
conditional_true/conditional_false are preserved across export/import.
- Around line 149-161: The bug is that seenIds is populated before validating
step type so unknown-type steps are treated as "seen" and later dependency
resolution calls stepMap.get(depId)! which can be undefined (causing
inferDependsOnEdgeType to throw); fix by moving seenIds.add(stepId) (and adding
to stepMap) only after validating stepType (i.e., after checking VALID_TYPES and
before continuing), and in dependency-resolution passes (the blocks around lines
referenced 205-215 and 240-244) guard uses of stepMap.get(depId)! by first
checking stepMap.has(depId) (or skipping/issuing a warning when the dependency
is not in stepMap) so inferDependsOnEdgeType only receives validated steps.

In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 304-362: The node drawer (WorkflowNodeDrawer) is rendered even in
YAML mode causing a stale inspector to remain; update the rendering logic so
WorkflowNodeDrawer only mounts when editorMode === 'visual' (and when a node is
selected) and ensure any selection state is cleared when switching out of visual
mode (e.g., call setSelectedNode(null) or invoke the existing close handler
inside the mode switch). Locate WorkflowNodeDrawer, editorMode, and the
selection state setter (setSelectedNode or equivalent) and wrap the drawer
render in a conditional check for editorMode === 'visual', plus clear selection
on mode change.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 69-72: The DropdownMenu.Content element's className currently
includes the Tailwind utility "shadow-lg" which bypasses design-system shadow
tokens; update the className on DropdownMenu.Content to remove "shadow-lg" and
add the design token variable (e.g. var(--so-shadow-card-hover) or the
appropriate shadow token) so the component uses the tokenized elevation, keeping
existing classes like "z-50 w-36 rounded-lg border border-border bg-card py-1"
intact; search for the DropdownMenu.Content usage in WorkflowCard.tsx to make
this change.
- Around line 1-2: The file WorkflowCard.tsx imports DropdownMenu from the
umbrella 'radix-ui' package; change that import to use the specific package by
replacing the current import of DropdownMenu with an import from
'@radix-ui/react-dropdown-menu' so it matches WorkflowTableView.tsx and follows
Radix recommendations (update the import that references DropdownMenu in
WorkflowCard.tsx).

In `@web/src/pages/workflows/WorkflowFilters.tsx`:
- Around line 7-17: Extract the duplicated WORKFLOW_TYPES constant into a shared
module (e.g., create and export WORKFLOW_TYPES and an associated WorkflowType
type from a new file like `@/utils/constants.ts` or `@/api/types.ts`) and update
both WorkflowFilters.tsx and WorkflowCreateDrawer.tsx to import WORKFLOW_TYPES;
keep WORKFLOW_TYPE_OPTIONS and formatLabel usage in WorkflowFilters.tsx
unchanged but build it from the imported WORKFLOW_TYPES, and ensure any type
annotations referencing the literal array use the exported WorkflowType type.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 58-64: The table row in WorkflowTableView.tsx currently uses
role="link" on the <tr> but isn’t keyboard-focusable or activatable; make the
row keyboard-accessible by adding tabIndex={0} and an onKeyDown handler (e.g.,
handleRowKeyDown) that calls navigate(editorUrl) when Enter or Space is pressed,
while preserving the existing onClick and aria-label; ensure the handler ignores
key events originating from interactive child elements so clicks on overflow
menu/buttons still behave normally.
- Around line 87-90: The DropdownMenu.Content currently uses the hardcoded
Tailwind utility "shadow-md"; update the dropdown panel to use the shared shadow
token instead by replacing that utility in the className of DropdownMenu.Content
with the shadow token (e.g. use the CSS variable var(--so-shadow-card-hover) as
the shadow), ensuring you keep the other tokenized classes like border-border
and the existing layout classes intact so the final className references the
shared shadow token rather than "shadow-md".

In `@web/src/stores/workflows.ts`:
- Around line 44-59: The fetchWorkflows implementation currently hardcodes
listWorkflows({ limit: 200 }); update fetchWorkflows to accept optional paging
parameters (e.g., limit and cursor/pageToken) or a config object, pass those
through to listWorkflows instead of the fixed 200, and expose a new method
(e.g., fetchWorkflowsPage or fetchWorkflows({ limit, cursor })) to support
cursor-based pagination; update any callers of fetchWorkflows to provide desired
limits or iterate using the cursor returned in result
(result.total/result.nextCursor) so large deployments can request pages instead
of relying on a single fixed limit.

---

Duplicate comments:
In `@src/synthorg/engine/task_engine.py`:
- Around line 703-714: The code currently awaits self._notify_observers(event)
inside _process_one(), blocking the single-writer path; change this to enqueue
or schedule observer delivery on a TaskEngine-owned background dispatcher so
observer handlers run asynchronously and don't stall later mutations. Implement
or reuse a lifecycle-owned queue/worker (e.g., create a self._observer_queue and
a background coroutine worker or a helper method like
_enqueue_observer_event(event) that uses asyncio.create_task or puts the event
on the queue) and have the worker call _notify_observers(event); ensure the
dispatcher is started when TaskEngine initializes and drained/stopped cleanly on
shutdown so observer delivery is eventual and not awaited inline in
_process_one().

In `@src/synthorg/engine/workflow/execution_activation_helpers.py`:
- Around line 187-196: Replace the parenthesized multi-except "except
(ValueError, TypeError, KeyError) as exc:" with the Python 3.14 multi-except
form (use the exception union operator) so it becomes "except ValueError |
TypeError | KeyError as exc:"; keep the body unchanged (the logger.exception
call that uses WORKFLOW_EXEC_CONDITION_EVAL_FAILED and the raise of
WorkflowConditionEvalError that references execution_id, nid, expr should remain
intact).

In `@src/synthorg/engine/workflow/execution_observer.py`:
- Around line 9-25: The module is missing the required module logger; add the
repo-standard logger by importing get_logger from synthorg.observability and
creating logger = get_logger(__name__) near the top of the file so
WorkflowExecutionObserver (and any top-level delegation/tracing) can emit DEBUG
traces using logger.debug(...) per the repo contract.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 25-50: Replace the custom outer div shell in WorkflowCard (the
rounded-lg border block wrapping the Link) with the shared SectionCard
component: move the workflow name, workflow_type badge
(formatLabel(workflow.workflow_type)) and optional description into
SectionCard's header/title area (use SectionCard props or header slot as
appropriate), keep the Link around the clickable header/body content, and place
the StatPill cluster (Nodes/Edges) and the footer row (version and Updated
formatRelativeTime(workflow.updated_at)) into the SectionCard body/footer;
remove the handcrafted border/spacing classes and rely on SectionCard for chrome
so spacing and header styling are consistent across the app.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0911101d-b3d3-4103-bcb8-828f6da07f2a

📥 Commits

Reviewing files that changed from the base of the PR and between fd11b59 and 86b689d.

📒 Files selected for processing (27)
  • CLAUDE.md
  • docs/design/engine.md
  • docs/design/page-structure.md
  • src/synthorg/engine/task_engine.py
  • src/synthorg/engine/workflow/execution_activation_helpers.py
  • src/synthorg/engine/workflow/execution_observer.py
  • src/synthorg/engine/workflow/execution_service.py
  • src/synthorg/observability/events/workflow_execution.py
  • tests/unit/engine/workflow/test_execution_helpers.py
  • tests/unit/engine/workflow/test_execution_lifecycle.py
  • tests/unit/engine/workflow/test_execution_observer.py
  • web/src/api/types.ts
  • web/src/hooks/useWorkflowsData.ts
  • web/src/pages/WorkflowEditorPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx
  • web/src/pages/workflow-editor/WorkflowSelector.tsx
  • web/src/pages/workflow-editor/WorkflowYamlEditor.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/stores/workflow-editor.ts
  • web/src/stores/workflows.ts

Comment on lines +175 to +184
expr = str(node.config.get("condition_expression", "false"))
if not node.config.get("condition_expression"):
logger.warning(
WORKFLOW_EXEC_CONDITION_EVALUATED,
execution_id=execution_id,
node_id=nid,
expression=expr,
result=False,
note="missing condition_expression, defaulting to false",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject missing condition_expression instead of treating it as false.

If YAML round-trip or editor state drops this field, activation silently takes the false branch and persists a run with different semantics than the workflow author defined. Treat a missing or blank expression as invalid config: log WORKFLOW_EXEC_CONDITION_EVAL_FAILED and raise WorkflowConditionEvalError before calling evaluate_condition().

💡 Suggested change
-    expr = str(node.config.get("condition_expression", "false"))
-    if not node.config.get("condition_expression"):
-        logger.warning(
-            WORKFLOW_EXEC_CONDITION_EVALUATED,
-            execution_id=execution_id,
-            node_id=nid,
-            expression=expr,
-            result=False,
-            note="missing condition_expression, defaulting to false",
-        )
+    raw_expr = node.config.get("condition_expression")
+    if not isinstance(raw_expr, str) or not raw_expr.strip():
+        msg = f"Conditional node {nid!r} is missing condition_expression"
+        logger.error(
+            WORKFLOW_EXEC_CONDITION_EVAL_FAILED,
+            execution_id=execution_id,
+            node_id=nid,
+            error=msg,
+        )
+        raise WorkflowConditionEvalError(msg)
+    expr = raw_expr

As per coding guidelines, Validate: at system boundaries (user input, external APIs, config files).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
175 - 184, The current code treats a missing or blank
node.config["condition_expression"] as false; instead, detect if
condition_expression is absent or empty (e.g., expr == "" or
node.config.get("condition_expression") is None/blank) before calling
evaluate_condition(), log using WORKFLOW_EXEC_CONDITION_EVAL_FAILED with
execution_id, node_id (nid) and expression (expr), and raise
WorkflowConditionEvalError so activation is rejected; update the block around
expr/evaluate_condition() to perform this validation and short-circuit by
logging and raising the WorkflowConditionEvalError rather than logging a warning
and defaulting to false.

Comment on lines +493 to +545
execution = await self._load_running(execution_id)
now = datetime.now(UTC)
completed = execution.model_copy(
update={
"status": WorkflowExecutionStatus.COMPLETED,
"updated_at": now,
"completed_at": now,
"version": execution.version + 1,
},
)
await self._execution_repo.save(completed)
logger.info(
WORKFLOW_EXEC_COMPLETED,
execution_id=execution_id,
)
return completed

async def fail_execution(
self,
execution_id: str,
*,
error: str,
) -> WorkflowExecution:
"""Transition a running execution to FAILED.

Args:
execution_id: The execution identifier.
error: Error message describing the failure.

Returns:
The updated execution in FAILED status.

Raises:
WorkflowExecutionNotFoundError: If not found.
WorkflowExecutionError: If execution is not RUNNING.
"""
execution = await self._load_running(execution_id)
now = datetime.now(UTC)
failed = execution.model_copy(
update={
"status": WorkflowExecutionStatus.FAILED,
"error": error,
"updated_at": now,
"completed_at": now,
"version": execution.version + 1,
},
)
await self._execution_repo.save(failed)
logger.info(
WORKFLOW_EXEC_FAILED,
execution_id=execution_id,
error=error,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make these lifecycle writes conflict-tolerant.

All of these transitions do read → version + 1 → save once. If a terminal task callback races cancel_execution() or another lifecycle write, the loser will raise VersionConflictError out of the observer/API path instead of converging on the already-persisted terminal state. Please funnel these saves through a conflict-aware reload/retry helper, and treat “already terminal after reload” as a benign no-op.

Also applies to: 580-658

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_service.py` around lines 493 - 545,
The lifecycle transition methods (e.g., fail_execution and the similar
complete/cancel handlers) currently read an execution via _load_running, set
status and version = execution.version + 1, then call _execution_repo.save,
which will raise VersionConflictError on races; refactor these to use a
conflict-aware retry pattern (extract or add a helper like
_save_with_conflict_retry) that on VersionConflictError reloads the execution
from storage, checks if its status is already a terminal state
(WorkflowExecutionStatus.FAILED/COMPLETED/CANCELLED) and if so returns that
reloaded execution as a benign no-op, otherwise applies the
status/update/version increment against the fresh model and retries the save
until success or a limited retry count; replace direct calls to
_execution_repo.save in fail_execution (and the same logic around
complete_execution and cancel_execution referenced) with this helper and ensure
the logger uses the final persisted entity.

Comment on lines +44 to +59
fetchWorkflows: async () => {
const token = ++_listRequestToken
set({ listLoading: true, listError: null })
try {
const result = await listWorkflows({ limit: 200 })
if (isStaleListRequest(token)) return
set({
workflows: result.data,
totalWorkflows: result.total,
listLoading: false,
})
} catch (err) {
if (isStaleListRequest(token)) return
set({ listLoading: false, listError: getErrorMessage(err) })
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider making the limit configurable or adding pagination.

The hardcoded limit: 200 may be insufficient for deployments with many workflows. Consider either making this configurable or implementing cursor-based pagination if the workflow count grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/workflows.ts` around lines 44 - 59, The fetchWorkflows
implementation currently hardcodes listWorkflows({ limit: 200 }); update
fetchWorkflows to accept optional paging parameters (e.g., limit and
cursor/pageToken) or a config object, pass those through to listWorkflows
instead of the fixed 200, and expose a new method (e.g., fetchWorkflowsPage or
fetchWorkflows({ limit, cursor })) to support cursor-based pagination; update
any callers of fetchWorkflows to provide desired limits or iterate using the
cursor returned in result (result.total/result.nextCursor) so large deployments
can request pages instead of relying on a single fixed limit.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (5)
web/src/pages/workflows/WorkflowCard.tsx (2)

25-98: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard instead of a bespoke card shell.

This still rebuilds a card-with-header layout manually. Please migrate the outer container/content chrome to the shared SectionCard to keep design-system behavior consistent.

Proposed refactor (adapt props/slots to your local SectionCard API)
+import { SectionCard } from '@/components/ui/section-card'
...
-      <div className="relative rounded-lg border border-border bg-card p-card transition-shadow hover:shadow-[var(--so-shadow-card-hover)]">
+      <SectionCard className="relative transition-shadow hover:shadow-[var(--so-shadow-card-hover)]">
         <Link to={editorUrl} className="block">
           ...
         </Link>
         <DropdownMenu.Root>
           ...
         </DropdownMenu.Root>
-      </div>
+      </SectionCard>

Based on learnings, "Do NOT build card-with-header layouts from scratch -- use <SectionCard> component."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 25 - 98, The card UI
in WorkflowCard.tsx is implemented manually; replace the outer <div
className="relative rounded-lg ..."> wrapper with the shared SectionCard
component and move the header/title, meta (version/updated), body (description,
StatPill row), and action menu into the appropriate SectionCard slots/props
(e.g., title slot for the workflow.name and workflow.workflow_type badge,
actions slot for the DropdownMenu Trigger/Portal, body for description and
StatPill), preserving existing behavior of Link to editorUrl, the
Edit/Duplicate/Delete handlers (onDuplicate, setConfirmDelete, navigate), and
CSS classes where needed so styling and interaction remain identical while using
SectionCard instead of a bespoke shell.

1-2: 🧹 Nitpick | 🔵 Trivial

Align DropdownMenu import with the specific Radix package.

Please switch to @radix-ui/react-dropdown-menu for consistency with other workflow page code and tighter dependency granularity.

#!/bin/bash
set -euo pipefail

echo "Check dropdown imports in workflow pages:"
rg -n "from 'radix-ui'|from '@radix-ui/react-dropdown-menu'" \
  web/src/pages/workflows/WorkflowCard.tsx \
  web/src/pages/workflows/WorkflowTableView.tsx

echo
echo "Check installed dependencies in web/package.json:"
jq -r '
  .dependencies // {} |
  to_entries[] |
  select(.key == "radix-ui" or .key == "@radix-ui/react-dropdown-menu") |
  "\(.key): \(.value)"
' web/package.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 1 - 2, Replace the
ambiguous import "import { DropdownMenu } from 'radix-ui'" in WorkflowCard.tsx
with the specific package "import { DropdownMenu } from
'@radix-ui/react-dropdown-menu' so it matches the pattern used in other workflow
pages (e.g., WorkflowTableView.tsx); update any related named imports if
necessary to align with `@radix-ui/react-dropdown-menu` exports and run the same
search used in the review to verify no other files import from 'radix-ui' and
that package.json has the correct dependency entry.
web/src/pages/workflows/WorkflowTableView.tsx (1)

58-64: ⚠️ Potential issue | 🟠 Major

Prevent row key handler from hijacking nested controls

Line 63 currently navigates even when Enter/Space originates from interactive children (e.g., the actions button/menu), which can unexpectedly open the editor instead of performing the child action.

Proposed fix
+  const isFromInteractiveChild = (target: EventTarget | null): boolean => {
+    if (!(target instanceof HTMLElement)) return false
+    return Boolean(
+      target.closest('button, a, input, select, textarea, [role="menuitem"], [role="button"]'),
+    )
+  }
+
   return (
@@
-                  onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(editorUrl) } }}
+                  onKeyDown={(e) => {
+                    if (isFromInteractiveChild(e.target)) return
+                    if (e.key === 'Enter' || e.key === ' ') {
+                      e.preventDefault()
+                      navigate(editorUrl)
+                    }
+                  }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowTableView.tsx` around lines 58 - 64, The
row-level onKeyDown handler in WorkflowTableView.tsx calls navigate(editorUrl)
for Enter/Space even when the event comes from interactive children; update the
onKeyDown for the <tr> to ignore events originating from nested interactive
elements by checking the event target (e.g., using e.currentTarget vs e.target
or DOM traversal like target.closest) and only call navigate(editorUrl) when the
key event originates from the row itself (not from descendants such as button,
a, input, select, textarea, or elements with role="button"/"menuitem"); keep the
Enter/Space check and preventDefault behavior but bail out early if an
interactive ancestor is found so child controls retain their expected behavior.
web/src/pages/workflow-editor/yaml-to-nodes.ts (1)

205-220: ⚠️ Potential issue | 🟠 Major

Preserve conditional branch labels instead of deriving them from traversal order.

conditionalBranchCounters makes the first dependent of a conditional node conditional_true and the second one conditional_false. Reordering YAML steps or dependency lists therefore flips true/false semantics without changing the workflow itself. Please carry explicit edge metadata in depends_on and prefer that here, keeping the counter only as a backward-compatible fallback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` around lines 205 - 220, The
current logic derives conditional branch labels using conditionalBranchCounters,
which breaks semantics when YAML order changes; update the depends_on handling
so that each dependency entry can be either a string or an object with explicit
edge metadata (e.g. { id: "stepA", branch: "true" } or similar), and prefer that
explicit branch label to determine the edge type (pass the explicit label into
inferDependsOnEdgeType or a new helper) when present; only fall back to using
conditionalBranchCounters when no explicit branch metadata exists, and only
increment conditionalBranchCounters in that fallback path for sourceStep.type
=== 'conditional' so existing workflows remain compatible.
web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx (1)

58-64: ⚠️ Potential issue | 🟠 Major

Add group still creates expressions this parser can't read back.

handleAddGroup() produces nested groups, but parseForBuilder() still just delegates to flattenExpression(), which returns null as soon as it sees a nested group. Any grouped condition therefore reopens in Advanced mode and loses the structured round-trip this feature is supposed to provide.

Also applies to: 284-293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines 58
- 64, parseForBuilder currently returns null when flattenExpression encounters
nested groups, breaking round-trip for conditions created by handleAddGroup;
update parseForBuilder/flattenExpression so they recursively traverse the AST
returned by parseConditionString and either (A) flatten nested GroupExpression
nodes into the comparisons array in the shape the builder expects or (B) return
a structured representation the builder can consume (preserving group
boundaries) instead of null. Locate parseForBuilder, flattenExpression and
handleAddGroup and implement a recursive merge or conversion of nested groups
into the builder's expected format (ensuring logicalOperator for the outer group
is preserved) so grouped conditions reopen in Structured mode rather than
Advanced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/workflow-editor/condition-expression-types.ts`:
- Around line 209-223: flattenExpression currently treats a unary NOT as a flat
logicalOperator which causes double-negation in ConditionExpressionBuilder;
change flattenExpression (the function named flattenExpression in this file) to
detect expr.logicalOperator === 'NOT' and do not flatten such expressions —
return null for NOT groups (or alternatively extend the return shape to include
a separate negate boolean and set negate=true instead of using
logicalOperator='NOT'); ensure callers (e.g., ConditionExpressionBuilder)
continue to rely on its separate negation tracking when NOT groups are
encountered.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 396-465: The JSX for entries.map and subGroups.map is too large
and duplicates layout/behavior; extract a ConditionRow component to render each
entry (use ComparisonRow, SegmentedControl, pass props like comparison, index,
baseId, canRemove, onUpdate: handleUpdateRow, onRemove: handleRemoveRow) and
extract a ConditionGroup component to render each group (use group.operator,
group.key, map group.entries to ConditionRow via
handleGroupUpdateRow/handleGroupRemoveRow, call handleGroupOperatorChange and
handleRemoveGroup), and replace the raw <button> in the subgroup block with the
shared Button component and the existing handleGroupAddRow handler so all wiring
is centralized and maps contain only <ConditionRow /> or <ConditionGroup />
calls.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Line 92: The element in WorkflowTableView.tsx currently uses a hardcoded
min-w-[140px] in its className; replace that with a design token or standard
Tailwind spacing class to avoid pixel hardcoding—for example use the project's
density-aware token (e.g., min-w-card or a provided token for card/min column
width) or a standard Tailwind spacing equivalent (e.g., min-w-36 / 9rem) so
sizing follows the design system; update the className string where
min-w-[140px] appears.

---

Duplicate comments:
In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 58-64: parseForBuilder currently returns null when
flattenExpression encounters nested groups, breaking round-trip for conditions
created by handleAddGroup; update parseForBuilder/flattenExpression so they
recursively traverse the AST returned by parseConditionString and either (A)
flatten nested GroupExpression nodes into the comparisons array in the shape the
builder expects or (B) return a structured representation the builder can
consume (preserving group boundaries) instead of null. Locate parseForBuilder,
flattenExpression and handleAddGroup and implement a recursive merge or
conversion of nested groups into the builder's expected format (ensuring
logicalOperator for the outer group is preserved) so grouped conditions reopen
in Structured mode rather than Advanced.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 205-220: The current logic derives conditional branch labels using
conditionalBranchCounters, which breaks semantics when YAML order changes;
update the depends_on handling so that each dependency entry can be either a
string or an object with explicit edge metadata (e.g. { id: "stepA", branch:
"true" } or similar), and prefer that explicit branch label to determine the
edge type (pass the explicit label into inferDependsOnEdgeType or a new helper)
when present; only fall back to using conditionalBranchCounters when no explicit
branch metadata exists, and only increment conditionalBranchCounters in that
fallback path for sourceStep.type === 'conditional' so existing workflows remain
compatible.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 25-98: The card UI in WorkflowCard.tsx is implemented manually;
replace the outer <div className="relative rounded-lg ..."> wrapper with the
shared SectionCard component and move the header/title, meta (version/updated),
body (description, StatPill row), and action menu into the appropriate
SectionCard slots/props (e.g., title slot for the workflow.name and
workflow.workflow_type badge, actions slot for the DropdownMenu Trigger/Portal,
body for description and StatPill), preserving existing behavior of Link to
editorUrl, the Edit/Duplicate/Delete handlers (onDuplicate, setConfirmDelete,
navigate), and CSS classes where needed so styling and interaction remain
identical while using SectionCard instead of a bespoke shell.
- Around line 1-2: Replace the ambiguous import "import { DropdownMenu } from
'radix-ui'" in WorkflowCard.tsx with the specific package "import { DropdownMenu
} from '@radix-ui/react-dropdown-menu' so it matches the pattern used in other
workflow pages (e.g., WorkflowTableView.tsx); update any related named imports
if necessary to align with `@radix-ui/react-dropdown-menu` exports and run the
same search used in the review to verify no other files import from 'radix-ui'
and that package.json has the correct dependency entry.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 58-64: The row-level onKeyDown handler in WorkflowTableView.tsx
calls navigate(editorUrl) for Enter/Space even when the event comes from
interactive children; update the onKeyDown for the <tr> to ignore events
originating from nested interactive elements by checking the event target (e.g.,
using e.currentTarget vs e.target or DOM traversal like target.closest) and only
call navigate(editorUrl) when the key event originates from the row itself (not
from descendants such as button, a, input, select, textarea, or elements with
role="button"/"menuitem"); keep the Enter/Space check and preventDefault
behavior but bail out early if an interactive ancestor is found so child
controls retain their expected behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f674b243-f169-4546-a71e-766bea751eb2

📥 Commits

Reviewing files that changed from the base of the PR and between 86b689d and 2dbe190.

📒 Files selected for processing (5)
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-*)) for colors. NEVER hardcode hex values or rgba() in .tsx/.ts files
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly in styles
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets
Use /// <reference types="vite/client" /> to enable side-effect imports for CSS in Storybook 10 with TypeScript 6
Ensure all TypeScript code passes type checking via npm --prefix web run type-check
Bundle size must be auditable via npm --prefix web run analyze (generates stats.html treemap)
Achieve Lighthouse performance audit target of 90+ via npm --prefix web run lighthouse

web/src/**/*.{ts,tsx}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones
Web Dashboard Design System (MANDATORY): See web/CLAUDE.md for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Run ESLint with zero warnings enforced via npm --prefix web run lint

Line length: 88 characters (ruff) for JavaScript/TypeScript files

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{ts,tsx}: Organize page-scoped sub-components in pages/<page-name>/ subdirectories rather than inline in page files
Use lazy-loaded page components in web/src/pages/ with React Router and route guards for auth/setup
Use @AnimatedPresence wrapper for page transitions in React Router (keyed by route)

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
🧠 Learnings (30)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` component

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T21:18:15.006Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T21:18:15.006Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web Dashboard Design System (MANDATORY): See `web/CLAUDE.md` for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-31T20:21:35.626Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:21:35.626Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-15T21:32:02.880Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🪛 ast-grep (0.42.0)
web/src/pages/workflow-editor/condition-expression-types.ts

[warning] 107-107: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\s+${op}\\s+)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (1)
web/src/pages/workflows/WorkflowCard.tsx (1)

69-73: Tokenized menu elevation looks good.

Using border-border and shadow-[var(--so-shadow-card-hover)] keeps this aligned with dashboard design tokens.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (3)
web/src/pages/workflow-editor/yaml-to-nodes.ts (1)

23-39: ⚠️ Potential issue | 🟠 Major

Preserve conditional branch labels explicitly.

conditionalBranchCounters still assigns conditional_true/conditional_false by encounter order, so reordering sibling steps in YAML can flip the imported true/false path. String(rawDepId) also means depends_on cannot carry explicit branch metadata, and the sourceStep.step.condition guard collapses existing conditional edges to sequential when the condition text is temporarily blank. Please read an explicit edge label from YAML first and keep this inference only as a backward-compatible fallback.

Also applies to: 81-98, 214-229

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` around lines 23 - 39, Update
the YAML->node import logic so it reads and preserves explicit branch labels
from the YAML depends_on entries (rather than assigning
"conditional_true"/"conditional_false" by encounter order): change the handling
around conditionalBranchCounters, String(rawDepId), and
sourceStep.step.condition to first parse an explicit edge label from the
depends_on item (if present) and attach that label to the created edge; only
fall back to inference (assigning true/false by index) when no explicit label
exists, and avoid collapsing conditional edges into sequential edges when
sourceStep.step.condition is temporarily empty by preserving any previously
assigned conditional edge metadata. Ensure this change is applied consistently
in the code paths around conditionalBranchCounters and the blocks referenced
(lines ~23-39, ~81-98, ~214-229) so explicit branch metadata survives reordering
and blank conditions.
web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx (2)

483-500: ⚠️ Potential issue | 🟠 Major

Restore the full parsed builder state when leaving Advanced mode.

parseForBuilder(freeText) already returns negate and subGroups, but Lines 496-497 discard both. Switching Advanced → Builder after editing NOT (...) or grouped conditions will silently strip those parts, and the next builder sync overwrites the parent with a different condition.

Suggested fix
       const flat = parseForBuilder(freeText)
       if (!flat) return
       setEntries(toEntries(flat.comparisons))
       setLogicalOperator(flat.logicalOperator)
-      setNegate(false)
-      setSubGroups([])
+      setNegate(flat.negate)
+      setSubGroups(
+        flat.subGroups.map((sg) => ({
+          key: allocKey(),
+          operator: sg.operator,
+          entries: toEntries(sg.comparisons),
+        })),
+      )

Also add allocKey to this callback’s dependency list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines
483 - 500, handleModeChange drops parsed negate and subGroups when switching
from advanced to builder; update the else branch to use the values returned by
parseForBuilder (e.g., use flat.negate in setNegate and flat.subGroups in
setSubGroups) and preserve grouping/negation instead of resetting them to
false/[]; also include allocKey in the useCallback dependency array so
handleModeChange updates correctly when allocKey changes.

198-210: ⚠️ Potential issue | 🟠 Major

Expose the root AND/OR selector even when there is only one top-level row.

The SegmentedControl is only rendered from ConditionRow, so with one top-level comparison plus any subgroup—or with only subgroups after parsing an existing expression—logicalOperator still drives serialization but there is no visible control for it. Users have to add a throwaway top-level row just to switch ANDOR, which makes grouped expressions awkward to inspect and edit from the normal builder flow.

Also applies to: 525-549

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines
198 - 210, The root AND/OR selector is hidden when there's only one top-level
row because SegmentedControl is only rendered inside ConditionRow gated by
showOperator; move or duplicate the SegmentedControl up into
ConditionExpressionBuilder so the root logicalOperator and onOperatorChange are
always visible for the top-level group (even when there is a single row or only
subgroups), and keep ConditionRow's SegmentedControl for non-root groups (or
remove the showOperator branch there if duplicating). Update props passed into
ConditionRow (logicalOperator, onOperatorChange) or lift rendering to
ConditionExpressionBuilder to ensure the selector controls the same
logicalOperator state used for serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/workflow-editor/condition-expression-types.ts`:
- Around line 45-76: serializeCondition currently emits AND/OR/NOT groups which
the runtime (see condition_eval.py evaluator for boolean literal/truthy lookups
and only ==/!= comparisons) cannot parse; update serializeCondition to only
produce single comparisons and to fail-fast when given compound/group
expressions: detect when expr.kind === 'group' or expr.logicalOperator is set
(and when expr.conditions.length > 1 or logicalOperator !== 'NONE'), and either
throw a clear error or return an explicit validation failure so the
caller/editor cannot save/send unsupported compound conditions; reference the
serializeCondition function and the ConditionExpression shape to locate where to
add this validation/guard.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 61-76: buildExpression currently emits all ConditionComparison
items before all SubGroupEntry items which reorders top-level items; change the
function to preserve original top-level order by accepting and iterating a
single ordered list of root items (e.g. parameter typed as
Array<ConditionComparison | SubGroupEntry> or a discriminated union) rather than
separate comparisons and subGroups, and when encountering a SubGroupEntry
produce either its single comparison or createGroup(group.operator,
group.entries.map(e=>e.comparison)); keep the existing use of createGroup and
LogicalOperator but build items in the passed order so (a AND b) OR c stays in
that order and round-trip parsing/serialization is stable.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 235-245: The edges creation in yaml-to-nodes.ts (the edges.push
block that builds id `edge-${depId}-${stepId}` and the similar block at lines
~259-265) can emit duplicate edges for depends_on/branches/repeated items; fix
by introducing a dedup set (e.g., emittedEdgeIds or emittedEdgeTuples) and check
before pushing: compute a unique key using source, target and edgeType (or
sourceHandle/branch) and skip pushing if the key already exists, otherwise add
the key to the set and then call edges.push; update both the edges.push sites to
use this guard so duplicate relations are not emitted.
- Around line 149-171: Normalize and validate step ids early in the Pass 1 loop
in yaml-to-nodes.ts: trim the raw step.id, treat an empty or whitespace-only
trimmed id as missing (so auto-generate using autoIdCounter), reject reserved
ids like "start-1" and "end-1" by pushing to errors, and check for duplicates on
the normalized id before adding to seenIds/stepMap; apply the same
normalization/validation logic in the other ID handling blocks (the similar code
at ranges 177-183 and 273-282) so reserved and whitespace-only ids are caught
consistently and synthetic nodes won't collide with user ids.
- Around line 149-171: The code currently casts each entry in steps to YamlStep
and auto-creates a task node for non-object YAML values; update the loop in
yaml-to-nodes.ts to reject non-object steps up front by checking the runtime
type of steps[i] (e.g., typeof step === 'object' && step !== null &&
!Array.isArray(step')) before treating it as a YamlStep; when the check fails
push an error or warning (consistent with existing errors/warnings arrays)
referencing the entry index, skip further processing for that entry (do not
increment autoIdCounter, add to seenIds, or call stepMap.set), and only cast to
YamlStep and continue validating type via VALID_TYPES once the runtime shape is
confirmed.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 16-23: The formatDate function uses toLocaleDateString while also
passing time options; change it to use toLocaleString for idiomatic date+time
formatting: update function formatDate(iso: string) to call new
Date(iso).toLocaleString(...) with the same options (month, day, hour, minute)
so the output is produced using the conventional API for combined date and time.

---

Duplicate comments:
In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 483-500: handleModeChange drops parsed negate and subGroups when
switching from advanced to builder; update the else branch to use the values
returned by parseForBuilder (e.g., use flat.negate in setNegate and
flat.subGroups in setSubGroups) and preserve grouping/negation instead of
resetting them to false/[]; also include allocKey in the useCallback dependency
array so handleModeChange updates correctly when allocKey changes.
- Around line 198-210: The root AND/OR selector is hidden when there's only one
top-level row because SegmentedControl is only rendered inside ConditionRow
gated by showOperator; move or duplicate the SegmentedControl up into
ConditionExpressionBuilder so the root logicalOperator and onOperatorChange are
always visible for the top-level group (even when there is a single row or only
subgroups), and keep ConditionRow's SegmentedControl for non-root groups (or
remove the showOperator branch there if duplicating). Update props passed into
ConditionRow (logicalOperator, onOperatorChange) or lift rendering to
ConditionExpressionBuilder to ensure the selector controls the same
logicalOperator state used for serialization.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 23-39: Update the YAML->node import logic so it reads and
preserves explicit branch labels from the YAML depends_on entries (rather than
assigning "conditional_true"/"conditional_false" by encounter order): change the
handling around conditionalBranchCounters, String(rawDepId), and
sourceStep.step.condition to first parse an explicit edge label from the
depends_on item (if present) and attach that label to the created edge; only
fall back to inference (assigning true/false by index) when no explicit label
exists, and avoid collapsing conditional edges into sequential edges when
sourceStep.step.condition is temporarily empty by preserving any previously
assigned conditional edge metadata. Ensure this change is applied consistently
in the code paths around conditionalBranchCounters and the blocks referenced
(lines ~23-39, ~81-98, ~214-229) so explicit branch metadata survives reordering
and blank conditions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e616cb2-9397-460e-a00f-175da39ddac9

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbe190 and badc70e.

📒 Files selected for processing (5)
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-*)) for colors. NEVER hardcode hex values or rgba() in .tsx/.ts files
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly in styles
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets
Use /// <reference types="vite/client" /> to enable side-effect imports for CSS in Storybook 10 with TypeScript 6
Ensure all TypeScript code passes type checking via npm --prefix web run type-check
Bundle size must be auditable via npm --prefix web run analyze (generates stats.html treemap)
Achieve Lighthouse performance audit target of 90+ via npm --prefix web run lighthouse

web/src/**/*.{ts,tsx}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones
Web Dashboard Design System (MANDATORY): See web/CLAUDE.md for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Run ESLint with zero warnings enforced via npm --prefix web run lint

Line length: 88 characters (ruff) for JavaScript/TypeScript files

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{ts,tsx}: Organize page-scoped sub-components in pages/<page-name>/ subdirectories rather than inline in page files
Use lazy-loaded page components in web/src/pages/ with React Router and route guards for auth/setup
Use @AnimatedPresence wrapper for page transitions in React Router (keyed by route)

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
🧠 Learnings (34)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch -- use `<SectionCard>` component

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch; use `<SectionCard>`

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T21:18:15.006Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T21:18:15.006Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web Dashboard Design System (MANDATORY): See `web/CLAUDE.md` for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-31T20:21:35.626Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:21:35.626Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowCard.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-15T21:32:02.880Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🪛 ast-grep (0.42.0)
web/src/pages/workflow-editor/condition-expression-types.ts

[warning] 107-107: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\s+${op}\\s+)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (8)
web/src/pages/workflows/WorkflowCard.tsx (5)

25-51: Card structure uses proper design tokens; consider SectionCard alignment.

The card shell correctly uses tokenized values (border-border, bg-card, p-card, shadow-[var(--so-shadow-card-hover)]), and the StatPill component is appropriately reused.

However, a previous review flagged that card-with-header layouts should use SectionCard to maintain design-system consistency. This card's primary role as a clickable navigation target with an inline title and metadata may justify the custom structure, but please confirm whether SectionCard can accommodate this pattern or if this deviation is intentional.


1-9: Imports are correctly organized and use the specific Radix package.

The import from @radix-ui/react-dropdown-menu (line 2) now follows the recommended pattern, matching WorkflowTableView.tsx and Radix documentation.


11-21: Clean interface and proper URL encoding.

The WorkflowCardProps interface is well-typed, and the editorUrl correctly applies encodeURIComponent to the workflow ID to handle special characters safely.


53-97: Dropdown menu correctly uses design tokens and handles event propagation.

The dropdown content now uses shadow-[var(--so-shadow-card-hover)] (line 72), addressing the previous review comment. Event propagation is properly stopped on the trigger button (lines 57-60) to prevent the underlying Link from activating. Menu items use semantic classes (hover:bg-surface, text-danger) appropriately.


100-111: ConfirmDialog usage is correct and complete.

All required props (open, onOpenChange, onConfirm, title) are provided, along with appropriate optional props (description, variant, confirmLabel). The onOpenChange handler correctly resets state only on close, preventing unnecessary state updates.

web/src/pages/workflows/WorkflowTableView.tsx (3)

58-72: Good keyboard accessibility implementation.

The keyboard handling properly addresses the previous review feedback—rows are now focusable with tabIndex={0}, and the onKeyDown handler correctly ignores events from interactive children. The focus-visible styling with outline-accent maintains design token consistency.


94-122: Dropdown styling follows design tokens correctly.

The dropdown now uses shadow-[var(--so-shadow-card-hover)] for elevation, border-border for borders, and min-w-36 (standard Tailwind) instead of hardcoded pixels—all addressing previous review feedback. Menu item hover/focus states use semantic bg-accent/10 and bg-danger/10 classes appropriately.


132-144: LGTM!

The ConfirmDialog integration is clean—state is properly cleared on both confirm and cancel paths, and the guard on confirmDeleteId prevents calling onDelete with a stale or null value.

Comment on lines +45 to +76
/**
* Serialize a structured condition expression to the string format
* expected by the backend condition evaluator.
*
* Single comparison: "field == value"
* Compound: "field1 == val1 AND field2 != val2"
* Nested groups are wrapped in parentheses:
* "(field1 == val1 AND field2 != val2) OR field3 == val3"
*/
export function serializeCondition(expr: ConditionExpression): string {
if (expr.kind === 'comparison') {
return `${expr.field} ${expr.operator} ${expr.value}`
}

if (expr.conditions.length === 0) return ''

// NOT wraps its single child in parentheses
if (expr.logicalOperator === 'NOT') {
const inner = expr.conditions.map((c) => serializeCondition(c)).join(' AND ')
return `NOT (${inner})`
}

const parts = expr.conditions.map((c) => {
// Nested groups get parenthesized to preserve precedence
if (c.kind === 'group' && c.conditions.length > 1) {
return `(${serializeCondition(c)})`
}
return serializeCondition(c)
})

return parts.join(` ${expr.logicalOperator} `)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not serialize a condition language the runtime cannot evaluate.

serializeCondition() emits AND / OR / NOT groups, but src/synthorg/engine/workflow/condition_eval.py:28-88 only evaluates boolean literals, truthy key lookups, and a single == / != comparison. A value like status == completed AND priority == high will reach runtime as one malformed comparison instead of a logical expression, so conditional nodes can take the wrong branch even though the editor accepts them. This needs a matching backend parser/evaluator before compound conditions ship.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/condition-expression-types.ts` around lines 45
- 76, serializeCondition currently emits AND/OR/NOT groups which the runtime
(see condition_eval.py evaluator for boolean literal/truthy lookups and only
==/!= comparisons) cannot parse; update serializeCondition to only produce
single comparisons and to fail-fast when given compound/group expressions:
detect when expr.kind === 'group' or expr.logicalOperator is set (and when
expr.conditions.length > 1 or logicalOperator !== 'NONE'), and either throw a
clear error or return an explicit validation failure so the caller/editor cannot
save/send unsupported compound conditions; reference the serializeCondition
function and the ConditionExpression shape to locate where to add this
validation/guard.

Comment on lines +61 to +76
/** Build a ConditionExpression from the builder state (comparisons + sub-groups). */
function buildExpression(
comparisons: ConditionComparison[],
logicalOperator: LogicalOperator,
subGroups: SubGroupEntry[] = [],
): ConditionExpression {
const items: ConditionExpression[] = [...comparisons]
for (const group of subGroups) {
if (group.entries.length === 1) {
items.push(group.entries[0]!.comparison)
} else if (group.entries.length > 1) {
items.push(createGroup(group.operator, group.entries.map((e) => e.comparison)))
}
}
if (items.length === 1) return items[0]!
return createGroup(logicalOperator, items)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the original top-level item order.

buildExpression() always serializes every top-level comparison before every subgroup. For an existing value like (a == 1 AND b == 2) OR c == 3, the first builder sync reconstructs c == 3 OR (a == 1 AND b == 2) instead, and Line 384 pushes that reordered text straight back upstream. Even if the boolean result is the same, this breaks parse/serialize round-tripping and can dirty the workflow just by opening the editor. Model the root items as one ordered list instead of splitting them into separate comparison and subgroup buckets.

Also applies to: 379-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines 61
- 76, buildExpression currently emits all ConditionComparison items before all
SubGroupEntry items which reorders top-level items; change the function to
preserve original top-level order by accepting and iterating a single ordered
list of root items (e.g. parameter typed as Array<ConditionComparison |
SubGroupEntry> or a discriminated union) rather than separate comparisons and
subGroups, and when encountering a SubGroupEntry produce either its single
comparison or createGroup(group.operator, group.entries.map(e=>e.comparison));
keep the existing use of createGroup and LogicalOperator but build items in the
passed order so (a AND b) OR c stays in that order and round-trip
parsing/serialization is stable.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
web/src/pages/workflow-editor/yaml-to-nodes.ts (3)

230-232: ⚠️ Potential issue | 🟠 Major

Reject malformed relation fields instead of silently changing topology.

If depends_on or branches is present but not an array, these blocks are skipped. And String(rawDepId) / String(rawTarget) accepts objects and numbers as synthetic ids. That makes malformed YAML look valid and then invents different roots/leaves. Emit parse errors for non-array fields and for non-string/blank items before edge construction.

Suggested fix
-    if (step.depends_on && Array.isArray(step.depends_on)) {
+    if (step.depends_on != null && !Array.isArray(step.depends_on)) {
+      errors.push(`Step '${stepId}' has invalid depends_on; expected an array`)
+    } else if (Array.isArray(step.depends_on)) {
       for (const rawDepId of step.depends_on) {
-        const depId = String(rawDepId)
+        if (typeof rawDepId !== 'string' || rawDepId.trim() === '') {
+          errors.push(`Step '${stepId}' has invalid dependency entry`)
+          continue
+        }
+        const depId = rawDepId.trim()

Apply the same pattern to branches.

Also applies to: 269-271

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` around lines 230 - 232, The
parser currently silently ignores non-array relation fields and coerces relation
items via String(rawDepId)/String(rawTarget), which accepts objects/numbers and
invents ids; update the relation handling in yaml-to-nodes.ts so that when
step.depends_on or step.branches is present but not an Array you emit a parse
error, and before constructing edges validate each item (rawDepId/rawTarget) is
a non-empty string (reject objects, numbers, null, empty strings) and emit a
parse error for invalid items instead of coercing; apply this same validation
logic to both depends_on and branches and perform these checks prior to any edge
construction or String(...) conversion.

246-255: ⚠️ Potential issue | 🟠 Major

The dedupe key and edge.id still use different uniqueness rules.

emittedEdges includes edgeType, but the emitted edge.id does not. If the same source/target pair is declared through both depends_on and branches, both edges survive dedupe and then collide on the same ReactFlow id (edge-A-B). Deduplicate against the actual emitted id, or include the relation type in both places and reject mixed duplicates explicitly.

Suggested fix
-        const edgeKey = `${depId}->${stepId}:${edgeType}`
-        if (emittedEdges.has(edgeKey)) continue
-        emittedEdges.add(edgeKey)
+        const edgeId = `edge-${depId}-${stepId}`
+        if (emittedEdges.has(edgeId)) {
+          errors.push(`Duplicate relation between '${depId}' and '${stepId}'`)
+          continue
+        }
+        emittedEdges.add(edgeId)

         const visualType = edgeTypeToVisualType(edgeType)
@@
         edges.push({
-          id: `edge-${depId}-${stepId}`,
+          id: edgeId,

Mirror the same rule in the branches block.

Also applies to: 277-283

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` around lines 246 - 255, The
dedupe key stored in emittedEdges (edgeKey) includes edgeType while the created
edge id (`id` property in the object pushed in edges within edges.push) does
not, causing collisions when the same source/target is emitted with different
relation types; update the logic so the emitted uniqueness rule matches the id
uniqueness rule: either (A) change the emitted edgeKey to exclude edgeType
(e.g., `${depId}->${stepId}`) so it deduplicates exactly like the generated id,
or (B) include edgeType in the generated edge id (e.g.,
`edge-${depId}-${stepId}-${edgeType}`) and apply the same change in the
`branches` block as well (mirror this fix at the 277-283 area) so emittedEdges,
edgeKey, and the `id` created in edges.push use the same composition and mixed
duplicates are rejected consistently.

23-39: ⚠️ Potential issue | 🟠 Major

Conditional branch semantics are still order-dependent.

Because depends_on only carries bare step ids, Line 95 has no stable metadata to use and assigns conditional_true / conditional_false from traversal order. Reordering dependent steps in YAML can still flip a conditional branch on import, which breaks the bidirectional round-trip guarantee. This schema needs explicit edge metadata here, with the current string form kept only as a backward-compatible fallback.

Also applies to: 90-97, 221-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` around lines 23 - 39,
YamlStep's depends_on currently only carries bare IDs causing
conditional_true/conditional_false assignment to depend on traversal order;
update the schema to allow explicit edge metadata (e.g., depends_on: Array<{ id:
string; condition?: 'true'|'false'|string; meta?: Record<string,any> }>) while
keeping the existing string[] form as a fallback, then modify the YAML->node
conversion logic (the function that maps YamlStep to graph nodes and that
currently sets conditional_true/conditional_false) to: 1) prefer the new object
form and use its condition/meta to deterministically assign
conditional_true/conditional_false edges, 2) if an entry is a string, preserve
current behavior for backward compatibility, and 3) ensure the export path emits
the explicit edge metadata when present so round-tripping remains stable.
web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx (1)

61-76: ⚠️ Potential issue | 🟠 Major

Preserve the original top-level condition order.

buildExpression() still serializes every root comparison before every subgroup, so a value like (a == 1 AND b == 2) OR c == 3 round-trips as c == 3 OR (a == 1 AND b == 2). Because Line 384 pushes that serialization upstream immediately, simply opening the builder can dirty the workflow without a user edit. The root builder state needs one ordered collection instead of separate comparisons / subGroups buckets.

Also applies to: 379-385

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines 61
- 76, buildExpression currently takes separate comparisons and subGroups which
loses root ordering; change it to accept a single ordered root collection (e.g.,
rootItems: Array<{kind: 'comparison'|'group', comparison?: ConditionComparison,
group?: SubGroupEntry}> or simply (ConditionComparison|SubGroupEntry)[]) so the
original top-level order is preserved, then iterate rootItems in order and for
each item: if it's a ConditionComparison push it to items, if it's a
SubGroupEntry push either its single comparison or createGroup(group.operator,
group.entries.map(e => e.comparison)); finally return the single item if
items.length === 1 else return createGroup(logicalOperator, items). Ensure you
update all call sites of buildExpression (and the parameter names) accordingly
to pass the ordered root collection instead of separate comparisons/subGroups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 585-592: Replace the raw <input> in ConditionExpressionBuilder
with the shared InputField component: import InputField, use value={freeText}
and call handleFreeTextChange with the new value from InputField's
onChange/onValueChange (adapt to the component's prop signature), and pass the
existing placeholder, aria-label, className/styles and type="text" so styling
and form behavior remain identical; ensure you remove the raw input and add the
InputField import and wire up freeText and handleFreeTextChange accordingly.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 177-180: The parser currently treats unknown step types as
warnings so typos (e.g., "condtional") allow apply to proceed; change the
handling so unknown step types are recorded as hard parse errors that abort
apply: in the block using stepType and VALID_TYPES, replace the warnings.push
call with pushing a descriptive error into the same errors collection that
WorkflowYamlEditor checks (result.errors), e.g., add to errors (or
result.errors) with a message referencing the stepId and stepType and then
continue; ensure you use the existing symbols stepType, VALID_TYPES,
warnings/errors and that result.errors will be non-empty for unknown types.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 70-71: In WorkflowTableView remove role="link" from the <tr>
(which overrides table semantics) and instead make the primary cell a real link:
wrap the clickable content for workflow w.name in an anchor inside the first
<td> (give it the existing aria-label={`Open workflow ${w.name}`} and ensure it
visually fills the cell via CSS/display so the row still appears clickable),
move any onKeyDown/onClick keyboard handlers to that anchor, and keep other
accessibility attributes (tabIndex, aria-selected, etc.) on the row only as
needed for selection not as a link.

---

Duplicate comments:
In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 61-76: buildExpression currently takes separate comparisons and
subGroups which loses root ordering; change it to accept a single ordered root
collection (e.g., rootItems: Array<{kind: 'comparison'|'group', comparison?:
ConditionComparison, group?: SubGroupEntry}> or simply
(ConditionComparison|SubGroupEntry)[]) so the original top-level order is
preserved, then iterate rootItems in order and for each item: if it's a
ConditionComparison push it to items, if it's a SubGroupEntry push either its
single comparison or createGroup(group.operator, group.entries.map(e =>
e.comparison)); finally return the single item if items.length === 1 else return
createGroup(logicalOperator, items). Ensure you update all call sites of
buildExpression (and the parameter names) accordingly to pass the ordered root
collection instead of separate comparisons/subGroups.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 230-232: The parser currently silently ignores non-array relation
fields and coerces relation items via String(rawDepId)/String(rawTarget), which
accepts objects/numbers and invents ids; update the relation handling in
yaml-to-nodes.ts so that when step.depends_on or step.branches is present but
not an Array you emit a parse error, and before constructing edges validate each
item (rawDepId/rawTarget) is a non-empty string (reject objects, numbers, null,
empty strings) and emit a parse error for invalid items instead of coercing;
apply this same validation logic to both depends_on and branches and perform
these checks prior to any edge construction or String(...) conversion.
- Around line 246-255: The dedupe key stored in emittedEdges (edgeKey) includes
edgeType while the created edge id (`id` property in the object pushed in edges
within edges.push) does not, causing collisions when the same source/target is
emitted with different relation types; update the logic so the emitted
uniqueness rule matches the id uniqueness rule: either (A) change the emitted
edgeKey to exclude edgeType (e.g., `${depId}->${stepId}`) so it deduplicates
exactly like the generated id, or (B) include edgeType in the generated edge id
(e.g., `edge-${depId}-${stepId}-${edgeType}`) and apply the same change in the
`branches` block as well (mirror this fix at the 277-283 area) so emittedEdges,
edgeKey, and the `id` created in edges.push use the same composition and mixed
duplicates are rejected consistently.
- Around line 23-39: YamlStep's depends_on currently only carries bare IDs
causing conditional_true/conditional_false assignment to depend on traversal
order; update the schema to allow explicit edge metadata (e.g., depends_on:
Array<{ id: string; condition?: 'true'|'false'|string; meta?: Record<string,any>
}>) while keeping the existing string[] form as a fallback, then modify the
YAML->node conversion logic (the function that maps YamlStep to graph nodes and
that currently sets conditional_true/conditional_false) to: 1) prefer the new
object form and use its condition/meta to deterministically assign
conditional_true/conditional_false edges, 2) if an entry is a string, preserve
current behavior for backward compatibility, and 3) ensure the export path emits
the explicit edge metadata when present so round-tripping remains stable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 43e2fdd2-4e8c-4f35-a6b6-de27a22297f1

📥 Commits

Reviewing files that changed from the base of the PR and between badc70e and b806069.

📒 Files selected for processing (3)
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-*)) for colors. NEVER hardcode hex values or rgba() in .tsx/.ts files
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly in styles
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets
Use /// <reference types="vite/client" /> to enable side-effect imports for CSS in Storybook 10 with TypeScript 6
Ensure all TypeScript code passes type checking via npm --prefix web run type-check
Bundle size must be auditable via npm --prefix web run analyze (generates stats.html treemap)
Achieve Lighthouse performance audit target of 90+ via npm --prefix web run lighthouse

web/src/**/*.{ts,tsx}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones
Web Dashboard Design System (MANDATORY): See web/CLAUDE.md for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Run ESLint with zero warnings enforced via npm --prefix web run lint

Line length: 88 characters (ruff) for JavaScript/TypeScript files

Files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{ts,tsx}: Organize page-scoped sub-components in pages/<page-name>/ subdirectories rather than inline in page files
Use lazy-loaded page components in web/src/pages/ with React Router and route guards for auth/setup
Use @AnimatedPresence wrapper for page transitions in React Router (keyed by route)

Files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🧠 Learnings (26)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-15T21:32:02.880Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🔇 Additional comments (6)
web/src/pages/workflows/WorkflowTableView.tsx (5)

1-14: Clean imports and type definitions.

Imports are minimal and properly aliased. The WorkflowTableViewProps interface correctly uses readonly WorkflowDefinition[] for immutability, which aligns with the type definition in web/src/api/types.ts where nodes and edges are required readonly arrays.


16-23: Date formatting implementation looks good.

Using toLocaleString with month/day/hour/minute options is the idiomatic approach for combined date+time formatting. Defining the helper outside the component avoids recreation on each render.


39-72: Well-implemented accessible table rows.

The table row implementation correctly addresses keyboard accessibility:

  • tabIndex={0} makes rows focusable
  • onKeyDown handler activates on Enter/Space while properly ignoring events from interactive children
  • focus-visible:outline provides visual focus indication
  • aria-label announces the workflow name

All styling uses semantic Tailwind classes (border-border, bg-muted/30, text-foreground, focus-visible:outline-accent), complying with the design token guidelines.


83-124: Dropdown menu implementation follows design guidelines.

The dropdown correctly:

  • Uses e.stopPropagation() to prevent row navigation
  • Applies token shadow via shadow-[var(--so-shadow-card-hover)]
  • Uses standard Tailwind width min-w-36 instead of hardcoded pixels
  • Uses semantic classes for colors (border-border, bg-popover, text-foreground, text-danger, bg-accent/10, bg-danger/10)

Previous review concerns about hardcoded shadow and pixel values have been properly addressed.


132-143: ConfirmDialog integration is clean.

Using the workflow ID as state (confirmDeleteId) rather than a boolean + separate reference is a good pattern that prevents stale closure issues. The onOpenChange handler correctly clears state when the dialog closes, and the defensive if (confirmDeleteId) check in onConfirm guards against edge cases.

web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx (1)

157-165: No action required. The concern about form submission is not applicable in this context.

The Button components at lines 157-165, 258-265, 278-285, and 574-581 are rendered inside a Drawer modal component, not a form element. Since WorkflowNodeDrawer does not wrap its content in a <form> tag, these buttons cannot unexpectedly submit a form, regardless of whether Button sets type="button" internally.

The buttons do not have explicit type="button" attributes, but this is a moot concern in a modal dialog context.

@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
web/src/pages/workflow-editor/yaml-to-nodes.ts (2)

90-98: ⚠️ Potential issue | 🟠 Major

Do not derive conditional polarity from visitation order or condition truthiness.

branchIndex is consumed in dependent visitation order, so moving the false successor above the true successor flips the imported handles. The extra sourceStep.step.condition check also downgrades a conditional node with a blank expression to sequential edges, even though web/src/pages/workflow-editor/ConditionalNode.tsx:15-17 still treats it as conditional. Please persist explicit branch metadata in YAML and key the fallback off sourceStep.type, not condition truthiness.

Also applies to: 247-252


230-230: ⚠️ Potential issue | 🟠 Major

Raise a parse error when depends_on or branches is not an array.

These guards silently skip malformed relation fields. YAML like depends_on: foo or branches: 1 therefore changes roots/leaves with no inline error, and web/src/pages/workflow-editor/WorkflowYamlEditor.tsx:49-96 will still apply it because result.errors stays empty.

Suggested fix
-    if (step.depends_on && Array.isArray(step.depends_on)) {
+    if (step.depends_on != null && !Array.isArray(step.depends_on)) {
+      errors.push(`Step '${stepId}' has invalid depends_on; expected an array`)
+    } else if (Array.isArray(step.depends_on)) {
@@
-    if (step.branches && Array.isArray(step.branches)) {
+    if (step.branches != null && !Array.isArray(step.branches)) {
+      errors.push(`Step '${stepId}' has invalid branches; expected an array`)
+    } else if (Array.isArray(step.branches)) {

Also applies to: 277-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts` at line 230, The code
currently ignores malformed relation fields like step.depends_on and
step.branches when they are not arrays; update the parser in yaml-to-nodes.ts
(the code handling each step/step parsing where you see "if (step.depends_on &&
Array.isArray(step.depends_on))" and the similar branches check around line 277)
to detect non-array values and surface a parse error instead of silently
skipping: when a step has depends_on or branches defined but not
Array.isArray(...) push a descriptive error into the parser result (e.g.,
result.errors.push({message: `Field "depends_on" must be an array on step
"${step.name || step.id}"`}) or throw a specific ParseError), include the step
identifier in the message, and skip further relation processing for that step to
avoid silently changing roots/leaves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx`:
- Around line 343-349: The long ternary assignment to currentSerialized in
ConditionExpressionBuilder.tsx exceeds the 88-char line limit; split the
assignment into multiple shorter statements or use a small if/else so the call
to serializeCondition(buildExpression(currentComparisons, logicalOperator,
subGroups)) and the freeText branch are on separate lines, keeping the
subsequent negate wrapping logic intact (keep the negate check: if (mode ===
'builder' && negate && currentSerialized) currentSerialized = `NOT
(${currentSerialized})`).

In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Around line 153-155: The error message uses typeof raw which reports arrays as
"object"; update the diagnostic to compute an actualType before pushing the
error (e.g., const actualType = raw === null ? 'null' : Array.isArray(raw) ?
'array' : typeof raw) and use actualType in the errors.push string for the
validation inside the loop that checks raw (the block containing
Array.isArray(raw) and errors.push).
- Around line 159-165: If a step.id exists but is not a string, treat that as an
error (do not fall through to auto-generate an id); change the logic around
rawId/stepId so you only auto-generate using autoIdCounter when the id key is
absent or present as an empty/whitespace string, and when step.id exists but
typeof step.id !== 'string' push an error into the errors collection (not
warnings) describing "Step N has non-string id" so the editor blocks the
malformed YAML from overwriting the canvas; update handling of rawId, stepId,
warnings and errors accordingly.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Line 80: Remove the stale click-suppression from the actions cell in the
WorkflowTableView component: delete the onClick={(e) => e.stopPropagation()}
attribute on the <td className="px-2 py-2.5"> actions cell so the row click
behavior is not implicitly blocked; update any related JSX in
WorkflowTableView.tsx and run the UI to confirm row clicks still work and no
linter/unused-variable issues are introduced.

---

Duplicate comments:
In `@web/src/pages/workflow-editor/yaml-to-nodes.ts`:
- Line 230: The code currently ignores malformed relation fields like
step.depends_on and step.branches when they are not arrays; update the parser in
yaml-to-nodes.ts (the code handling each step/step parsing where you see "if
(step.depends_on && Array.isArray(step.depends_on))" and the similar branches
check around line 277) to detect non-array values and surface a parse error
instead of silently skipping: when a step has depends_on or branches defined but
not Array.isArray(...) push a descriptive error into the parser result (e.g.,
result.errors.push({message: `Field "depends_on" must be an array on step
"${step.name || step.id}"`}) or throw a specific ParseError), include the step
identifier in the message, and skip further relation processing for that step to
avoid silently changing roots/leaves.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49383c00-e199-4ff7-b315-b3f33663b93b

📥 Commits

Reviewing files that changed from the base of the PR and between b806069 and 5789a5f.

📒 Files selected for processing (3)
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowTableView.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-*)) for colors. NEVER hardcode hex values or rgba() in .tsx/.ts files
Use font-sans or font-mono for typography (maps to Geist tokens). NEVER set fontFamily directly in styles
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT hardcode Framer Motion transition durations -- use @/lib/motion presets
Use /// <reference types="vite/client" /> to enable side-effect imports for CSS in Storybook 10 with TypeScript 6
Ensure all TypeScript code passes type checking via npm --prefix web run type-check
Bundle size must be auditable via npm --prefix web run analyze (generates stats.html treemap)
Achieve Lighthouse performance audit target of 90+ via npm --prefix web run lighthouse

web/src/**/*.{ts,tsx}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones
Web Dashboard Design System (MANDATORY): See web/CLAUDE.md for the full component inventory, design token rules, and post-training references (TS6, Storybook 10).

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

Run ESLint with zero warnings enforced via npm --prefix web run lint

Line length: 88 characters (ruff) for JavaScript/TypeScript files

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{ts,tsx}: Organize page-scoped sub-components in pages/<page-name>/ subdirectories rather than inline in page files
Use lazy-loaded page components in web/src/pages/ with React Router and route guards for auth/setup
Use @AnimatedPresence wrapper for page transitions in React Router (keyed by route)

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🧠 Learnings (33)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/pages/workflows/WorkflowTableView.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-01T05:46:17.064Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T05:46:17.064Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-03-15T21:32:02.880Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:32:02.880Z
Learning: Applies to .github/workflows/*.yml : Dependabot: daily updates for uv + github-actions + npm + pre-commit + docker + gomod, grouped minor/patch, no auto-merge. Use `/review-dep-pr` to review Dependabot PRs before merging.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-03T21:18:15.006Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T21:18:15.006Z
Learning: When review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes), fix them all. No deferring, no "out of scope" skipping.

Applied to files:

  • web/src/pages/workflow-editor/yaml-to-nodes.ts
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map()—extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()`; extract to a shared component

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-03-31T20:21:35.626Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:21:35.626Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
📚 Learning: 2026-04-03T18:36:51.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-03T18:36:51.730Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Place new shared components in `web/src/components/ui/` with descriptive kebab-case filenames

Applied to files:

  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
🔇 Additional comments (8)
web/src/pages/workflows/WorkflowTableView.tsx (1)

92-96: Good use of design tokens in the dropdown panel.

The dropdown content styling is aligned with shared design-system tokens (border-border, shadow-[var(--so-shadow-card-hover)]).

As per coding guidelines, "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders."

web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx (6)

1-53: LGTM!

Imports are well-organized, reusing shared UI components from the design system. The ComparisonEntry and SubGroupEntry types correctly include stable keys for React list rendering.


62-78: Top-level item ordering is still not preserved.

buildExpression() concatenates all comparisons before all subGroups, which reorders expressions. For (a == 1 AND b == 2) OR c == 3, the builder reconstructs c == 3 OR (a == 1 AND b == 2). This breaks parse/serialize round-tripping and dirties the workflow on open.

Consider modeling root items as a single ordered list (e.g., Array<{ type: 'comparison' | 'group', ... }>) so the original order is preserved during serialization.

[duplicate_comment, raise_major_issue]


92-170: LGTM!

The ComparisonRow component correctly uses SelectField for the operator dropdown (addressing past feedback), and the raw <input> elements with datalists are an acceptable trade-off given that InputField doesn't support the list attribute. Semantic Tailwind classes (border-border, bg-surface, text-foreground) are used appropriately.


186-289: LGTM!

Good extraction of ConditionRow and ConditionGroupPanel as sub-components, addressing past feedback about complex JSX in .map() callbacks. The components properly reuse Button, SegmentedControl, and ComparisonRow.


484-509: LGTM!

The handleModeChange callback correctly blocks switching to Builder mode when parseForBuilder(freeText) returns null (addressing past feedback). The subgroup state is properly restored from the parsed result.


511-597: LGTM!

The render section properly uses shared UI components (SegmentedControl, ToggleField, Button, InputField), and the extracted ConditionRow and ConditionGroupPanel components keep the .map() callbacks clean. The defensive logicalOperator === 'NOT' ? 'AND' : logicalOperator fallback handles potential type mismatches gracefully.

web/src/pages/workflow-editor/yaml-to-nodes.ts (1)

356-375: Config mapping matches the consuming node components.

The field names here line up with web/src/pages/workflow-editor/TaskNode.tsx:19-24, web/src/pages/workflow-editor/ConditionalNode.tsx:15-17, and web/src/pages/workflow-editor/AgentAssignmentNode.tsx:15-17, so the round-tripped nodes keep the config those components actually read.

Comment on lines +343 to +349
let currentSerialized =
mode === 'builder'
? serializeCondition(buildExpression(currentComparisons, logicalOperator, subGroups))
: freeText
if (mode === 'builder' && negate && currentSerialized) {
currentSerialized = `NOT (${currentSerialized})`
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Line exceeds 88-character limit.

Line 345 is approximately 95 characters with indentation, exceeding the 88-character guideline for TypeScript files.

♻️ Proposed fix
     const currentComparisons = entries.map((e) => e.comparison)
+    const builderExpr = buildExpression(
+      currentComparisons,
+      logicalOperator,
+      subGroups,
+    )
     let currentSerialized =
-      mode === 'builder'
-        ? serializeCondition(buildExpression(currentComparisons, logicalOperator, subGroups))
-        : freeText
+      mode === 'builder' ? serializeCondition(builderExpr) : freeText
     if (mode === 'builder' && negate && currentSerialized) {
       currentSerialized = `NOT (${currentSerialized})`
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx` around lines
343 - 349, The long ternary assignment to currentSerialized in
ConditionExpressionBuilder.tsx exceeds the 88-char line limit; split the
assignment into multiple shorter statements or use a small if/else so the call
to serializeCondition(buildExpression(currentComparisons, logicalOperator,
subGroups)) and the freeText branch are on separate lines, keeping the
subsequent negate wrapping logic intact (keep the negate check: if (mode ===
'builder' && negate && currentSerialized) currentSerialized = `NOT
(${currentSerialized})`).

…tion, accessibility

- Replace raw <input> with InputField in ConditionExpressionBuilder advanced mode
- Promote unknown step types from warning to error in yaml-to-nodes (blocks apply)
- Validate depends_on/branches items are strings, reject objects/null/empty
- Include edgeType in generated edge IDs to prevent collisions across edge types
- Replace role="link" on table rows with real <Link> in first cell (proper table semantics)
- Remove row-level onClick/onKeyDown -- navigation via Link element
…n, cleanup

- Fix typeof raw error message to distinguish null/array/primitive
- Error on non-string step.id instead of silently auto-generating
- Error on non-array depends_on/branches instead of silently skipping
- Remove stale onClick stopPropagation on table actions cell
- buildConfig validates string/number types before copying to node config
- data.label only accepts string titles, falls back to stepId
- Prevents YAML type coercion (arrays, objects, booleans) from silently
  corrupting the canvas
…ing, tests

- Fix engine.md observer signature: Coroutine -> Awaitable[None]
- Gate WorkflowNodeDrawer on visual mode (unmount in YAML mode)
- Add role=alert aria-live=assertive on WorkflowCreateDrawer submitError
- Allow removing last builder entry when sub-groups exist
- Log before ValueError in _update_node_status and parse_task_config
- Fake repo list_by_definition/list_by_status return deep copies
- Parametrize TestTaskLinkedStatusOnNonTaskNodes (4 tests -> 1 parametrized)
@Aureliolo Aureliolo force-pushed the feat/workflow-execution-lifecycle branch from 4c0491c to da2697b Compare April 4, 2026 09:00
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 4, 2026 09:02 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (8)
web/src/pages/workflows/WorkflowFilters.tsx (1)

7-17: 🧹 Nitpick | 🔵 Trivial

Consider extracting WORKFLOW_TYPES to a shared module.

This constant is duplicated between this file and WorkflowCreateDrawer.tsx. While not blocking, extracting to a shared location (e.g., @/utils/constants.ts) would ensure consistency and simplify maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowFilters.tsx` around lines 7 - 17,
WORKFLOW_TYPES is duplicated between WorkflowFilters.tsx and
WorkflowCreateDrawer.tsx; extract it into a shared constant (e.g., export const
WORKFLOW_TYPES) in a common module like utils/constants, then import it into
both files and rebuild WORKFLOW_TYPE_OPTIONS in WorkflowFilters.tsx using the
imported WORKFLOW_TYPES (keeping formatLabel and WORKFLOW_TYPE_OPTIONS logic
unchanged) so both components read the single source of truth.
src/synthorg/engine/workflow/execution_observer.py (1)

9-24: 🛠️ Refactor suggestion | 🟠 Major

Add the required module logger.

This new business-logic module still omits the mandated get_logger / logger = get_logger(__name__) setup used across src/synthorg, so it does not meet the observability convention yet.

♻️ Minimal fix
 from typing import TYPE_CHECKING
 
+from synthorg.observability import get_logger
 from synthorg.engine.workflow.execution_service import (
     WorkflowExecutionService,
 )
@@
+logger = get_logger(__name__)
+
 
 class WorkflowExecutionObserver:

As per coding guidelines: "Every module with business logic must import from synthorg.observability import get_logger and set logger = get_logger(__name__)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_observer.py` around lines 9 - 24, This
module is missing the standardized module logger; import get_logger from
synthorg.observability and add logger = get_logger(__name__) near the top of
execution_observer.py (alongside the existing TYPE_CHECKING block and imports)
so any functions or classes in this file (e.g., WorkflowExecutionService usage)
can use logger per project convention.
src/synthorg/engine/task_engine.py (1)

703-714: ⚠️ Potential issue | 🟠 Major

Keep observer delivery off the single-writer hot path.

_process_one() now awaits every registered observer after a successful mutation. One slow repository-backed observer will head-of-line block unrelated task mutations, grow queue pressure, and lengthen shutdown drains even though observer delivery is best-effort. Hand these events off to a dedicated background dispatcher/queue instead of awaiting them inline here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/task_engine.py` around lines 703 - 714, The code
currently awaits _notify_observers(event) inline inside _process_one(), which
blocks the single-writer path; instead enqueue observer delivery to a background
dispatcher and do not await it here. Modify the code so _process_one() still
awaits publish_snapshot(...) but replaces awaiting _notify_observers(event) with
handing the event to a dedicated background queue/dispatcher (e.g., call a
non-blocking method like _enqueue_observer_event(event) or
asyncio.create_task(self._observer_dispatcher.enqueue(event))). Implement a
background worker (start/stop lifecycle methods such as
_start_observer_dispatcher and _stop_observer_dispatcher) that consumes the
asyncio.Queue and calls _notify_observers(event) there, and ensure graceful
shutdown drains the queue before stop; keep references to _observers,
_message_bus, _SNAPSHOT_SENDER, and _SNAPSHOT_CHANNEL when locating where to
integrate the dispatcher.
src/synthorg/engine/workflow/execution_models.py (1)

63-75: ⚠️ Potential issue | 🟠 Major

Reject generic terminal control-node statuses on TASK nodes.

This validator now covers the new TASK_* states, but the inverse invalid state still gets through: a WorkflowNodeType.TASK with status=COMPLETED and no task_id passes. That weakens the new TASK_COMPLETED / TASK_FAILED split and can bypass lifecycle checks keyed off those statuses. Please reject generic control-node terminal statuses for TASK nodes here and add a regression test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_models.py` around lines 63 - 75, The
validator must also reject generic control-node terminal statuses on TASK nodes;
update the check in the validation block around
self.status/self.node_type/_TASK_LINKED_STATUSES so that if self.node_type is
WorkflowNodeType.TASK and self.status is one of the generic terminal
control-node statuses (e.g., COMPLETED/FAILED), raise ValueError requiring a
TASK_* status (and/or a task_id) instead of allowing no task_id; additionally
add a regression test that constructs a WorkflowNode with
node_type=WorkflowNodeType.TASK and status=COMPLETED (and no task_id) and
asserts validation fails.
src/synthorg/engine/workflow/execution_activation_helpers.py (4)

262-266: 🧹 Nitpick | 🔵 Trivial

Use ERROR level for validation failures that raise exceptions.

The function logs at WARNING before raising ValueError, but the coding guidelines specify "All error paths must log at WARNING or ERROR with context before raising." For validation failures that abort execution, ERROR is more appropriate than WARNING.

🔧 Suggested fix (applies to all three validation blocks)
-        logger.warning(
+        logger.error(
             WORKFLOW_EXEC_TASK_CREATED,
             node_id=nid,
             error=msg,
         )

Also applies to lines 276-280 and 290-294.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
262 - 266, The log call currently uses logger.warning when emitting
WORKFLOW_EXEC_TASK_CREATED with node_id=nid and error=msg right before raising a
ValueError; change these to logger.error for all three validation blocks (the
occurrences at the shown block and the ones around lines with similar
logger.warning + raise ValueError) so validation failures that abort execution
are logged at ERROR level while preserving the same context fields (node_id and
error).

221-234: ⚠️ Potential issue | 🟠 Major

Missing conditional edges should fail closed.

When true_target or false_target is None, the code logs a warning but continues. If the selected branch is absent, taken becomes None, find_skipped_nodes is never called (line 231 guard), and the execution proceeds with potentially incorrect routing.

🛡️ Suggested fix
     if true_target is None or false_target is None:
-        logger.warning(
-            WORKFLOW_EXEC_CONDITION_EVALUATED,
+        msg = f"Conditional node {nid!r} missing required edges (true={true_target}, false={false_target})"
+        logger.error(
+            WORKFLOW_EXEC_CONDITION_EVAL_FAILED,
             execution_id=execution_id,
             node_id=nid,
-            note="conditional node missing true or false edge",
-            true_target=true_target,
-            false_target=false_target,
+            error=msg,
         )
+        raise WorkflowConditionEvalError(msg)

As per coding guidelines, "Validate at system boundaries (user input, external APIs, config files)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
221 - 234, The conditional node currently only logs a warning when true_target
or false_target is None, allowing execution to continue incorrectly; change this
to fail closed by replacing the warning-only behavior with an error and abort:
in the block that checks "if true_target is None or false_target is None", call
logger.error (including WORKFLOW_EXEC_CONDITION_EVALUATED, execution_id and nid,
and the missing targets) and then raise an appropriate exception (e.g.,
ValueError or a WorkflowExecutionError) so execution stops instead of
proceeding; keep the existing guard around find_skipped_nodes (untaken/taken)
but ensure the raised exception prevents routing when a branch is missing.

185-196: ⚠️ Potential issue | 🟡 Minor

Use PEP 758 exception syntax without parentheses.

The parenthesized form violates the project's Python 3.14 style enforced by ruff.

🔧 Suggested fix
-    except (ValueError, TypeError, KeyError) as exc:
+    except ValueError, TypeError, KeyError as exc:

As per coding guidelines, "Use PEP 758 except syntax: except A, B: (no parentheses) — ruff enforces this on Python 3.14."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
185 - 196, Replace the parenthesized multi-except clause with PEP 758's
unparenthesized form in the try/except that calls evaluate_condition: change the
current "except (ValueError, TypeError, KeyError) as exc:" to the new syntax
"except ValueError, TypeError, KeyError as exc:" while keeping the
logger.exception(...) call and the subsequent raise of
WorkflowConditionEvalError (using execution_id, nid, expr, and exc as before).

175-184: ⚠️ Potential issue | 🟠 Major

Missing condition_expression should fail closed, not default to false.

A missing or blank expression silently takes the false branch, which can produce different runtime semantics than the workflow author intended. This masks configuration errors that should be caught at activation time.

🛡️ Suggested fix
-    expr = str(node.config.get("condition_expression", "false"))
-    if not node.config.get("condition_expression"):
-        logger.warning(
-            WORKFLOW_EXEC_CONDITION_EVALUATED,
-            execution_id=execution_id,
-            node_id=nid,
-            expression=expr,
-            result=False,
-            note="missing condition_expression, defaulting to false",
-        )
+    raw_expr = node.config.get("condition_expression")
+    if not isinstance(raw_expr, str) or not raw_expr.strip():
+        msg = f"Conditional node {nid!r} is missing condition_expression"
+        logger.error(
+            WORKFLOW_EXEC_CONDITION_EVAL_FAILED,
+            execution_id=execution_id,
+            node_id=nid,
+            error=msg,
+        )
+        raise WorkflowConditionEvalError(msg)
+    expr = raw_expr

As per coding guidelines, "Validate at system boundaries (user input, external APIs, config files)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py` around lines
175 - 184, The code currently treats a missing/blank
node.config["condition_expression"] as false and only logs a warning; instead,
when condition_expression is absent or empty you must fail closed: replace the
warning branch in execution_activation_helpers.py (the block that reads expr =
str(node.config.get("condition_expression", "false")) and checks
node.config.get("condition_expression")) with logic that logs an error via
logger.error using WORKFLOW_EXEC_CONDITION_EVALUATED (include execution_id,
node_id/nid, expression/expr and a clear note), and then raise an
activation/validation exception to abort activation (use the project’s
WorkflowActivationError or a clear ValueError if no specific exception exists)
so missing/blank expressions are rejected at activation time rather than
silently defaulted to false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/engine.md`:
- Around line 424-431: Update the description of task-to-workflow transitions in
handle_task_state_changed to explicitly state that CANCELLED tasks count as
failing the workflow: when any task is FAILED or CANCELLED the overall
WorkflowNodeExecution should transition to FAILED (not just on task failures),
while all tasks done still yields COMPLETED; keep the note that the node status
update and execution transition are persisted in a single repository save to
avoid inconsistent intermediate states.

In `@src/synthorg/engine/task_engine_events.py`:
- Around line 43-48: The code sets effective_task_id = task_id or "unknown"
without logging when both task_id and result.task are None; add a DEBUG log
right before assigning effective_task_id that records the fallback case
including reason, mutation identifier if available, and any relevant result
state so you can correlate events; update the block around getattr(mutation,
"reason", None), getattr(mutation, "task_id", None), the conditional checking
result.task, and the effective_task_id assignment to emit process/logger.debug
(or the module logger) when task_id is None and result.task is None, including
the reason and a short description like "falling back to unknown task_id".

In `@web/src/pages/workflow-editor/condition-expression-types.ts`:
- Around line 54-76: serializeCondition currently emits AND/OR/NOT compound
strings (e.g. "a == b AND c != d") which the backend's evaluate_condition cannot
parse; change serializeCondition/ConditionExpression handling so it only emits
single comparisons, boolean literals, or single-key lookups (never "AND", "OR",
"NOT"). Concretely, remove the branches that join multiple expr.conditions or
wrap with NOT in serializeCondition and instead: for 'group' kinds return the
serialized value of only the single child (or empty string if no children), for
'comparison' return the existing `${expr.field} ${expr.operator} ${expr.value}`,
and ensure any code creating ConditionExpression never produces logicalOperator
values other than a neutral/single-child form; reference serializeCondition and
ConditionExpression here and verify behavior against the backend
evaluate_condition in condition_eval.py.

In `@web/src/pages/workflow-editor/copy-paste.ts`:
- Around line 66-71: The fallback nullish coalesces on clipboard.edges mapping
(idMap.get(edge.source) ?? edge.source and similar for target) is unreachable
and may hide bugs; update the mapping in copy-paste.ts to assert the invariant
instead of silently falling back: require that idMap has entries for edge.source
and edge.target (e.g., throw or use a runtime assert when idMap.get(...) is
undefined) so invalid ClipboardData surfaces immediately, and document this
invariant where copyNodes/clipboard.edges are constructed.

In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 232-246: The node/edge mapping in WorkflowEditorPage.tsx (the
nodeData and edgeData creation blocks) relies on unchecked type assertions like
(n.data as Record<string, unknown>)?.nodeType as string which can mask shape
errors; update these mappings to perform runtime checks (e.g., verify typeof
n.data === 'object' && n.data !== null, then typeof nodeType === 'string' etc.),
supply explicit fallbacks when checks fail (default 'task' for node type, id for
label, {} for config, 'sequential' or null for edge fields), or extract
validation into a small helper (e.g., safeGetNodeData or isRecord) and use it in
the nodeData/edgeData mappings to avoid silent failures.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 55-124: The map's large JSX in WorkflowTableView should be
extracted into a new WorkflowTableRow component: create WorkflowTableRow({
workflow, onEdit, onDuplicate, onDelete }) and move the <tr>..</tr> JSX there,
keeping usages of ROUTES.WORKFLOW_EDITOR, encodeURIComponent(workflow.id),
MoreHorizontal/Pencil/Copy/Trash2 icons, and formatDate(workflow.updated_at). In
WorkflowTableView replace the inline map body with <WorkflowTableRow key={w.id}
workflow={w} onEdit={(id) =>
navigate(`${ROUTES.WORKFLOW_EDITOR}?id=${encodeURIComponent(id)}`)}
onDuplicate={onDuplicate} onDelete={setConfirmDeleteId} /> ensuring the
DropdownMenu item handlers call onEdit/onDuplicate/onDelete and all aria-labels,
classname and other props remain unchanged.

In `@web/src/stores/workflows.ts`:
- Around line 94-96: The updateFromWsEvent handler in useWorkflowsStore
currently calls fetchWorkflows() on every WebSocket event; wrap or replace that
direct call with a debounced invoker so rapid successive events only trigger one
API fetch within a short window. Implement a debounce (e.g., lodash.debounce or
a simple setTimeout with cancellation) associated with updateFromWsEvent that
calls useWorkflowsStore.getState().fetchWorkflows(), and ensure the debounced
function is created once (module scope or store init) so repeated events share
the same timer; update the updateFromWsEvent to call the debounced function
instead of invoking fetchWorkflows directly.

---

Duplicate comments:
In `@src/synthorg/engine/task_engine.py`:
- Around line 703-714: The code currently awaits _notify_observers(event) inline
inside _process_one(), which blocks the single-writer path; instead enqueue
observer delivery to a background dispatcher and do not await it here. Modify
the code so _process_one() still awaits publish_snapshot(...) but replaces
awaiting _notify_observers(event) with handing the event to a dedicated
background queue/dispatcher (e.g., call a non-blocking method like
_enqueue_observer_event(event) or
asyncio.create_task(self._observer_dispatcher.enqueue(event))). Implement a
background worker (start/stop lifecycle methods such as
_start_observer_dispatcher and _stop_observer_dispatcher) that consumes the
asyncio.Queue and calls _notify_observers(event) there, and ensure graceful
shutdown drains the queue before stop; keep references to _observers,
_message_bus, _SNAPSHOT_SENDER, and _SNAPSHOT_CHANNEL when locating where to
integrate the dispatcher.

In `@src/synthorg/engine/workflow/execution_activation_helpers.py`:
- Around line 262-266: The log call currently uses logger.warning when emitting
WORKFLOW_EXEC_TASK_CREATED with node_id=nid and error=msg right before raising a
ValueError; change these to logger.error for all three validation blocks (the
occurrences at the shown block and the ones around lines with similar
logger.warning + raise ValueError) so validation failures that abort execution
are logged at ERROR level while preserving the same context fields (node_id and
error).
- Around line 221-234: The conditional node currently only logs a warning when
true_target or false_target is None, allowing execution to continue incorrectly;
change this to fail closed by replacing the warning-only behavior with an error
and abort: in the block that checks "if true_target is None or false_target is
None", call logger.error (including WORKFLOW_EXEC_CONDITION_EVALUATED,
execution_id and nid, and the missing targets) and then raise an appropriate
exception (e.g., ValueError or a WorkflowExecutionError) so execution stops
instead of proceeding; keep the existing guard around find_skipped_nodes
(untaken/taken) but ensure the raised exception prevents routing when a branch
is missing.
- Around line 185-196: Replace the parenthesized multi-except clause with PEP
758's unparenthesized form in the try/except that calls evaluate_condition:
change the current "except (ValueError, TypeError, KeyError) as exc:" to the new
syntax "except ValueError, TypeError, KeyError as exc:" while keeping the
logger.exception(...) call and the subsequent raise of
WorkflowConditionEvalError (using execution_id, nid, expr, and exc as before).
- Around line 175-184: The code currently treats a missing/blank
node.config["condition_expression"] as false and only logs a warning; instead,
when condition_expression is absent or empty you must fail closed: replace the
warning branch in execution_activation_helpers.py (the block that reads expr =
str(node.config.get("condition_expression", "false")) and checks
node.config.get("condition_expression")) with logic that logs an error via
logger.error using WORKFLOW_EXEC_CONDITION_EVALUATED (include execution_id,
node_id/nid, expression/expr and a clear note), and then raise an
activation/validation exception to abort activation (use the project’s
WorkflowActivationError or a clear ValueError if no specific exception exists)
so missing/blank expressions are rejected at activation time rather than
silently defaulted to false.

In `@src/synthorg/engine/workflow/execution_models.py`:
- Around line 63-75: The validator must also reject generic control-node
terminal statuses on TASK nodes; update the check in the validation block around
self.status/self.node_type/_TASK_LINKED_STATUSES so that if self.node_type is
WorkflowNodeType.TASK and self.status is one of the generic terminal
control-node statuses (e.g., COMPLETED/FAILED), raise ValueError requiring a
TASK_* status (and/or a task_id) instead of allowing no task_id; additionally
add a regression test that constructs a WorkflowNode with
node_type=WorkflowNodeType.TASK and status=COMPLETED (and no task_id) and
asserts validation fails.

In `@src/synthorg/engine/workflow/execution_observer.py`:
- Around line 9-24: This module is missing the standardized module logger;
import get_logger from synthorg.observability and add logger =
get_logger(__name__) near the top of execution_observer.py (alongside the
existing TYPE_CHECKING block and imports) so any functions or classes in this
file (e.g., WorkflowExecutionService usage) can use logger per project
convention.

In `@web/src/pages/workflows/WorkflowFilters.tsx`:
- Around line 7-17: WORKFLOW_TYPES is duplicated between WorkflowFilters.tsx and
WorkflowCreateDrawer.tsx; extract it into a shared constant (e.g., export const
WORKFLOW_TYPES) in a common module like utils/constants, then import it into
both files and rebuild WORKFLOW_TYPE_OPTIONS in WorkflowFilters.tsx using the
imported WORKFLOW_TYPES (keeping formatLabel and WORKFLOW_TYPE_OPTIONS logic
unchanged) so both components read the single source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f9265be-8ce0-4025-8262-32b1e12c9356

📥 Commits

Reviewing files that changed from the base of the PR and between 4c0491c and da2697b.

📒 Files selected for processing (42)
  • CLAUDE.md
  • docs/design/engine.md
  • docs/design/page-structure.md
  • src/synthorg/api/app.py
  • src/synthorg/core/enums.py
  • src/synthorg/engine/task_engine.py
  • src/synthorg/engine/task_engine_events.py
  • src/synthorg/engine/task_engine_models.py
  • src/synthorg/engine/workflow/execution_activation_helpers.py
  • src/synthorg/engine/workflow/execution_models.py
  • src/synthorg/engine/workflow/execution_observer.py
  • src/synthorg/engine/workflow/execution_service.py
  • src/synthorg/observability/events/task_engine.py
  • src/synthorg/observability/events/workflow_execution.py
  • tests/unit/engine/test_task_engine_observers.py
  • tests/unit/engine/workflow/test_execution_helpers.py
  • tests/unit/engine/workflow/test_execution_lifecycle.py
  • tests/unit/engine/workflow/test_execution_models.py
  • tests/unit/engine/workflow/test_execution_observer.py
  • web/CLAUDE.md
  • web/src/api/types.ts
  • web/src/components/layout/Sidebar.tsx
  • web/src/hooks/useWorkflowsData.ts
  • web/src/pages/WorkflowEditorPage.tsx
  • web/src/pages/WorkflowsPage.tsx
  • web/src/pages/workflow-editor/ConditionExpressionBuilder.tsx
  • web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx
  • web/src/pages/workflow-editor/WorkflowSelector.tsx
  • web/src/pages/workflow-editor/WorkflowToolbar.tsx
  • web/src/pages/workflow-editor/WorkflowYamlEditor.tsx
  • web/src/pages/workflow-editor/condition-expression-types.ts
  • web/src/pages/workflow-editor/copy-paste.ts
  • web/src/pages/workflow-editor/yaml-to-nodes.ts
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowGridView.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/pages/workflows/WorkflowsSkeleton.tsx
  • web/src/router/index.tsx
  • web/src/stores/workflow-editor.ts
  • web/src/stores/workflows.ts

Comment on lines +43 to +48
reason: str | None = getattr(mutation, "reason", None)
task_id: str | None = getattr(mutation, "task_id", None)
# For create mutations, task_id comes from the result
if task_id is None and result.task is not None:
task_id = result.task.id
effective_task_id = task_id or "unknown"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider logging when falling back to "unknown" task_id.

When task_id is None and result.task is also None, the fallback to "unknown" could mask correlation issues. A DEBUG log here would aid troubleshooting.

💡 Optional: Add debug logging for fallback
     task_id: str | None = getattr(mutation, "task_id", None)
     # For create mutations, task_id comes from the result
     if task_id is None and result.task is not None:
         task_id = result.task.id
-    effective_task_id = task_id or "unknown"
+    effective_task_id = task_id
+    if effective_task_id is None:
+        effective_task_id = "unknown"
+        logger.debug("task_id_fallback", mutation_type=mutation.mutation_type)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/engine/task_engine_events.py` around lines 43 - 48, The code
sets effective_task_id = task_id or "unknown" without logging when both task_id
and result.task are None; add a DEBUG log right before assigning
effective_task_id that records the fallback case including reason, mutation
identifier if available, and any relevant result state so you can correlate
events; update the block around getattr(mutation, "reason", None),
getattr(mutation, "task_id", None), the conditional checking result.task, and
the effective_task_id assignment to emit process/logger.debug (or the module
logger) when task_id is None and result.task is None, including the reason and a
short description like "falling back to unknown task_id".

Comment on lines +54 to +76
export function serializeCondition(expr: ConditionExpression): string {
if (expr.kind === 'comparison') {
return `${expr.field} ${expr.operator} ${expr.value}`
}

if (expr.conditions.length === 0) return ''

// NOT wraps its single child in parentheses
if (expr.logicalOperator === 'NOT') {
const inner = expr.conditions.map((c) => serializeCondition(c)).join(' AND ')
return `NOT (${inner})`
}

const parts = expr.conditions.map((c) => {
// Nested groups get parenthesized to preserve precedence
if (c.kind === 'group' && c.conditions.length > 1) {
return `(${serializeCondition(c)})`
}
return serializeCondition(c)
})

return parts.join(` ${expr.logicalOperator} `)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if condition_eval.py supports AND/OR/NOT operators
echo "=== Searching for AND/OR/NOT handling in condition_eval.py ==="
fd -t f "condition_eval.py" --exec cat {}

echo ""
echo "=== Check for any logical operator parsing ==="
rg -n -i "AND|OR|NOT" --type py -g '*condition*'

Repository: Aureliolo/synthorg

Length of output: 21402


Compound AND/OR/NOT expressions are not supported by the backend evaluator—remove these operators or implement backend support.

The frontend's serializeCondition() generates strings like "field1 == val1 AND field2 != val2" and "NOT (expr)", but the backend's evaluate_condition() in condition_eval.py only supports:

  • Boolean literals ("true", "false")
  • Key lookups ("has_budget")
  • Single equality/inequality comparisons ("key == value", "key != value")

The backend has no parsing logic for AND, OR, or NOT operators. When it receives "field1 == val1 AND field2 == val2", it will fail the comparison check and fall through to key lookup, treating the entire string as a missing key and returning false. This causes silent failures and incorrect branch selection at runtime.

Either remove the AND/OR/NOT types and serialization logic from the frontend, or extend the backend evaluator to parse and evaluate compound expressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/condition-expression-types.ts` around lines 54
- 76, serializeCondition currently emits AND/OR/NOT compound strings (e.g. "a ==
b AND c != d") which the backend's evaluate_condition cannot parse; change
serializeCondition/ConditionExpression handling so it only emits single
comparisons, boolean literals, or single-key lookups (never "AND", "OR", "NOT").
Concretely, remove the branches that join multiple expr.conditions or wrap with
NOT in serializeCondition and instead: for 'group' kinds return the serialized
value of only the single child (or empty string if no children), for
'comparison' return the existing `${expr.field} ${expr.operator} ${expr.value}`,
and ensure any code creating ConditionExpression never produces logicalOperator
values other than a neutral/single-child form; reference serializeCondition and
ConditionExpression here and verify behavior against the backend
evaluate_condition in condition_eval.py.

Comment on lines +66 to +71
const edges = clipboard.edges.map((edge) => ({
...edge,
id: `${edge.id}-copy-${crypto.randomUUID().slice(0, 8)}`,
source: idMap.get(edge.source) ?? edge.source,
target: idMap.get(edge.target) ?? edge.target,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider removing unreachable fallback or adding a warning.

The fallback ?? edge.source and ?? edge.target on lines 69-70 can never be reached for valid ClipboardData since copyNodes only includes edges where both source and target are in the selection (and thus in idMap). The fallback could silently mask bugs if ClipboardData is constructed incorrectly elsewhere.

Option 1: Assert the invariant (recommended)
   const edges = clipboard.edges.map((edge) => ({
     ...edge,
     id: `${edge.id}-copy-${crypto.randomUUID().slice(0, 8)}`,
-    source: idMap.get(edge.source) ?? edge.source,
-    target: idMap.get(edge.target) ?? edge.target,
+    source: idMap.get(edge.source)!,
+    target: idMap.get(edge.target)!,
   }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const edges = clipboard.edges.map((edge) => ({
...edge,
id: `${edge.id}-copy-${crypto.randomUUID().slice(0, 8)}`,
source: idMap.get(edge.source) ?? edge.source,
target: idMap.get(edge.target) ?? edge.target,
}))
const edges = clipboard.edges.map((edge) => ({
...edge,
id: `${edge.id}-copy-${crypto.randomUUID().slice(0, 8)}`,
source: idMap.get(edge.source)!,
target: idMap.get(edge.target)!,
}))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflow-editor/copy-paste.ts` around lines 66 - 71, The
fallback nullish coalesces on clipboard.edges mapping (idMap.get(edge.source) ??
edge.source and similar for target) is unreachable and may hide bugs; update the
mapping in copy-paste.ts to assert the invariant instead of silently falling
back: require that idMap has entries for edge.source and edge.target (e.g.,
throw or use a runtime assert when idMap.get(...) is undefined) so invalid
ClipboardData surfaces immediately, and document this invariant where
copyNodes/clipboard.edges are constructed.

Comment on lines +232 to +246
const nodeData = state.nodes.map((n) => ({
id: n.id,
type: (n.data as Record<string, unknown>)?.nodeType as string ?? n.type ?? 'task',
label: (n.data as Record<string, unknown>)?.label as string ?? n.id,
position_x: n.position.x,
position_y: n.position.y,
config: (n.data as Record<string, unknown>)?.config as Record<string, unknown> ?? {},
}))
const edgeData = state.edges.map((e) => ({
id: e.id,
source_node_id: e.source,
target_node_id: e.target,
type: ((e.data as Record<string, unknown>)?.edgeType as string) ?? 'sequential',
label: ((e.data as Record<string, unknown>)?.label as string) ?? null,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Type assertions in node/edge mapping could fail silently.

The nested type assertions (n.data as Record<string, unknown>)?.nodeType as string) will produce undefined` rather than throwing if the data shape differs from expectations. Consider adding runtime validation or using optional chaining with explicit fallbacks more defensively.

-        const nodeData = state.nodes.map((n) => ({
-          id: n.id,
-          type: (n.data as Record<string, unknown>)?.nodeType as string ?? n.type ?? 'task',
-          label: (n.data as Record<string, unknown>)?.label as string ?? n.id,
-          position_x: n.position.x,
-          position_y: n.position.y,
-          config: (n.data as Record<string, unknown>)?.config as Record<string, unknown> ?? {},
-        }))
+        const nodeData = state.nodes.map((n) => {
+          const data = (n.data ?? {}) as Record<string, unknown>
+          return {
+            id: n.id,
+            type: String(data.nodeType ?? n.type ?? 'task'),
+            label: String(data.label ?? n.id),
+            position_x: n.position.x,
+            position_y: n.position.y,
+            config: (data.config as Record<string, unknown>) ?? {},
+          }
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/WorkflowEditorPage.tsx` around lines 232 - 246, The node/edge
mapping in WorkflowEditorPage.tsx (the nodeData and edgeData creation blocks)
relies on unchecked type assertions like (n.data as Record<string,
unknown>)?.nodeType as string which can mask shape errors; update these mappings
to perform runtime checks (e.g., verify typeof n.data === 'object' && n.data !==
null, then typeof nodeType === 'string' etc.), supply explicit fallbacks when
checks fail (default 'task' for node type, id for label, {} for config,
'sequential' or null for edge fields), or extract validation into a small helper
(e.g., safeGetNodeData or isRecord) and use it in the nodeData/edgeData mappings
to avoid silent failures.

Comment on lines +55 to +124
{workflows.map((w) => {
const editorUrl = `${ROUTES.WORKFLOW_EDITOR}?id=${encodeURIComponent(w.id)}`
return (
<tr
key={w.id}
className="border-b border-border last:border-0 transition-colors hover:bg-muted/30"
>
<td className="px-4 py-2.5 font-medium text-foreground">
<Link
to={editorUrl}
className="block w-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
aria-label={`Open workflow ${w.name}`}
>
{w.name}
</Link>
</td>
<td className="px-4 py-2.5">
<span className="rounded-full bg-accent/10 px-2 py-0.5 text-xs font-medium text-accent">
{w.workflow_type.replace(/_/g, ' ')}
</span>
</td>
<td className="px-4 py-2.5 text-right text-muted-foreground">{w.nodes.length}</td>
<td className="px-4 py-2.5 text-right text-muted-foreground">{w.edges.length}</td>
<td className="px-4 py-2.5 text-right text-muted-foreground">v{w.version}</td>
<td className="px-4 py-2.5 text-muted-foreground">{formatDate(w.updated_at)}</td>
<td className="px-2 py-2.5">
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
type="button"
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
aria-label={`Actions for ${w.name}`}
>
<MoreHorizontal className="size-4" />
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
align="end"
sideOffset={4}
className="z-50 min-w-36 rounded-md border border-border bg-popover p-1 shadow-[var(--so-shadow-card-hover)]"
>
<DropdownMenu.Item
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-accent/10 focus:bg-accent/10"
onSelect={() => navigate(editorUrl)}
>
<Pencil className="size-3.5" />
Edit
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-accent/10 focus:bg-accent/10"
onSelect={() => onDuplicate(w.id)}
>
<Copy className="size-3.5" />
Duplicate
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-danger outline-none hover:bg-danger/10 focus:bg-danger/10"
onSelect={() => setConfirmDeleteId(w.id)}
>
<Trash2 className="size-3.5" />
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</td>
</tr>
)
})}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Extract table row rendering to a dedicated component.

The .map() block spans ~70 lines with complex JSX including dropdown menus, link formatting, and multiple conditional elements. Per coding guidelines, complex (>8 line) JSX inside .map() should be extracted to a shared component for maintainability.

♻️ Proposed refactor
// Extract to WorkflowTableRow.tsx or inline above the component
interface WorkflowTableRowProps {
  workflow: WorkflowDefinition
  onEdit: (id: string) => void
  onDuplicate: (id: string) => void
  onDelete: (id: string) => void
}

function WorkflowTableRow({ workflow, onEdit, onDuplicate, onDelete }: WorkflowTableRowProps) {
  const editorUrl = `${ROUTES.WORKFLOW_EDITOR}?id=${encodeURIComponent(workflow.id)}`
  return (
    <tr className="border-b border-border last:border-0 transition-colors hover:bg-muted/30">
      {/* ... row content ... */}
    </tr>
  )
}

// Then in WorkflowTableView:
{workflows.map((w) => (
  <WorkflowTableRow
    key={w.id}
    workflow={w}
    onEdit={(id) => navigate(`${ROUTES.WORKFLOW_EDITOR}?id=${encodeURIComponent(id)}`)}
    onDuplicate={onDuplicate}
    onDelete={setConfirmDeleteId}
  />
))}

As per coding guidelines, "Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/workflows/WorkflowTableView.tsx` around lines 55 - 124, The
map's large JSX in WorkflowTableView should be extracted into a new
WorkflowTableRow component: create WorkflowTableRow({ workflow, onEdit,
onDuplicate, onDelete }) and move the <tr>..</tr> JSX there, keeping usages of
ROUTES.WORKFLOW_EDITOR, encodeURIComponent(workflow.id),
MoreHorizontal/Pencil/Copy/Trash2 icons, and formatDate(workflow.updated_at). In
WorkflowTableView replace the inline map body with <WorkflowTableRow key={w.id}
workflow={w} onEdit={(id) =>
navigate(`${ROUTES.WORKFLOW_EDITOR}?id=${encodeURIComponent(id)}`)}
onDuplicate={onDuplicate} onDelete={setConfirmDeleteId} /> ensuring the
DropdownMenu item handlers call onEdit/onDuplicate/onDelete and all aria-labels,
classname and other props remain unchanged.

Comment on lines +94 to +96
updateFromWsEvent: () => {
useWorkflowsStore.getState().fetchWorkflows()
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider debouncing WebSocket-triggered fetches.

updateFromWsEvent triggers a full fetch on every WebSocket event. If multiple events arrive in quick succession, this could cause redundant API calls. Consider adding a debounce.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/workflows.ts` around lines 94 - 96, The updateFromWsEvent
handler in useWorkflowsStore currently calls fetchWorkflows() on every WebSocket
event; wrap or replace that direct call with a debounced invoker so rapid
successive events only trigger one API fetch within a short window. Implement a
debounce (e.g., lodash.debounce or a simple setTimeout with cancellation)
associated with updateFromWsEvent that calls
useWorkflowsStore.getState().fetchWorkflows(), and ensure the debounced function
is created once (module scope or store init) so repeated events share the same
timer; update the updateFromWsEvent to call the debounced function instead of
invoking fetchWorkflows directly.

- engine.md: clarify CANCELLED tasks count as failing the workflow
- parse_task_config: upgrade validation log level from WARNING to ERROR
- Extract shared WORKFLOW_TYPES constant to utils/constants.ts (DRY)
@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/engine.md`:
- Around line 417-419: Update the "Notification semantics" paragraph to state
that observer errors are best-effort and logged at WARNING and swallowed for
normal exceptions, but explicitly carve out and re-raise critical exceptions
MemoryError and RecursionError (so a failing observer won't block the mutation
pipeline except for those two). Mention the Observer/observer callback behavior
and the mutation pipeline semantics, and ensure the doc text matches the
implementation's exception handling carveout for MemoryError and RecursionError.

In `@web/src/pages/workflows/WorkflowCreateDrawer.tsx`:
- Around line 28-31: WORKFLOW_TYPE_OPTIONS manually formats labels causing
duplication; replace the inline formatting with the shared utility used in
WorkflowFilters.tsx by importing and calling formatLabel for each WORKFLOW_TYPES
entry (i.e., map WORKFLOW_TYPES to { value: t, label: formatLabel(t) }) so
labels are consistent and duplication is removed; update the import to include
formatLabel and leave WORKFLOW_TYPE_OPTIONS, WORKFLOW_TYPES identifiers
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7b6d171-6f1c-49e7-bfac-931d601d98b0

📥 Commits

Reviewing files that changed from the base of the PR and between da2697b and 45d4dc0.

📒 Files selected for processing (5)
  • docs/design/engine.md
  • src/synthorg/engine/workflow/execution_activation_helpers.py
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/utils/constants.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones
Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and @/lib/motion presets

Files:

  • web/src/utils/constants.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: A PostToolUse hook (scripts/check_web_design_system.py) enforces design system rules on every Edit/Write to web/src/
Use property-based testing with fast-check (fc.assert + fc.property) for React component tests

Files:

  • web/src/utils/constants.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,js,jsx}: Always use createLogger from @/lib/logger -- never bare console.warn/console.error/console.debug in application code
Variable name for logger: always log (e.g. const log = createLogger('module-name'))
Use log.debug() (DEV-only, stripped in production), log.warn(), log.error() logging levels
Pass dynamic/untrusted values as separate args to logger (not interpolated into the message string) so they go through sanitizeArg
Attacker-controlled fields inside structured objects must be wrapped in sanitizeForLog() before embedding in log messages

Files:

  • web/src/utils/constants.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{tsx,ts}: Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Colors: use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)). NEVER hardcode hex values in .tsx/.ts files
Typography: use font-sans or font-mono (maps to Geist tokens). NEVER set fontFamily directly
Spacing: use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Shadows/Borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright)
Do NOT use rgba() with hardcoded values -- use design token variables
Hardcoded hex colors and rgba values in web/src/ files are automatically caught by a PostToolUse hook and must be fixed
Hardcoded font-family declarations in web/src/ files are automatically caught by a PostToolUse hook and must be fixed
Hardcoded Framer Motion transition durations (should use @/lib/motion presets) are automatically caught by a PostToolUse hook and must be fixed
Duplicate patterns that should use existing shared components in web/src/ are automatically caught by a PostToolUse hook and must be fixed
Complex .map() blocks that should be extracted to components in web/src/ are automatically caught by a PostToolUse hook and must be fixed
CSS side-effect imports need type declarations (Vite's /// <reference types="vite/client" /> covers this) as noUncheckedSideEffectImports defaults to true in TypeScript 6

Files:

  • web/src/utils/constants.ts
  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Always read the relevant docs/design/ page before implementing any feature or planning any issue. The design spec is the starting point for architecture, data models, and behavior.

Files:

  • docs/design/engine.md
web/src/pages/**/*.{tsx,ts}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones -- do not recreate status dots inline, use <StatusBadge>
Do NOT build card-with-header layouts from scratch -- use <SectionCard>
Do NOT create metric displays with text-metric font-bold -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>

Files:

  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: No from __future__ import annotations — Python 3.14 has PEP 649 native lazy annotations
Use PEP 758 except syntax: except A, B: (no parentheses) — ruff enforces this on Python 3.14
All public functions must have type hints with mypy strict mode
Public classes and functions must have Google-style docstrings (enforced by ruff D rules)
Use NotBlankStr (from core.types) for all identifier/name fields, including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants, instead of manual whitespace validators
For non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction and MappingProxyType wrapping for read-only enforcement
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
In all Pydantic ConfigDict declarations, use allow_inf_nan=False to reject NaN/Inf in numeric fields at validation time
Use @computed_field for derived values in Pydantic models instead of storing and validating redundant fields (e.g., TokenUsage.total_tokens)
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare create_task.
Functions must be less than 50 lines; files must be less than 800 lines
Handle errors explicitly; never silently swallow exceptions
Validate at system boundaries (user input, external APIs, config files)
Every module with business logic must import from synthorg.observability import get_logger and set logger = get_logger(__name__). Never use import logging or logging.getLogger() in application code.
Always use variable name logger (not _logger, not log) for structured logging
Line length must be 88 characters (enforced by ruff)

Files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
src/synthorg/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

src/synthorg/**/*.py: Use event name constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool). Import directly: from synthorg.observability.events.<domain> import EVENT_CONSTANT
All logger calls must use structured kwargs: logger.info(EVENT, key=value) — never use string formatting like logger.info('msg %s', val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG level for object creation, internal flow, and entry/exit of key functions
Never implement retry logic in driver subclasses or calling code — retry is handled automatically by BaseCompletionProvider. Set RetryConfig and RateLimiterConfig per-provider in ProviderConfig.
Never use vendor-specific names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, or comments. Use generic names: example-provider, example-large-001, example-medium-001, example-small-001. Vendor names only appear in: (1) docs/design/operations.md, (2) .claude/ skill files, (3) third-party import paths, (4) src/synthorg/providers/presets.py (user-facing runtime data).
Create new objects instead of mutating existing ones. Immutability is enforced throughout the codebase.

Files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
src/**/*.py

⚙️ CodeRabbit configuration file

This project uses Python 3.14+ with PEP 758 except syntax: "except A, B:" (comma-separated, no parentheses) is correct and mandatory -- do NOT flag it as a typo or suggest parenthesized form. The "except builtins.MemoryError, RecursionError: raise" pattern is intentional project convention for system-error propagation. When evaluating the 50-line function limit, count only the function body excluding the signature lines, decorators, and docstring. Functions 1-5 lines over due to docstrings or multi-line signatures should not be flagged. Do not suggest extracting single-use helper functions called exactly once -- this reduces readability without improving maintainability.

Files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
🧠 Learnings (33)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
📚 Learning: 2026-04-04T08:29:49.559Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T08:29:49.559Z
Learning: Applies to web/**/tsconfig.json : Explicitly list needed types in tsconfig.json `types` field (e.g. `"types": ["vitest/globals"]`) as TypeScript 6 no longer auto-discovers `types/*`

Applied to files:

  • web/src/utils/constants.ts
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...

Applied to files:

  • docs/design/engine.md
  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)

Applied to files:

  • docs/design/engine.md
  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/engine/coordination/**/*.py : Task coordination uses multi-agent pipeline with 4 dispatchers (SAS/centralized/decentralized/context-dependent), wave execution, and workspace lifecycle integration.

Applied to files:

  • docs/design/engine.md
  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-04-04T08:29:49.559Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T08:29:49.559Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : Export props as a TypeScript interface for new shared components

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export props as a TypeScript interface for new components

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
  • web/src/pages/workflows/WorkflowCreateDrawer.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-31T20:21:35.626Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:21:35.626Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from web/src/components/ui/ before creating new ones

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-04-04T08:29:49.559Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T08:29:49.559Z
Learning: Applies to web/src/pages/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones -- do not recreate status dots inline, use `<StatusBadge>`

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-04-04T08:29:49.559Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T08:29:49.559Z
Learning: Applies to web/src/**/*.{tsx,ts} : Duplicate patterns that should use existing shared components in `web/src/` are automatically caught by a PostToolUse hook and must be fixed

Applied to files:

  • web/src/pages/workflows/WorkflowFilters.tsx
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-04-04T08:16:41.646Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-04T08:16:41.646Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — ruff enforces this on Python 3.14

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, and key function entry/exit

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Prefer `asyncio.TaskGroup` for fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over bare `create_task`. Existing code is being migrated incrementally.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to src/ai_company/!(observability)/**/*.py : All error paths must log at WARNING or ERROR with context before raising.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-31T20:07:03.035Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T20:07:03.035Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising; all state transitions must log at INFO; DEBUG for object creation, internal flow, entry/exit of key functions

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
📚 Learning: 2026-03-17T06:43:14.114Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:43:14.114Z
Learning: Applies to src/synthorg/**/*.py : All error paths must log at WARNING or ERROR with context before raising. All state transitions must log at INFO. DEBUG for object creation, internal flow, entry/exit of key functions. Pure data models, enums, and re-exports do NOT need logging.

Applied to files:

  • src/synthorg/engine/workflow/execution_activation_helpers.py
🔇 Additional comments (11)
docs/design/engine.md (1)

260-263: Per-node status taxonomy is now correctly documented

This matches the runtime model split between task-node terminal outcomes (TASK_COMPLETED/TASK_FAILED) and control-node processing (COMPLETED).

web/src/utils/constants.ts (1)

208-213: LGTM!

The WORKFLOW_TYPES constant correctly mirrors the backend WorkflowType enum values and follows the established pattern with as const for type safety. This appropriately centralizes the constant to avoid duplication across components.

web/src/pages/workflows/WorkflowFilters.tsx (1)

1-39: LGTM!

Clean implementation that:

  • Uses shared UI components (InputField, SelectField) per coding guidelines
  • Imports WORKFLOW_TYPES from the centralized constants
  • Applies formatLabel for consistent label transformation
  • Correctly handles null/empty string normalization for the type filter
web/src/pages/workflows/WorkflowCreateDrawer.tsx (1)

33-121: LGTM!

Solid implementation with:

  • Proper state management for form, errors, and submission status
  • Accessible error announcement with role="alert" (addresses past review)
  • Correct cleanup on both success and cancel paths (addresses past review about resetting errors)
  • Uses shared UI components per coding guidelines
src/synthorg/engine/workflow/execution_activation_helpers.py (7)

185-196: PEP 758 exception syntax remains unfixed.

Line 187 still uses parenthesized form which is non-compliant with the project's Python 3.14 style.

-    except (ValueError, TypeError, KeyError) as exc:
+    except ValueError, TypeError, KeyError as exc:

As per coding guidelines, "Use PEP 758 except syntax: except A, B: (no parentheses) — ruff enforces this on Python 3.14."


1-49: LGTM!

Clean module setup with appropriate use of get_logger(__name__), TYPE_CHECKING for heavy imports, and frozenset for the immutable CONTROL_NODE_TYPES constant.


52-79: LGTM!

Correct BFS implementation for downstream task discovery. Properly stops propagation at AGENT_ASSIGNMENT nodes and collects only TASK nodes.


82-113: LGTM!

Correct reverse BFS for upstream task discovery. The sorted tuple return ensures deterministic dependency ordering across runs.


116-146: LGTM!

Two-pass BFS correctly identifies nodes reachable only through the untaken conditional branch. Safe adjacency lookups with .get(nid, []).


243-298: LGTM — validation issues addressed.

Strict validation now rejects unknown task_type, priority, and complexity values with structured ERROR logging before raising, per prior review feedback.


301-379: LGTM!

Well-structured async task creation with proper immutability (dict(node.config) copy), documented mutation of accumulators, and INFO-level logging for the state transition. The absence of local error handling is acceptable since task failures are handled via the observer pattern at the service level.

@Aureliolo
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Aureliolo Aureliolo merged commit 7b54262 into main Apr 4, 2026
33 of 34 checks passed
@Aureliolo Aureliolo deleted the feat/workflow-execution-lifecycle branch April 4, 2026 10:49
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 4, 2026 10:49 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 4, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.1](v0.6.0...v0.6.1)
(2026-04-04)


### Features

* capability-aware prompt profiles for model tier adaptation
([#1047](#1047))
([67650c5](67650c5)),
closes [#805](#805)
* implement procedural memory auto-generation from agent failures
([#1048](#1048))
([55f5206](55f5206)),
closes [#420](#420)
* implement quality scoring Layers 2+3 -- LLM judge and human override
([#1057](#1057))
([4a8adfe](4a8adfe)),
closes [#230](#230)
* token-based personality trimming via
PromptProfile.max_personality_tokens
([#1059](#1059))
([75afd52](75afd52)),
closes [#1045](#1045)
* workflow execution lifecycle + editor improvements
([#1058](#1058))
([7b54262](7b54262)),
closes [#1029](#1029)
[#1042](#1042)


### Refactoring

* **web:** address complexity and logging issues in dashboard
([#1056](#1056))
([ada997b](ada997b)),
closes [#1055](#1055)


### Documentation

* comprehensive documentation refresh
([#1050](#1050))
([c7a4259](c7a4259))


### Tests

* fix Hypothesis fuzzing infra and speed up slow unit tests
([#1044](#1044))
([1111602](1111602))


### Maintenance

* add text=auto catch-all to .gitattributes
([#1051](#1051))
([fc65d72](fc65d72))
* bump defu from 6.1.4 to 6.1.6 in /site
([#1062](#1062))
([f0cc439](f0cc439))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Aureliolo added a commit that referenced this pull request Apr 4, 2026
…1065)

## Summary

Composite PR implementing three features across Python backend, React
dashboard, and docs:

### Fine-tuning pipeline + dashboard (#1001)
- **5-stage ML pipeline**: synthetic data generation, hard negative
mining, contrastive fine-tuning (InfoNCE/MNRL), evaluation
(NDCG@10/Recall@10), deploy with backup/rollback
- **Background orchestration**: `asyncio.create_task` with SQLite state
persistence, cooperative cancellation via `threading.Event`, WebSocket
progress events, startup recovery
- **Checkpoint management API**: list, deploy, rollback, delete with
atomic set_active and TOCTOU-safe delete
- **Pre-flight validation**: dependency check, GPU detection, document
count validation
- **Dashboard page** at `/settings/memory/fine-tuning`: pipeline control
panel, live progress stepper/bar, checkpoint table with
deploy/rollback/delete (ConfirmDialog), run history, dependency banner

### CompositeBackend (#850)
- **Namespace-based routing**: routes memory operations to different
backends by namespace prefix
- **InMemoryBackend**: session-scoped, dict-based, with per-agent limits
and expiration
- **Configuration**: YAML-driven via `CompanyMemoryConfig.composite`
with bidirectional validation
- **Graceful degradation**: fan-out retrieve continues on partial
backend failures

### Workflow execution lifecycle (#1058)
- **COMPLETED/FAILED transitions**: TaskEngine observer mechanism
bridges task events to workflow execution status
- **Workflow list page**: card grid with search, type filter,
create/duplicate/delete
- **Editor improvements**: bidirectional YAML editing, structured
condition builder (AND/OR), minimap, copy/paste

## Test plan

- 13,862 Python tests pass (including 48 new: pipeline stages,
orchestrator lifecycle, cancellation, persistence CRUD, migration
schema, session store time-sensitivity fixes)
- 2,396 web dashboard tests pass
- All linters clean: ruff, mypy (strict), ESLint (zero warnings),
TypeScript

## Review coverage

Pre-reviewed by 18 specialized agents: docs-consistency, code-reviewer,
python-reviewer, pr-test-analyzer, silent-failure-hunter,
type-design-analyzer, logging-audit, resilience-audit,
conventions-enforcer, security-reviewer, frontend-reviewer,
design-token-audit, api-contract-drift, persistence-reviewer,
test-quality-reviewer, async-concurrency-reviewer, comment-analyzer,
issue-resolution-verifier. 42 findings addressed across 6 Critical, 24
Major, 6 Medium, 5 Docs, 1 Docstring bundle.

Key fixes from review:
- CancellationToken uses `threading.Event` (thread-safe for
`asyncio.to_thread` workers)
- Atomic `set_active` with `BEGIN IMMEDIATE` + existence verification
- TOCTOU-safe `delete_checkpoint` via conditional DELETE
- `_read_jsonl` wrapped in `asyncio.to_thread` (was blocking event loop)
- Pagination limits clamped to [1, 200]
- ConfirmDialog on destructive checkpoint delete
- ARIA attributes on progress bar and alert banner

Closes #1001
Closes #850
Closes #1058
Aureliolo added a commit that referenced this pull request Apr 4, 2026
)

## Summary

Implements #1061 (PerformanceConfig in RootConfig) and all 5 items from
#1060 (deferred improvements from PR #1058 review).

### Changes

**#1061: PerformanceConfig in RootConfig**
- Add `performance: PerformanceConfig` field to `RootConfig` with
default factory
- Add `"performance": {}` to `default_config_dict()` for YAML merge base
layer
- Wire `effective_config.performance` into `_build_performance_tracker`
in `create_app`
- Document all 12 fields in operations design spec (YAML example +
reference table + validation rules)
- Tests: schema defaults/custom, YAML round-trip via
`load_config_from_string`

**#1060.1: JWT localStorage to sessionStorage**
- Migrate all auth token storage from `localStorage` to `sessionStorage`
(tab-scoped)
- Updated: `auth.ts`, `client.ts`, `providers.ts` (SSE auth), and all
test files
- Security comment updated with sessionStorage threat model

**#1060.2: Compound condition evaluator (AND/OR/NOT)**
- Tokenizer + recursive descent parser for compound expressions with
parenthesized groups
- Operator precedence: NOT > AND > OR
- Quick-path for simple expressions (zero overhead for backward compat)
- Keywords inside values not split (e.g. `brand == ORLANDO` works
correctly)
- Never-raise contract preserved
- 31 new tests (compound operators, precedence, parens, edge cases)

**#1060.3: O(1) execution lookup by task_id**
- Add `find_by_task_id` to `WorkflowExecutionRepository` protocol
- SQLite implementation uses `json_each()` + `json_extract()` with
status index
- Replace O(R*N) scan in `execution_service._find_execution_by_task`
with repo delegation
- All 4 fake repos updated (execution_service, lifecycle, api, protocol
compliance)

**#1060.4: Background observer dispatcher**
- Decouple observer notification from single-writer mutation pipeline
- New `_observer_queue` + `_observer_dispatch_loop` (mirrors
`_processing_loop` pattern)
- Mutation pipeline uses `put_nowait()` instead of inline `await
_notify_observers()`
- Graceful shutdown drains observer queue after processing queue
- New test: slow observer does not block mutations

**#1060.5: Explicit branch metadata in YAML depends_on**
- `depends_on` entries support `string | { id, branch }` union type
- Exporter emits `{ id, branch: "true"|"false" }` for conditional edges
- Importer uses explicit branch when present, falls back to counter
inference
- Warns on unrecognized branch values
- 10 new tests across importer and exporter

### Additional fixes (from pre-PR review)
- Fix `WorkflowExecutionStatus` runtime import in `fakes_workflow.py`
(was under `TYPE_CHECKING`)
- Add `sessionStorage.clear()` to 3 missed auth test reset functions
- Complete operations.md field table (added `windows`,
`llm_sampling_model`, `collaboration_weights`)
- Add try/except error resilience to `_observer_dispatch_loop`
- Add `queue_size` to observer `QueueFull` log warning
- Remove duplicate JSDoc on `inferDependsOnEdgeType`
- Document operator-key-name limitation in condition evaluator
- Update CLAUDE.md engine description (observer dispatch, compound
conditions)
- Add unclosed-paren and double-NOT edge case tests
- Trim task_engine.py docstrings to stay under 800-line limit

### Test plan
- Python: 13,714 unit tests passing, lint + mypy clean
- Web: 2,406 tests passing, ESLint + TypeScript clean
- Pre-reviewed by 6 agents, 17 findings addressed

Closes #1061
Closes #1060
Aureliolo added a commit that referenced this pull request Apr 6, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.2](v0.6.1...v0.6.2)
(2026-04-06)


### Features

* add issue analyzer script for priority/scope management
([#1084](#1084))
([1ccba27](1ccba27))
* config fixes and deferred improvements from PR
[#1058](#1058) review
([#1067](#1067))
([2cac2d3](2cac2d3)),
closes [#1061](#1061)
[#1060](#1060)
* cumulative risk-unit action budgets
([#806](#806)) and automated
reporting ([#245](#245))
([#1063](#1063))
([4689816](4689816))
* fine-tuning pipeline + CompositeBackend + workflow lifecycle
([#1065](#1065))
([85b05bc](85b05bc)),
closes [#1001](#1001)
[#850](#850)
[#1058](#1058)
* memory consolidation upgrades (LLM Merge, Search-and-Ask, diversity
penalty, distillation capture)
([#1071](#1071))
([174e2be](174e2be)),
closes [#704](#704)
* migrate web dashboard from Radix UI to Base UI, activate CSP nonce,
rebuild org chart page, and fix agent routing
([#1083](#1083))
([ebc6921](ebc6921))
* v0.7.0 engine foundations -- structured failure diagnosis + auditable
decisions ([#1072](#1072))
([d341d37](d341d37))
* workflow templates and versioning with diff and rollback
([#1069](#1069))
([7af94de](7af94de)),
closes [#1006](#1006)
[#1008](#1008)


### Documentation

* unify REST API docs under /docs/openapi/ and patch sitemap
([#1073](#1073))
([af19382](af19382))


### Maintenance

* bump hypothesis from 6.151.10 to 6.151.11 in the all group
([#1086](#1086))
([3176318](3176318))
* bump nginxinc/nginx-unprivileged from `f99cc61` to `601c823` in
/docker/web in the all group
([#1085](#1085))
([5eb99ac](5eb99ac))
* bump the all group in /web with 3 updates
([#1087](#1087))
([8deae44](8deae44))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants