Skip to content

cue: pipeline persistence + per-root partitioning fixes, save UX hardening, frontend IPC service refactor#837

Merged
reachrazamair merged 6 commits intorcfrom
cue-polish
Apr 14, 2026
Merged

cue: pipeline persistence + per-root partitioning fixes, save UX hardening, frontend IPC service refactor#837
reachrazamair merged 6 commits intorcfrom
cue-polish

Conversation

@reachrazamair
Copy link
Copy Markdown
Contributor

@reachrazamair reachrazamair commented Apr 14, 2026

Summary

End-to-end overhaul of the Maestro Cue subsystem combining bug fixes for a cluster of user-reported persistence/UX issues with refactors that introduce a dedicated frontend Cue IPC service, a Zustand dirty-state store, a backend cleanup service, and a partitioned YAML validator.

Refactors

Frontend Cue IPC service layer (src/renderer/services/cue.ts)

  • New module: sole owner of window.maestro.cue.* calls. Reads return safe defaults; writes rethrow (matches the git.ts createIpcMethod pattern already used elsewhere).
  • Migrated 8 renderer files off raw window.maestro.cue.* and onto cueService.*.

Cue dirty-state store (src/renderer/stores/cueDirtyStore.ts)

  • New Zustand store eliminates the onDirtyChange prop chain and the pipelineDirtyRef hack.
  • CueModal reads dirty state via getState() so the value is always fresh; a cleanup effect resets flags on unmount.

YAML validator decomposition

  • Extracted validateSubscription(sub, prefix) and validateSettings(settings) from the monolithic validateCueConfigDocument.
  • New partitionValidSubscriptions(config) returns { configErrors, validIndices, subscriptionErrors } — separates fatal config-level errors from per-subscription errors so the loader can drop bad subs without failing the whole file.

Backend cleanup service (src/main/cue/cue-cleanup-service.ts)

  • New module: periodic sweep (every 10 heartbeat ticks) evicts stale fan-in trackers (removed sessions, > 2× timeout) and stale time.scheduled dedup keys.
  • Wired via CueHeartbeat.onTick; replaces ad-hoc cleanup scattered across the run manager.

Run-manager error-handling cleanup

  • New safeRecordCueEvent / safeUpdateCueEventStatus wrappers in cue-db.ts.
  • Migrated 3 silent try/catch blocks in cue-run-manager.ts to use the safe wrappers (errors now logged + reported to Sentry instead of swallowed).

Session-registry / fan-in tracker API extensions

  • CueFanInTracker: added getActiveTrackerKeys, getTrackerCreatedAt, expireTracker, plus a fanInCreatedAt map.
  • CueSessionRegistry: added sweepStaleScheduledKeys.
  • initSession() now idempotent — tears down existing registration before re-registering, preventing duplicate trigger sources from racing init paths.

Editor viewport-restore refactor

  • New pendingSavedViewportRef threaded from usePipelineLayout to CuePipelineEditor. Viewport restore now waits for ReactFlow to measure nodes (useNodesInitialized) instead of racing on a setTimeout against fitView — fixes "empty canvas on first open" symptoms.
  • PipelineCanvas + All-Pipelines-View locking polish.

Fixes

Persistence

  • Mirrored prompts / pipelines across project roots. handleSave was writing the same aggregated YAML and prompt files to every involved projectRoot. Now partitions pipelines by root and writes only that root's pipelines + prompts to its own .maestro/cue.yaml. Cross-root pipelines are rejected with a clear validation error.
  • Deleted pipeline reappears after restart. Save now tracks previously-saved roots and writes an empty YAML to any root whose pipelines were all removed this save, so stale YAMLs can no longer rehydrate "deleted" pipelines on next launch.
  • Agent swap reverts after restart. findTargetSession no longer overrides an explicit agent_id on a coincidental name match. Combined with per-root partitioning, agent_id is the source of truth.
  • Cross-session bleed in Dashboard. cue-query-service.getGraphData() filters subscriptions per-session by agent_id, so each session only surfaces its own subscriptions even when sharing a project root.
  • Orphan prompt files. New pruneOrphanedPromptFiles runs after every cue:writeYaml to delete .maestro/prompts/*.md files the new YAML no longer references.

Save UX (the "didn't save" class of bugs)

  • Empty pipelines silently saved as nothing. validatePipelines now flags completely-empty pipelines (add a trigger and an agent before saving) instead of skipping them; handleSave refuses to no-op when the editor has pipelines but nothing partitions to a root.
  • Trust gap on writes. Every cue:writeYaml is followed by a write-back read and byte-for-byte compare; mismatches throw, isDirty is preserved on failure, and explicit success/error toasts (Saved N pipelines to M projects / Your changes were NOT saved. <reason>) replace the missable 2-second in-button flash.
  • Pipeline vanishes after save. createPipeline assigns timestamp ids; the next reload regenerates name-based ids, leaving the saved selectedPipelineId stale and convertToReactFlowNodes skipping every pipeline. mergePipelinesWithSavedLayout now validates the saved selection against live pipeline ids and falls back to the first pipeline; a live safety-net effect in usePipelineState resets selectedPipelineId to null whenever it points at a pipeline that no longer exists.
  • Trigger config caught at load instead of save. pipelineToYaml was happy to write a time.scheduled subscription with no schedule_times (or time.heartbeat with no interval_minutes, etc.); the loader then rejected the entire YAML for every agent in that project root on Cue toggle. validatePipelines now mirrors the YAML schema's per-event requirements, blocking the bad save up front.

Loader resilience

  • One bad subscription killing a whole project's config. Loader now uses partitionValidSubscriptions to drop individual invalid subs as warnings instead of failing the entire load. Config-level errors (missing subscriptions array, bad settings) remain fatal.

Run output

  • Codex Reading additional input from stdin... in activity log. Cue child processes now spawn with stdio[0] = 'ignore' in local mode so codex exec doesn't emit the stdin banner before observing EOF. SSH stdin-script and SSH small-prompt paths still get a writable pipe. Claude was already correct — Codex needed /dev/null for stdin.

Test plan

  • npm run lint — clean
  • npm run test25,260 passing, 0 failing (107 skipped, pre-existing). Includes 65+ new tests across cue-cleanup-service, cue-db, cue-fan-in-tracker, cue-session-lifecycle, cueDirtyStore, services/cue, plus extended cue-engine, usePipelineState, pipelineLayout, usePipelineLayout, and two new CuePipelineEditor test files (allPipelinesViewLock, initialViewport).
  • Manual: delete a pipeline → save → restart app → pipeline does not reappear
  • Manual: replace Agent A with Agent B in a pipeline → save → close + reopen modal → restart app → still resolves to Agent B
  • Manual: create a pipeline with no nodes → click Save → see validation error in toolbar instead of fake "Saved"
  • Manual: create time.scheduled trigger without entering a time → click Save → see trigger needs at least one schedule time validation error
  • Manual: corrupt one subscription in .maestro/cue.yaml (e.g. delete schedule_times) → toggle Cue off/on → engine logs a single Skipped invalid subscription... warning instead of red error spam; other agents in the same project keep working
  • Manual: run a Codex agent through a pipeline → activity log no longer contains Reading additional input from stdin...
  • Manual: edit + save a pipeline → canvas keeps rendering the pipeline (does not vanish until tab-switch)

Commits

  • b8d35a95 — refactor: backend hardening + frontend IPC service layer
  • 4cd47878 — fix: prune orphan prompts, filter by session, trust agent_id
  • 1d917e5e — fix: pipeline persistence, save UX, loader resilience, codex stdin banner

Summary by CodeRabbit

  • New Features

    • Read-only "All Pipelines" mode; improved viewport restore and safer selection when saved layouts are stale.
    • Centralized Cue service API and a persistent dirty-state store for editor UX.
    • Background cleanup that evicts stale fan-in trackers and outdated scheduled dedup keys.
    • Automatic pruning of orphaned prompt files on save; lenient YAML loading that skips invalid subscriptions with warnings.
  • Bug Fixes

    • Subscriptions scoped to their owning sessions.
    • Prevented stray stdin reads for local runs.
    • Session initialization made idempotent.
  • Tests

    • Expanded coverage for cleanup, fan-in tracking, validation, viewport, dirty state, and editor behaviors.

backend:
- add safeRecordCueEvent/safeUpdateCueEventStatus wrappers in cue-db.ts;
  migrate 3 silent try/catch blocks in cue-run-manager.ts
- add idempotency guard to initSession() — tears down existing
  registration before re-registering to prevent duplicate trigger sources
- new CueCleanupService: periodic sweep (every 10 heartbeat ticks)
  evicts stale fan-in trackers (removed sessions, > 2× timeout) and
  stale time.scheduled dedup keys; wired via CueHeartbeat.onTick
- extend CueFanInTracker with getActiveTrackerKeys/getTrackerCreatedAt/
  expireTracker + fanInCreatedAt map
- extend CueSessionRegistry with sweepStaleScheduledKeys

frontend:
- new src/renderer/services/cue.ts: sole owner of window.maestro.cue.*
  calls; reads return safe defaults, writes rethrow (createIpcMethod
  pattern matching git.ts)
- migrate 8 renderer files from window.maestro.cue.* to cueService.*
- new cueDirtyStore (Zustand): eliminates onDirtyChange prop chain and
  pipelineDirtyRef hack; CueModal reads via getState() — always fresh
- add cleanup effect in CueModal to reset dirty flags on unmount

Tests: +65 new tests across 7 new/extended test files.
- pruneOrphanedPromptFiles: after cue:writeYaml, delete .md files under
  .maestro/prompts/ that the new YAML no longer references. Handles
  pipeline/agent renames and deletions so stale prompt files don't
  accumulate.
- cue-query-service: only report subscriptions whose agent_id matches
  the queried session (or is absent/shared). Previously every session
  sharing a project root surfaced every subscription, causing
  cross-session bleed in the Dashboard.
- yamlToPipeline: trust explicit agent_id when resolving target session
  instead of falling back to name match. Per-project-root YAML
  partitioning makes agent_id the source of truth; a coincidental
  pipeline-name/session-name overlap must not flip the resolved agent.
  Fixes "Maestro swap reverts" bug where replacing an agent would
  snap back to the original on reload.
…in banner

Fixes a cluster of Cue pipeline editor bugs around persistence and rendering:

- Pipeline vanish after save: createPipeline assigned timestamp ids; on
  reload subscriptionsToPipelines regenerated name-based ids, leaving the
  saved selectedPipelineId stale and convertToReactFlowNodes skipping every
  pipeline. mergePipelinesWithSavedLayout now validates the saved selection
  against the live pipeline ids and falls back to the first pipeline; a
  live safety-net effect in usePipelineState resets selectedPipelineId to
  null whenever it points at a pipeline that no longer exists.

- Save silently doing nothing: validatePipelines used to skip empty
  pipelines, so 'create N pipelines, click save' returned success without
  writing anything. Empty pipelines are now flagged ('add a trigger and an
  agent before saving') and handleSave refuses to no-op when the editor
  has pipelines but nothing partitions to a root.

- Lost-on-save trust gap: handleSave now write-back-verifies every
  cue:writeYaml by reading the file and comparing bytes, throws on
  mismatch, preserves isDirty on failure, and fires explicit success/
  error toast notifications so the 2-second in-button flash can no longer
  be missed.

- Trigger config caught at load instead of save: pipelineToYaml was happy
  to write a time.scheduled subscription with no schedule_times (or
  time.heartbeat with no interval_minutes, etc.), the loader then rejected
  the entire YAML for every agent in that project root on Cue toggle.
  validatePipelines now mirrors the YAML schema's per-event requirements,
  blocking the bad save up front.

- One bad subscription killing a whole project's config: extracted
  validateSubscription and added partitionValidSubscriptions; loader now
  drops individual invalid subs as warnings instead of failing the entire
  load. Config-level errors (missing subscriptions array, bad settings)
  remain fatal.

- Codex 'Reading additional input from stdin...' in run output: cue
  child processes spawn with stdio[0]='ignore' in local mode so codex
  exec doesn't emit the stdin banner before observing EOF. SSH stdin
  script and SSH small-prompt paths still get a writable pipe.

Also includes editor polish from the working branch: pendingSavedViewportRef
threaded from usePipelineLayout to CuePipelineEditor so viewport restore
waits for ReactFlow to measure nodes (no more empty canvas on first open),
plus PipelineCanvas + AllPipelinesView locking tweaks and matching tests.

Tests: 25,260 passing.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

Adds a heartbeat-driven Cue cleanup service (fan‑in expiry and scheduled-key sweep), safe DB wrappers, lenient per‑subscription validation and loader, renderer-side cueService and dirty-state store, viewport/read‑only pipeline editor changes, prompt-file pruning, and many corresponding tests.

Changes

Cohort / File(s) Summary
Cleanup infra & heartbeat
src/main/cue/cue-cleanup-service.ts, src/main/cue/cue-heartbeat.ts, src/main/cue/cue-engine.ts
New cleanup service with CLEANUP_INTERVAL_TICKS; heartbeat accepts onTick and engine wired to call cleanup.onTick; provides immediate sweep() and tick-based onTick() cadence.
Fan‑in tracker & registry
src/main/cue/cue-fan-in-tracker.ts, src/main/cue/cue-session-registry.ts
Fan‑in tracker gains inspection/expiry APIs (getActiveTrackerKeys, getTrackerCreatedAt, expireTracker); session registry adds sweepStaleScheduledKeys(currentTime) to evict stale time.scheduled dedup keys.
Prompt file write/prune & IPC
src/main/cue/config/cue-config-repository.ts, src/main/ipc/handlers/cue.ts
Strict .md prompt path validation; added pruneOrphanedPromptFiles(projectRoot, referencedRelativePaths); cue:writeYaml collects referenced prompt files and invokes pruning after successful parse/write.
Config validation & YAML loader
src/main/cue/config/cue-config-validator.ts, src/main/cue/cue-yaml-loader.ts
Per-subscription validateSubscription and partitionValidSubscriptions added; loader now skips invalid subscriptions with warnings instead of failing whole load.
Cue DB safe wrappers & run manager
src/main/cue/cue-db.ts, src/main/cue/cue-run-manager.ts
Added safeRecordCueEvent and safeUpdateCueEventStatus (catch, warn, captureException); run-manager uses safe wrappers for output-prompt lifecycle and adjusts error handling.
Process stdin/stdio behavior
src/main/cue/cue-process-lifecycle.ts
Spawn stdin mode set to 'ignore' unless stdin writes are required (SSH/stdin scripts), avoiding stray stdin reads when unused.
Query filtering
src/main/cue/cue-query-service.ts
getGraphData() now filters subscriptions per session to include only unbound subs or those owned by the session (agent_id === sessionId).
Session init idempotency
src/main/cue/cue-session-runtime-service.ts
initSession detects already-initialized sessions, warns, tears down prior state, and unregisters previous entry before continuing.
Renderer IPC wrapper & consumers
src/renderer/services/cue.ts, src/renderer/components/.../CueModal.tsx, src/renderer/components/.../CueYamlEditor.tsx, src/renderer/components/.../SessionList.tsx, src/renderer/hooks/*, src/renderer/hooks/remote/useRemoteIntegration.ts
Introduced cueService that wraps IPC with configured rethrow/default semantics; many renderer consumers switched to cueService with updated error/reporting and validation flows.
Dirty-state store
src/renderer/stores/cueDirtyStore.ts, various renderer components/tests
Added useCueDirtyStore (pipelineDirty, yamlDirty, setters, isAnyDirty, resetAll); components/tests migrated from onDirtyChange prop to store-based dirty tracking.
Pipeline editor viewport & read-only gating
src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx, src/renderer/components/CuePipelineEditor/PipelineCanvas.tsx, src/renderer/hooks/cue/usePipelineLayout.ts, src/renderer/hooks/cue/usePipelineState.ts, src/renderer/components/CuePipelineEditor/utils/pipelineLayout.ts, src/renderer/components/CuePipelineEditor/utils/yamlToPipeline.ts
Removed onDirtyChange prop; introduced pendingSavedViewportRef for deferred viewport restore; added All‑Pipelines read-only gating that prevents edits/context actions; reconciles saved selectedPipelineId and prefers explicit agent_id for session resolution.
Save flow, validations & layout persistence
src/renderer/hooks/cue/usePipelineState.ts, src/renderer/hooks/cue/usePipelineLayout.ts, src/shared/cue-pipeline-types.ts
handleSave partitions pipelines by projectRoot, writes per-root YAML, clears disappeared roots, verifies write via read-back, refreshes touched sessions, tracks lastWrittenRootsRef; added writtenRoots to persisted layout.
IPC wrapper Sentry & transform behavior
src/renderer/services/ipcWrapper.ts
createIpcMethod reports swallowed IPC errors to Sentry, applies transform outside try/catch so transform errors propagate, and documents cue.ts consumer.
Tests & mocks
src/__tests__/**/main/cue/*, src/__tests__/renderer/**
Many new/updated tests: cleanup service, fan‑in tracker inspection/expiry, registry scheduled-key sweep, safe DB wrappers, lenient loader behavior, stdio expectations, viewport/read-only editor tests, cueService tests, plus numerous mock extensions for cue-db and config repo.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Heartbeat as Heartbeat Loop
    participant Cleanup as Cleanup Service
    participant FanIn as Fan‑In Tracker
    participant Registry as Session Registry
    participant Log as Logger

    Heartbeat->>Cleanup: onTick()
    Note over Cleanup: increment tick counter
    alt tick >= CLEANUP_INTERVAL_TICKS
        Cleanup->>FanIn: getActiveTrackerKeys()
        FanIn-->>Cleanup: [keys]
        loop per key
            Cleanup->>FanIn: getTrackerCreatedAt(key)
            FanIn-->>Cleanup: timestamp or undefined
            Cleanup->>Registry: getSessions() / getSessionTimeoutMs(owner)
            alt owner not present OR age > 2×timeout
                Cleanup->>FanIn: expireTracker(key)
                Cleanup->>Log: warn("evicted fan-in key")
            end
        end
        Cleanup->>Registry: sweepStaleScheduledKeys(currentMinute)
        Registry-->>Cleanup: evictedCount
        alt evictedCount > 0
            Cleanup->>Log: info("scheduled keys evicted")
        end
        Cleanup->>Cleanup: reset tick counter
    end
Loading
sequenceDiagram
    autonumber
    participant Editor as Pipeline Editor
    participant Hook as usePipelineState
    participant Store as useCueDirtyStore
    participant Service as cueService
    participant IPC as IPC Handler

    Editor->>Hook: user edits -> setIsDirty(true)
    Hook->>Store: setPipelineDirty(true)
    Editor->>Hook: save -> handleSave()
    Hook->>Service: writeYaml(root, content, promptFiles)
    Service->>IPC: window.maestro.cue.writeYaml(...)
    IPC->>IPC: parse content -> collect prompt file paths
    IPC->>IPC: pruneOrphanedPromptFiles(projectRoot, keepPaths)
    IPC-->>Service: write success
    Service->>Service: readYaml() verify written content
    Service-->>Hook: verified
    Hook->>Store: setPipelineDirty(false)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ready to merge

Poem

🐇 I hop the ticks and sweep the keys,

I nudge old trackers from the trees.
Dirty flags tucked safe and neat,
Viewports wait for code to meet.
Prompt files pruned — I nibble, not eat.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: pipeline persistence, per-root partitioning fixes, save UX improvements, and a frontend IPC service refactor—all of which are directly supported by the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cue-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 14, 2026

Greptile Summary

Large-scope Cue subsystem overhaul combining persistence bug fixes (per-root YAML partitioning, deleted-pipeline reappearance, agent swap reversion, cross-session Dashboard bleed), save UX hardening (write-back verification, explicit toast feedback, empty-pipeline validation), and structural refactors (frontend IPC service layer, cueDirtyStore, backend cleanup service, lenient YAML loader).

  • P1 — cue:writeYaml IPC handler deletes referenced prompt files via YAML editor: when CueYamlEditor.handleSave calls cueService.writeYaml without a promptFiles argument, referencedPaths is [] and pruneOrphanedPromptFiles removes every .md file under .maestro/prompts/ including files still referenced by the saved YAML. (src/main/ipc/handlers/cue.ts lines 241–247)
  • P2 — validateYaml default silently reports "valid" on IPC failure, allowing saves of unvalidated content through CueYamlEditor's if (!isValid) return gate.

Confidence Score: 3/5

Not safe to merge — saving via the YAML editor silently deletes prompt files still referenced by the config.

The P1 issue in the writeYaml IPC handler causes data loss on every YAML editor save for projects with pipeline-editor-generated prompt files. The rest of the PR is well-structured and the fixes are correct, but this gap needs to be addressed before shipping.

src/main/ipc/handlers/cue.ts (writeYaml handler), src/renderer/services/cue.ts (validateYaml defaultValue)

Important Files Changed

Filename Overview
src/main/ipc/handlers/cue.ts writeYaml handler calls pruneOrphanedPromptFiles with empty referencedPaths when promptFiles is absent (YAML editor path), deleting all referenced prompt files on save.
src/renderer/services/cue.ts New IPC service layer correctly wraps all cue.* calls; validateYaml uses an unsafe defaultValue of valid:true which silently bypasses validation on IPC failure.
src/renderer/stores/cueDirtyStore.ts Clean Zustand store replacing the pipelineDirtyRef/onDirtyChange prop chain; exposes isAnyDirty() but callers in CueModal use raw pipelineDirty instead.
src/renderer/components/CueModal/CueModal.tsx Modal shell refactored to use cueService and cueDirtyStore; close handlers check pipelineDirty directly instead of isAnyDirty(), missing yamlDirty state.
src/main/cue/cue-cleanup-service.ts New periodic sweep service correctly evicts stale fan-in trackers and time.scheduled dedup keys; wired via CueHeartbeat.onTick.
src/main/cue/config/cue-config-validator.ts validateSubscription extracted cleanly; partitionValidSubscriptions correctly separates config-level errors from per-subscription errors for lenient loading.
src/main/cue/cue-yaml-loader.ts loadCueConfigDetailed now uses partitionValidSubscriptions to drop bad subscriptions as warnings instead of failing the whole file; resilience fix looks correct.
src/main/cue/cue-run-manager.ts Silent try/catch blocks replaced with safeRecordCueEvent / safeUpdateCueEventStatus wrappers; run lifecycle state machine unchanged and correct.
src/renderer/hooks/cue/usePipelineState.ts handleSave correctly partitions pipelines by root, adds write-back verification, and clears stale roots; safety-net effect for stale selectedPipelineId is a solid fix.
src/main/cue/cue-query-service.ts getGraphData now filters subscriptions by agent_id per-session, fixing cross-session bleed in Dashboard; logic is clean.
src/main/cue/cue-process-lifecycle.ts stdin set to 'ignore' in local mode to prevent Codex banner; SSH paths correctly retain 'pipe' stdin; fix is minimal and targeted.

Sequence Diagram

sequenceDiagram
    participant YE as CueYamlEditor
    participant PE as usePipelineState
    participant SVC as cueService
    participant IPC as cue:writeYaml handler
    participant REPO as cue-config-repository
    participant FS as Filesystem

    Note over YE,FS: YAML Editor save path (bug)
    YE->>SVC: writeYaml(root, yamlContent)
    SVC->>IPC: writeYaml({root, content, promptFiles=undefined})
    IPC->>IPC: referencedPaths = []
    IPC->>FS: writeCueConfigFile(root, content)
    IPC->>REPO: pruneOrphanedPromptFiles(root, [])
    REPO->>FS: DELETE all .md files in .maestro/prompts/

    Note over PE,FS: Pipeline Editor save path (correct)
    PE->>PE: pipelinesToYaml() returns {yaml, promptFiles}
    PE->>SVC: writeYaml(root, yaml, promptFilesObj)
    SVC->>IPC: writeYaml({root, content, promptFiles: {...}})
    IPC->>IPC: referencedPaths = Object.keys(promptFiles)
    IPC->>FS: writeCuePromptFile() for each entry
    IPC->>FS: writeCueConfigFile(root, content)
    IPC->>REPO: pruneOrphanedPromptFiles(root, referencedPaths)
    REPO->>FS: DELETE only unreferenced .md files
    PE->>SVC: readYaml(root) for write-back verify
    SVC-->>PE: onDisk content
    PE->>PE: byte-compare, throw on mismatch
Loading

Comments Outside Diff (1)

  1. src/renderer/components/CueModal/CueModal.tsx, line 249-258 (link)

    P2 Close handler ignores yamlDirty — use isAnyDirty()

    Both handleCloseWithConfirm and the onEscape handler (line 135) call useCueDirtyStore.getState().pipelineDirty directly, missing yamlDirty. The store provides isAnyDirty() for exactly this pattern.

Reviews (1): Last reviewed commit: "fix(cue): pipeline persistence, save UX,..." | Re-trigger Greptile

Comment on lines 241 to 247
writeCueConfigFile(options.projectRoot, options.content);

// Remove any `.md` files under .maestro/prompts/ that the new YAML no
// longer references (handles pipeline/agent renames and deletions).
pruneOrphanedPromptFiles(options.projectRoot, referencedPaths);
}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 YAML editor save deletes all referenced prompt files

When cueService.writeYaml is called from the YAML editor (CueYamlEditor.handleSave), promptFiles is undefined, so referencedPaths stays []. pruneOrphanedPromptFiles(projectRoot, []) then deletes every .md file under .maestro/prompts/ — including files that are still referenced by prompt_file: entries in the YAML the user just wrote.

A user who edits YAML manually and hits Save will silently lose all their pipeline prompt files. On next Cue engine load, subscriptions with prompt_file references will fail to materialise.

The IPC handler should derive the keep-set from the YAML content when promptFiles is absent:

if (!options.promptFiles) {
    try {
        const parsed = yaml.load(options.content) as Record<string, unknown> | null;
        const subs = parsed?.subscriptions;
        if (Array.isArray(subs)) {
            for (const sub of subs as Record<string, unknown>[]) {
                if (typeof sub?.prompt_file === 'string') referencedPaths.push(sub.prompt_file);
                if (typeof sub?.output_prompt_file === 'string') referencedPaths.push(sub.output_prompt_file);
            }
        }
    } catch {
        // parse error — do NOT prune when YAML is unparseable
    }
}

Comment on lines +84 to +90
async validateYaml(content: string): Promise<{ valid: boolean; errors: string[] }> {
return createIpcMethod({
call: () => window.maestro.cue.validateYaml(content),
errorContext: 'Cue validateYaml',
defaultValue: { valid: true, errors: [] },
});
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 validateYaml silently reports "valid" on IPC failure

defaultValue: { valid: true, errors: [] } means any IPC error is reported as a clean validation pass. CueYamlEditor.handleSave gates on isValid, so a failed validation call leaves isValid = true and allows saving unvalidated content.

Suggested change
async validateYaml(content: string): Promise<{ valid: boolean; errors: string[] }> {
return createIpcMethod({
call: () => window.maestro.cue.validateYaml(content),
errorContext: 'Cue validateYaml',
defaultValue: { valid: true, errors: [] },
});
},
async validateYaml(content: string): Promise<{ valid: boolean; errors: string[] }> {
return createIpcMethod({
call: () => window.maestro.cue.validateYaml(content),
errorContext: 'Cue validateYaml',
defaultValue: { valid: false, errors: ['Validation service unavailable'] },
});
},

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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/main/ipc/handlers/cue.ts (1)

221-246: ⚠️ Potential issue | 🔴 Critical

Pruning from promptFiles can delete still-referenced prompt files

pruneOrphanedPromptFiles is fed only options.promptFiles keys. When promptFiles is missing or partial, valid .maestro/prompts/*.md files can be removed even if YAML still references them.

Safer approach (derive keep-set from YAML content)
 			async (options: {
 				projectRoot: string;
 				content: string;
 				promptFiles?: Record<string, string>;
 			}): Promise<void> => {
-				const referencedPaths: string[] = [];
+				const referencedPaths = new Set<string>();
+
+				const collectPromptPaths = (node: unknown): void => {
+					if (Array.isArray(node)) {
+						for (const item of node) collectPromptPaths(item);
+						return;
+					}
+					if (!node || typeof node !== 'object') return;
+					const obj = node as Record<string, unknown>;
+					for (const [key, value] of Object.entries(obj)) {
+						if (
+							(key === 'prompt_file' || key === 'output_prompt_file') &&
+							typeof value === 'string'
+						) {
+							referencedPaths.add(value);
+						}
+						collectPromptPaths(value);
+					}
+				};
+
 				if (options.promptFiles) {
 					const promptsBase = path.resolve(options.projectRoot, '.maestro/prompts');
 					for (const [relativePath, content] of Object.entries(options.promptFiles)) {
@@
 						writeCuePromptFile(options.projectRoot, relativePath, content);
-						referencedPaths.push(relativePath);
+						referencedPaths.add(relativePath);
 					}
 				}
 
 				writeCueConfigFile(options.projectRoot, options.content);
 
-				// Remove any `.md` files under .maestro/prompts/ that the new YAML no
-				// longer references (handles pipeline/agent renames and deletions).
-				pruneOrphanedPromptFiles(options.projectRoot, referencedPaths);
+				const parsedYaml = yaml.load(options.content);
+				collectPromptPaths(parsedYaml);
+				pruneOrphanedPromptFiles(options.projectRoot, referencedPaths);
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/cue.ts` around lines 221 - 246, The current prune call
only uses referencedPaths built from options.promptFiles so files referenced
solely inside the YAML (options.content) can be deleted; before calling
pruneOrphanedPromptFiles, derive the keep-set from the YAML content written by
writeCueConfigFile by parsing options.content to extract any referenced prompt
file paths (e.g., pipeline/agent prompt keys that resolve to
.maestro/prompts/*.md), normalize them to relative paths like referencedPaths
uses, then take the union of that set and the existing referencedPaths (and any
explicitly written via writeCuePromptFile) and pass that union into
pruneOrphanedPromptFiles(options.projectRoot, keepPaths) so missing/partial
options.promptFiles won't cause referenced files to be pruned.
src/renderer/components/SessionList/SessionList.tsx (1)

147-163: ⚠️ Potential issue | 🟠 Major

Don’t silently swallow cueService.getStatus() failures.

The catch {} at Line 161 swallows both expected and unexpected failures. Please handle known recoverable states explicitly (e.g., engine-not-initialized), and report unexpected errors via Sentry so production failures are observable.
As per coding guidelines: src/**/*.{ts,tsx}: “Do not silently swallow errors... For unexpected errors, re-throw them to allow Sentry to capture them. Use Sentry utilities (captureException, captureMessage).”

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

In `@src/renderer/components/SessionList/SessionList.tsx` around lines 147 - 163,
The catch in fetchCueStatus around cueService.getStatus() currently swallows all
errors; update the fetchCueStatus error handling to detect and ignore only the
known recoverable condition (e.g., check error.code or error.message for the
engine-not-initialized indicator) and leave the rest to be reported: call
Sentry.captureException(error) (or captureMessage) for unexpected errors and
re-throw them so they surface, while keeping the existing behavior of returning
early when the engine is simply not ready; references: fetchCueStatus function,
cueService.getStatus(), and setCueSessionMap.
src/renderer/components/CueYamlEditor/CueYamlEditor.tsx (2)

69-77: ⚠️ Potential issue | 🟠 Major

Avoid silent catch-all blocks in the new cueService paths.

These blocks currently suppress unexpected failures with no telemetry. Keep recoverable handling, but report unexpected exceptions (and/or rethrow) so errors are visible in production.
As per coding guidelines: src/**/*.{ts,tsx}: “Do not silently swallow errors... For unexpected errors, re-throw them to allow Sentry to capture them. Use Sentry utilities (captureException, captureMessage).”

Also applies to: 143-151

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

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx` around lines 69 -
77, The silent catch-all blocks around the cueService paths (notably where
cueService.validateYaml is awaited and the outer try blocks in
CueYamlEditor.tsx) should not swallow unexpected errors: change the catch blocks
to capture/report the exception (e.g., call Sentry.captureException(err) or
captureMessage with context) and then re-throw or return as appropriate; keep
the existing recoverable handling for validation failures (still setIsValid /
setValidationErrors when not cancelled) but ensure unexpected exceptions are
forwarded to telemetry (or re-thrown) so they surface in production—apply the
same change to the other catch at the 143-151 area.

136-149: ⚠️ Potential issue | 🟠 Major

Empty YAML from disk is incorrectly ignored during refresh.

At Line 139, if (content) treats '' as falsy, so an intentionally empty .maestro/cue.yaml won’t refresh the editor and stale content can remain visible.

🐛 Proposed fix
 const refreshYamlFromDisk = useCallback(async () => {
 	try {
 		const content = await cueService.readYaml(projectRoot);
-		if (content) {
-			setYamlContent(content);
-			setOriginalContent(content);
+		if (content !== null) {
+			setYamlContent(content);
+			setOriginalContent(content);
 			try {
 				const result = await cueService.validateYaml(content);
 				setIsValid(result.valid);
 				setValidationErrors(result.errors);
 			} catch {
 				// non-fatal
 			}
 		}
 	} catch {
 		// non-fatal
 	}
 }, [projectRoot]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx` around lines 136 -
149, The refreshYamlFromDisk callback incorrectly ignores an intentionally empty
YAML because it checks `if (content)`; change that check to explicitly test for
null/undefined (e.g., `if (content != null)` or `if (content !== undefined &&
content !== null)`) so that an empty string "" from
`cueService.readYaml(projectRoot)` still triggers `setYamlContent`,
`setOriginalContent`, and the subsequent validation call
(`cueService.validateYaml`, `setIsValid`, `setValidationErrors`).
src/renderer/hooks/remote/useRemoteIntegration.ts (1)

887-895: ⚠️ Potential issue | 🟠 Major

Capture remote Cue trigger failures instead of only logging them.

This handler now routes control flow through cueService.triggerSubscription(...), but unexpected failures still get reduced to false plus console.error. The response fallback is fine; the missing piece is explicit Sentry reporting.

Refactor sketch
+import { captureException } from '../../../utils/sentry';
...
				} catch (error) {
-					console.error('[Remote Cue Trigger] Failed:', subscriptionName, error);
+					captureException(error, {
+						operation: 'remote-trigger-cue-subscription',
+						subscriptionName,
+					});
					window.maestro.process.sendRemoteTriggerCueSubscriptionResponse(responseChannel, false);
				}

As per coding guidelines, "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production... Use Sentry utilities (captureException, captureMessage) from src/utils/sentry.ts for explicit error reporting with context."

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

In `@src/renderer/hooks/remote/useRemoteIntegration.ts` around lines 887 - 895,
The catch block in the onRemoteTriggerCueSubscription handler currently only
logs the error and returns false; update it to explicitly report the failure to
Sentry by importing and calling captureException (and optionally captureMessage)
from src/utils/sentry.ts inside the catch, passing the caught error plus
contextual metadata (subscriptionName, responseChannel, prompt) so Sentry
records the failure; keep the existing console.error and
window.maestro.process.sendRemoteTriggerCueSubscriptionResponse(responseChannel,
false) behavior after reporting.
🧹 Nitpick comments (3)
src/renderer/hooks/useCue.ts (1)

4-4: Finish the cueService migration inside refresh().

The write paths and activity subscription now use the wrapper, but refresh() still talks to window.maestro.cue.* directly. That leaves two IPC contracts in the same hook and bypasses the wrapper's read-side behavior.

Refactor sketch
			const [statusData, runsData, logData, queueData] = await Promise.all([
-				window.maestro.cue.getStatus(),
-				window.maestro.cue.getActiveRuns(),
-				window.maestro.cue.getActivityLog(100),
-				window.maestro.cue.getQueueStatus(),
+				cueService.getStatus(),
+				cueService.getActiveRuns(),
+				cueService.getActivityLog(100),
+				cueService.getQueueStatus(),
			]);

Also applies to: 61-99

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

In `@src/renderer/hooks/useCue.ts` at line 4, The refresh() function in useCue.ts
still calls window.maestro.cue.* directly, causing mixed IPC usage; change
refresh() to use the cueService read/get methods (e.g., cueService.getCue,
cueService.getAllCues or whichever read APIs exist) instead of
window.maestro.cue.* and remove direct window.maestro calls; also update any
similar direct reads between lines ~61-99 to their cueService equivalents so all
read and subscription logic consistently uses the wrapper (keep existing
write/subscription changes intact and ensure return types/error handling match
cueService methods).
src/__tests__/main/cue/cue-session-lifecycle.test.ts (1)

633-660: Either add a real trigger here or rename the test.

With subscriptions: [], initSession() never creates timers/watchers/pollers. snapshot().size === 1 only proves registry dedupe, so duplicate trigger registration leaks would still pass unnoticed.

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

In `@src/__tests__/main/cue/cue-session-lifecycle.test.ts` around lines 633 - 660,
The test uses subscriptions: [] so initSession(session, ...) never registers any
trigger sources — change the test to either provide a real subscription/trigger
in the mock Cue config or rename the test to reflect it only validates registry
deduplication; specifically update mockLoadCueConfig call to return a config
with at least one trigger/subscription (so initSession on
CueSessionRuntimeService will create timers/watchers/pollers and register
sources in the registry created by createCueSessionRegistry), or if you prefer
keep empty subscriptions then rename the test title and assertions to state it
only verifies registry dedupe for initSession rather than "does not
double-register trigger sources".
src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx (1)

192-250: Add the post-mount useNodesInitialized() flip.

The regression path is “false on first render, true later”. All current cases set mockNodesInitialized before mount, so an effect missing useNodesInitialized() in its dependency list would still pass.

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

In
`@src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx`
around lines 192 - 250, The tests currently set mockNodesInitialized true before
mounting which hides a missing post-mount flip of the useNodesInitialized()
dependency; update the relevant tests (the ones named "fits view once nodes have
been measured", "restores saved viewport once nodes have been measured (does NOT
fitView)", "consumes the pending saved viewport after applying it", and "runs
the initial viewport step exactly once") to simulate the real regression path by
mounting with mockNodesInitialized = false, calling renderEditor(), then setting
mockNodesInitialized = true and forcing a post-mount update (e.g., rerender or
act) so the effect depending on useNodesInitialized() runs; use the existing
helpers mockPendingSavedViewportRef, mockFitView, mockSetViewport and
renderEditor()/rerender to drive and then assert the same expectations.
🤖 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/main/cue/config/cue-config-repository.ts`:
- Around line 137-140: The try/catch around fs.readdirSync (the block that
currently returns on error) must not silently swallow errors—import and call
captureException (and optionally captureMessage) from the Sentry utilities
(src/utils/sentry.ts) with contextual info, then re-throw the error for
unexpected failures so callers can handle it; apply the same treatment to the
other catch block around the save/prune traversal (the catch covering lines
~152-157) so both places use captureException(...) and then throw the caught
error.

In `@src/main/cue/cue-db.ts`:
- Around line 198-222: The catch blocks in safeRecordCueEvent and
safeUpdateCueEventStatus swallow DB errors without Sentry reporting; update both
functions to import and call the Sentry utility (e.g., captureException or
reportError) from src/utils/sentry.ts inside the catch, passing the caught error
and a context object that includes operation and relevant data (for
safeRecordCueEvent include { operation: 'safeRecordCueEvent', event } and for
safeUpdateCueEventStatus include { operation: 'safeUpdateCueEventStatus', id,
status }), keep the existing log('warn', ...) behavior and still return without
throwing so callers remain non-fatal.

In `@src/main/cue/cue-fan-in-tracker.ts`:
- Around line 66-67: The issue is that fanInCreatedAt entries are left behind
when fan-in completes because only fanInTrackers gets deleted; update the
cleanup logic to also remove the timestamp entries. Specifically, wherever
fanInTrackers.delete(key) or the fan-in successful completion path removes a
tracker (e.g., the completion handler and the block that currently deletes
fanInTrackers), also call fanInCreatedAt.delete(key); likewise, adjust
clearForSession() to either remove all fanInCreatedAt entries for the session or
to iterate the same active keys it uses for fanInTrackers and delete
corresponding fanInCreatedAt entries so no orphaned timestamps remain.

In `@src/main/cue/cue-run-manager.ts`:
- Around line 238-246: The child output run (created via safeRecordCueEvent with
id outputRunId and status 'running') is never finalized if the output prompt
rejects; wrap the output prompt invocation/await in a try/catch (or add a catch
on its promise) and in the rejection path call safeRecordCueEvent for
outputRunId with a terminal status (e.g., 'failed' or 'error') and include error
details (e.g., JSON.stringify(error)) so the :output DB row is finalized; mirror
this change for the other similar output-finalization site referenced
(outputRunId usage around the later block).

In `@src/main/cue/cue-yaml-loader.ts`:
- Around line 85-93: The filter logic in parseCueConfigDocument incorrectly
applies indices from partitionValidSubscriptions (subscriptionErrors) which
refer to the raw YAML array, not the normalized document.subscriptions, causing
wrong removals; fix by changing the flow so validation runs against the
normalized subscriptions instead of raw YAML: after parseCueConfigDocument
builds the normalized array (document.subscriptions), call
partitionValidSubscriptions on that normalized array (or modify
partitionValidSubscriptions to accept the normalized array) and then derive
skippedIndices from subscriptionErrors to filter the same normalized array,
ensuring symbols involved are parseCueConfigDocument,
partitionValidSubscriptions, subscriptionErrors, and document.subscriptions.

In `@src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx`:
- Around line 649-668: The context menu's Configure/Delete/Duplicate actions
still execute when the UI is in All Pipelines view because only
onNodeContextMenu was blocked; add the same read-only guard to the context menu
action handlers so they no-op if isAllPipelinesView (or selectedPipelineId ===
null). Locate the context-menu action handlers (the Delete and Duplicate
handlers and any Configure handler invoked by the context menu) and add an early
return when isAllPipelinesView is true (or selectedPipelineId === null) before
performing any edits or calling setContextMenu-related logic; also apply the
same guard to any helper functions those handlers call so an already-open menu
cannot perform edits.

In `@src/renderer/hooks/cue/usePipelineState.ts`:
- Around line 423-441: The code currently rebuilds previousRoots from
savedStateRef.current which fails when sessions moved/removed; instead persist
the last-written root set in a dedicated ref/field (e.g., lastWrittenRootsRef)
when performing successful writes and use that persisted set when computing
previousRoots (replace the current JSON.parse(savedStateRef.current) logic), and
ensure clears use the same read-back verification path as non-empty writes
(reuse the write-and-verify routine used elsewhere) so the empty-YAML clear
isn't a silent no-op; apply the same change to the analogous logic around the
block handling lines ~476-482.

In `@src/renderer/services/cue.ts`:
- Around line 20-90: The createIpcMethod catch block in
src/renderer/services/ipcWrapper.ts currently only console.errors and returns
options.defaultValue; update it to also call captureException(error, { extra: {
context: options.errorContext } }) before returning the default so IPC failures
are reported to Sentry. Locate the createIpcMethod implementation and inside its
try/catch where it handles errors for calls that have a defaultValue, import or
reference captureException and invoke it with the caught error and an extra
field containing options.errorContext, then continue to return
options.defaultValue as before.

---

Outside diff comments:
In `@src/main/ipc/handlers/cue.ts`:
- Around line 221-246: The current prune call only uses referencedPaths built
from options.promptFiles so files referenced solely inside the YAML
(options.content) can be deleted; before calling pruneOrphanedPromptFiles,
derive the keep-set from the YAML content written by writeCueConfigFile by
parsing options.content to extract any referenced prompt file paths (e.g.,
pipeline/agent prompt keys that resolve to .maestro/prompts/*.md), normalize
them to relative paths like referencedPaths uses, then take the union of that
set and the existing referencedPaths (and any explicitly written via
writeCuePromptFile) and pass that union into
pruneOrphanedPromptFiles(options.projectRoot, keepPaths) so missing/partial
options.promptFiles won't cause referenced files to be pruned.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 69-77: The silent catch-all blocks around the cueService paths
(notably where cueService.validateYaml is awaited and the outer try blocks in
CueYamlEditor.tsx) should not swallow unexpected errors: change the catch blocks
to capture/report the exception (e.g., call Sentry.captureException(err) or
captureMessage with context) and then re-throw or return as appropriate; keep
the existing recoverable handling for validation failures (still setIsValid /
setValidationErrors when not cancelled) but ensure unexpected exceptions are
forwarded to telemetry (or re-thrown) so they surface in production—apply the
same change to the other catch at the 143-151 area.
- Around line 136-149: The refreshYamlFromDisk callback incorrectly ignores an
intentionally empty YAML because it checks `if (content)`; change that check to
explicitly test for null/undefined (e.g., `if (content != null)` or `if (content
!== undefined && content !== null)`) so that an empty string "" from
`cueService.readYaml(projectRoot)` still triggers `setYamlContent`,
`setOriginalContent`, and the subsequent validation call
(`cueService.validateYaml`, `setIsValid`, `setValidationErrors`).

In `@src/renderer/components/SessionList/SessionList.tsx`:
- Around line 147-163: The catch in fetchCueStatus around cueService.getStatus()
currently swallows all errors; update the fetchCueStatus error handling to
detect and ignore only the known recoverable condition (e.g., check error.code
or error.message for the engine-not-initialized indicator) and leave the rest to
be reported: call Sentry.captureException(error) (or captureMessage) for
unexpected errors and re-throw them so they surface, while keeping the existing
behavior of returning early when the engine is simply not ready; references:
fetchCueStatus function, cueService.getStatus(), and setCueSessionMap.

In `@src/renderer/hooks/remote/useRemoteIntegration.ts`:
- Around line 887-895: The catch block in the onRemoteTriggerCueSubscription
handler currently only logs the error and returns false; update it to explicitly
report the failure to Sentry by importing and calling captureException (and
optionally captureMessage) from src/utils/sentry.ts inside the catch, passing
the caught error plus contextual metadata (subscriptionName, responseChannel,
prompt) so Sentry records the failure; keep the existing console.error and
window.maestro.process.sendRemoteTriggerCueSubscriptionResponse(responseChannel,
false) behavior after reporting.

---

Nitpick comments:
In `@src/__tests__/main/cue/cue-session-lifecycle.test.ts`:
- Around line 633-660: The test uses subscriptions: [] so initSession(session,
...) never registers any trigger sources — change the test to either provide a
real subscription/trigger in the mock Cue config or rename the test to reflect
it only validates registry deduplication; specifically update mockLoadCueConfig
call to return a config with at least one trigger/subscription (so initSession
on CueSessionRuntimeService will create timers/watchers/pollers and register
sources in the registry created by createCueSessionRegistry), or if you prefer
keep empty subscriptions then rename the test title and assertions to state it
only verifies registry dedupe for initSession rather than "does not
double-register trigger sources".

In
`@src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx`:
- Around line 192-250: The tests currently set mockNodesInitialized true before
mounting which hides a missing post-mount flip of the useNodesInitialized()
dependency; update the relevant tests (the ones named "fits view once nodes have
been measured", "restores saved viewport once nodes have been measured (does NOT
fitView)", "consumes the pending saved viewport after applying it", and "runs
the initial viewport step exactly once") to simulate the real regression path by
mounting with mockNodesInitialized = false, calling renderEditor(), then setting
mockNodesInitialized = true and forcing a post-mount update (e.g., rerender or
act) so the effect depending on useNodesInitialized() runs; use the existing
helpers mockPendingSavedViewportRef, mockFitView, mockSetViewport and
renderEditor()/rerender to drive and then assert the same expectations.

In `@src/renderer/hooks/useCue.ts`:
- Line 4: The refresh() function in useCue.ts still calls window.maestro.cue.*
directly, causing mixed IPC usage; change refresh() to use the cueService
read/get methods (e.g., cueService.getCue, cueService.getAllCues or whichever
read APIs exist) instead of window.maestro.cue.* and remove direct
window.maestro calls; also update any similar direct reads between lines ~61-99
to their cueService equivalents so all read and subscription logic consistently
uses the wrapper (keep existing write/subscription changes intact and ensure
return types/error handling match cueService methods).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5353fe2-1332-42d4-b41b-2f259b79908a

📥 Commits

Reviewing files that changed from the base of the PR and between c530bbb and 1d917e5.

📒 Files selected for processing (55)
  • src/__tests__/main/cue/cue-cleanup-service.test.ts
  • src/__tests__/main/cue/cue-completion-chains.test.ts
  • src/__tests__/main/cue/cue-concurrency.test.ts
  • src/__tests__/main/cue/cue-db.test.ts
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-executor.test.ts
  • src/__tests__/main/cue/cue-fan-in-tracker.test.ts
  • src/__tests__/main/cue/cue-ipc-handlers.test.ts
  • src/__tests__/main/cue/cue-multi-hop-chains.test.ts
  • src/__tests__/main/cue/cue-process-lifecycle.test.ts
  • src/__tests__/main/cue/cue-run-manager.test.ts
  • src/__tests__/main/cue/cue-session-lifecycle.test.ts
  • src/__tests__/main/cue/cue-session-registry.test.ts
  • src/__tests__/main/cue/cue-sleep-prevention.test.ts
  • src/__tests__/main/cue/cue-sleep-wake.test.ts
  • src/__tests__/main/cue/cue-startup.test.ts
  • src/__tests__/main/cue/cue-yaml-loader.test.ts
  • src/__tests__/renderer/components/CueModal.test.tsx
  • src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.allPipelinesViewLock.test.tsx
  • src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.drag.test.tsx
  • src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx
  • src/__tests__/renderer/components/CuePipelineEditor/utils/pipelineLayout.test.ts
  • src/__tests__/renderer/components/CuePipelineEditor/utils/yamlToPipeline.test.ts
  • src/__tests__/renderer/components/CueYamlEditor.test.tsx
  • src/__tests__/renderer/hooks/cue/usePipelineLayout.test.ts
  • src/__tests__/renderer/hooks/cue/usePipelineState.test.ts
  • src/__tests__/renderer/services/cue.test.ts
  • src/__tests__/renderer/stores/cueDirtyStore.test.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/cue/config/cue-config-validator.ts
  • src/main/cue/cue-cleanup-service.ts
  • src/main/cue/cue-db.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-fan-in-tracker.ts
  • src/main/cue/cue-heartbeat.ts
  • src/main/cue/cue-process-lifecycle.ts
  • src/main/cue/cue-query-service.ts
  • src/main/cue/cue-run-manager.ts
  • src/main/cue/cue-session-registry.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/main/ipc/handlers/cue.ts
  • src/renderer/components/CueModal/CueModal.tsx
  • src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx
  • src/renderer/components/CuePipelineEditor/PipelineCanvas.tsx
  • src/renderer/components/CuePipelineEditor/utils/pipelineLayout.ts
  • src/renderer/components/CuePipelineEditor/utils/yamlToPipeline.ts
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/components/SessionList/SessionList.tsx
  • src/renderer/hooks/cue/usePipelineLayout.ts
  • src/renderer/hooks/cue/usePipelineState.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/renderer/hooks/useCue.ts
  • src/renderer/services/cue.ts
  • src/renderer/stores/cueDirtyStore.ts

Comment on lines +137 to +140
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
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

Do not silently swallow filesystem errors in prune traversal

Both catch blocks suppress all failures, which hides unexpected runtime errors during save and prevents Sentry visibility.

Proposed fix
+import { captureException } from '../../../utils/sentry';
+
 	const walk = (dir: string) => {
 		let entries: fs.Dirent[];
 		try {
 			entries = fs.readdirSync(dir, { withFileTypes: true });
-		} catch {
-			return;
+		} catch (error) {
+			const err = error as NodeJS.ErrnoException;
+			if (err.code === 'ENOENT') return;
+			captureException(error, {
+				context: 'Cue pruneOrphanedPromptFiles readdirSync failed',
+				extra: { projectRoot, dir },
+			});
+			throw error;
 		}
 		for (const entry of entries) {
 			const abs = path.join(dir, entry.name);
@@
 			try {
 				fs.unlinkSync(abs);
 				removed.push(abs);
-			} catch {
-				// Ignore — a file we can't delete is not worth failing the save.
+			} catch (error) {
+				const err = error as NodeJS.ErrnoException;
+				if (err.code === 'ENOENT') continue;
+				captureException(error, {
+					context: 'Cue pruneOrphanedPromptFiles unlinkSync failed',
+					extra: { projectRoot, file: abs },
+				});
+				throw error;
 			}
 		}
 	};

As per coding guidelines: “Do not silently swallow errors… For unexpected errors, re-throw them… Use Sentry utilities (captureException, captureMessage) from src/utils/sentry.ts.”

Also applies to: 152-157

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

In `@src/main/cue/config/cue-config-repository.ts` around lines 137 - 140, The
try/catch around fs.readdirSync (the block that currently returns on error) must
not silently swallow errors—import and call captureException (and optionally
captureMessage) from the Sentry utilities (src/utils/sentry.ts) with contextual
info, then re-throw the error for unexpected failures so callers can handle it;
apply the same treatment to the other catch block around the save/prune
traversal (the catch covering lines ~152-157) so both places use
captureException(...) and then throw the caught error.

Comment on lines +66 to +67
/** Tracks when the first completion arrived for each tracker key (for cleanup staleness checks). */
const fanInCreatedAt = new Map<string, number>();
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

Clear fanInCreatedAt on successful fan-in completion too.

Line 218 only deletes fanInTrackers. Since the new timestamp map is cleaned up everywhere else, successful completions now leave orphaned fanInCreatedAt entries behind, and clearForSession() won't remove them later because it iterates active tracker keys only.

Possible fix
 			if (timer) {
 				clearTimeout(timer);
 				fanInTimers.delete(key);
 			}
 			fanInTrackers.delete(key);
+			fanInCreatedAt.delete(key);

 			const completions = [...tracker.values()];
 			const event = createCueEvent('agent.completed', sub.name, {

Also applies to: 184-187

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

In `@src/main/cue/cue-fan-in-tracker.ts` around lines 66 - 67, The issue is that
fanInCreatedAt entries are left behind when fan-in completes because only
fanInTrackers gets deleted; update the cleanup logic to also remove the
timestamp entries. Specifically, wherever fanInTrackers.delete(key) or the
fan-in successful completion path removes a tracker (e.g., the completion
handler and the block that currently deletes fanInTrackers), also call
fanInCreatedAt.delete(key); likewise, adjust clearForSession() to either remove
all fanInCreatedAt entries for the session or to iterate the same active keys it
uses for fanInTrackers and delete corresponding fanInCreatedAt entries so no
orphaned timestamps remain.

…bility

Verified each review finding against the current code and applied fixes
where the bug was real:

Real bugs:
- cue-fan-in-tracker: success completion deleted fanInTrackers but left
  fanInCreatedAt entries behind, leaking timestamp keys forever (every
  other path — timeout/break, clearForSession, expireTracker, reset —
  already cleaned up correctly).
- cue-run-manager: when the output-prompt onCueRun rejected, the
  outputRunId DB row was created with status 'running' but never
  finalized, leaving phantom never-ending entries in the activity log.
  Wrap the inner await; mark the row 'failed' on rejection then re-throw.
- cue-yaml-loader: partitionValidSubscriptions returned errors keyed by
  RAW yaml indices, but parseCueConfigDocument silently skips non-object
  entries, so the indices drifted and the lenient filter dropped the
  wrong subscriptions. Build a raw->normalized translation table; filter
  by translated indices.
- CuePipelineEditor: only onNodeContextMenu blocked in All Pipelines
  view — the Configure/Delete/Duplicate handlers themselves had no guard,
  so an already-open menu could still mutate state if the user switched
  views. Re-check isAllPipelinesView in each handler.
- usePipelineState: previousRoots was rebuilt from savedStateRef.current
  which fails when an agent has been renamed/removed since the last save
  (sessionId/Name no longer resolve). Persist a dedicated
  lastWrittenRootsRef on every successful save (and seed it from the
  initial load), use that for the clear set. Empty-YAML clears now use
  the same write-and-verify path as non-empty writes so they can't be a
  silent no-op.
- ipc/handlers/cue:writeYaml: prune keep-set was built only from
  promptFiles, so a YAML referencing pre-existing prompt files that
  weren't re-passed would have those files deleted as orphans. Parse the
  YAML and union prompt_file/output_prompt_file paths into the keep-set.
- CueYamlEditor.refreshYamlFromDisk: `if (content)` skipped intentionally
  empty YAML. Use `if (content != null)` so an empty string still
  triggers state updates and revalidation.

Observability (Sentry) at silent catches:
- cue-db: safeRecordCueEvent / safeUpdateCueEventStatus now also
  captureException alongside the existing warn (kept non-throwing).
- cue-config-repository.pruneOrphanedPromptFiles: report readdir/unlink
  failures to Sentry. Deliberately NOT re-throwing — pruning runs after a
  successful YAML write, and re-throwing would surface a failed-save
  toast even though the YAML did persist.
- ipcWrapper.createIpcMethod: capture on the swallow path (defaultValue),
  rethrow path leaves reporting to the caller.
- CueYamlEditor: load/refresh catches now report unexpected exceptions.
- SessionList.fetchCueStatus: report unexpected errors; ignore the known
  recoverable "Cue engine not initialized" message.
- useRemoteIntegration: remote Cue trigger catch now captureExceptions
  with subscription/channel/prompt context.

Tests:
- cue-session-lifecycle: renamed the idempotency test + comment to
  reflect that empty subscriptions test only registry dedupe, not
  trigger-source dedupe (which would need a wired createTriggerSource).
- CuePipelineEditor.initialViewport: refactored 4 tests to mount with
  mockNodesInitialized=false then flip to true and rerender, so they
  exercise the real false->true regression path through the effect's
  dependency change instead of asserting after a single mount with true.
- usePipelineLayout: createDefaultParams now provides the new
  lastWrittenRootsRef.

Pushed back on:
- pruneOrphanedPromptFiles re-throwing: rejected because re-throwing
  would surface "save failed" for a save that actually persisted.
- Migrating useCue.refresh() to cueService.* reads: cueService swallows
  IPC failures and returns defaults, which would silently kill useCue's
  user-visible error banner on the dashboard. Reverted reads to direct
  window.maestro.cue.* with a comment explaining why this hook is the
  exception. Writes (enable/disable/stop/trigger) stay on cueService.

Tests: 25,260 passing.
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/services/ipcWrapper.ts (1)

90-104: ⚠️ Potential issue | 🟠 Major

Unexpected transform errors are currently swallowed in default mode.

Because options.transform(...) is inside the same try as options.call(), a transform bug can be caught and converted to defaultValue (Line 91 to Line 104), masking real defects. Keep fallback behavior for IPC-call failures, but let transform exceptions bubble.

💡 Suggested fix
 export async function createIpcMethod<T>(options: IpcMethodOptions<T>): Promise<T> {
+	let result: T;
 	try {
-		const result = await options.call();
-		return options.transform ? options.transform(result) : result;
+		result = await options.call();
 	} catch (error) {
 		console.error(`${options.errorContext} error:`, error);
 		if (options.rethrow) {
 			// Caller is responsible for handling/reporting.
 			throw error;
 		}
 		// Swallow path: the caller never sees this error, so report it to
 		// Sentry here — otherwise IPC failures behind read methods (return
 		// default on error) would be invisible in production.
 		void captureException(error, { extra: { context: options.errorContext } });
 		return options.defaultValue as T;
 	}
+	// Transform failures are unexpected; do not swallow them.
+	return options.transform ? options.transform(result) : result;
 }

As per coding guidelines: “Handle expected/recoverable errors explicitly (e.g., NETWORK_ERROR). For unexpected errors, re-throw them to allow Sentry to capture them.”

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

In `@src/renderer/services/ipcWrapper.ts` around lines 90 - 104, In
createIpcMethod, the current try/catch wraps both options.call() and
options.transform(), so transform errors get swallowed and converted to
defaultValue; change the flow to only catch call() failures: await
options.call() inside a try/catch that logs, conditionally rethrows (respecting
options.rethrow), sends call errors to captureException and returns
options.defaultValue on IPC-call failure, then apply options.transform(result)
outside the catch so any transform exceptions bubble up (do not catch/convert
them); reference createIpcMethod, options.call, options.transform,
options.rethrow, captureException, and options.defaultValue when making the
change.
♻️ Duplicate comments (1)
src/renderer/hooks/cue/usePipelineLayout.ts (1)

154-171: ⚠️ Potential issue | 🟠 Major

lastWrittenRootsRef can still forget roots after a session rename/removal.

This set is rebuilt only from the current sessions lookup. If a previously-written pipeline's agent no longer resolves by id/name before the editor opens, its original root never lands in loadedRoots, so the next save cannot clear that stale .maestro/cue.yaml. Seed this from data that already carries the root, or persist the written-root set alongside the saved layout and reuse it here.

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

In `@src/renderer/hooks/cue/usePipelineLayout.ts` around lines 154 - 171,
lastWrittenRootsRef is only seeded from resolving agent nodes via sessions and
thus can miss roots when a session was renamed/removed; update the seeding logic
in usePipelineLayout to also incorporate any persisted written-root info from
the saved layout or from node payloads that already include a root field so
stale roots aren't lost. Specifically, when building loadedRoots (the Set used
to initialize lastWrittenRootsRef.current), merge in roots from the layout's
saved written-roots metadata (if present) or from pipeline.node.data (e.g., a
root property on AgentNodeData) before or in addition to looking up
sessionsById/sessionsByName, and ensure lastWrittenRootsRef.current is set from
this combined set so renames/removals do not drop previously written roots.
🧹 Nitpick comments (1)
src/main/ipc/handlers/cue.ts (1)

221-275: Move the save/prune orchestration back out of the IPC handler.

This block now does path validation, prompt-file writes, YAML parsing, keep-set derivation, and orphan cleanup. That breaks the module’s own “thin transport layer” contract and makes this save path harder to reuse and test outside Electron. A single domain-level writeCueConfig... entry point would keep the handler back to delegation.

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

In `@src/main/ipc/handlers/cue.ts` around lines 221 - 275, The IPC handler
currently performs validation, writes prompt files, writes the YAML, parses it
and prunes orphans — move that orchestration into a single domain API (e.g.
export a function like writeCueConfigAndPrune(projectRoot, content,
promptFiles)) so the handler just delegates. Implement writeCueConfigAndPrune to
perform the path validation (same checks using path.isAbsolute and promptsBase),
call writeCuePromptFile for each prompt, call writeCueConfigFile, parse via
yaml.load to collect prompt_file/output_prompt_file into a keepPaths Set, and
finally call pruneOrphanedPromptFiles; then replace the large block in the IPC
handler with a single call to the new function and proper error handling. Ensure
the new function uses the existing helpers writeCuePromptFile,
writeCueConfigFile, pruneOrphanedPromptFiles and preserves the current try/catch
fallback behavior for yaml.load.
🤖 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/main/cue/cue-db.ts`:
- Around line 207-211: The current safeRecordCueEvent sends the entire event
(including payload with possible sensitive sourceOutput) to captureException;
change this to send only minimal metadata by creating a sanitizedEvent that
strips or replaces event.payload (e.g., set payload to undefined or to a small
object containing non-sensitive fields such as payloadSize and truncated: true)
and include only identifiers like event.id, event.type, event.status, and runId
before calling captureException(err, { operation: 'safeRecordCueEvent', event:
sanitizedEvent }); ensure safeRecordCueEvent still swallows errors and does not
rethrow.

In `@src/main/ipc/handlers/cue.ts`:
- Around line 248-275: The current try/catch around yaml.load(...) can swallow a
parse error and still call pruneOrphanedPromptFiles(...), risking mass deletion;
fix by either (A) computing the keepPaths set from options.content (parsing
subscriptions) before calling writeCueConfigFile(), or (B) if yaml.load throws,
do not call pruneOrphanedPromptFiles(...) — return/skip pruning — and report the
error via the Sentry helper (captureException) with context (e.g., include
options.projectRoot and a note that parse failed for writeCueConfigFile flow);
update references to yaml.load, keepPaths, pruneOrphanedPromptFiles, and
writeCueConfigFile accordingly so pruning only runs when parsing succeeded and
errors are explicitly captured.

In `@src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx`:
- Around line 311-322: The current effect delays applying
pendingSavedViewportRef until nodesInitialized and computedNodes exist; change
it so the saved-viewport branch runs immediately: inside the useEffect for
hasInitialFitRef, first check if hasInitialFitRef.current is false and if
pendingSavedViewportRef.current exists — if so, clear
pendingSavedViewportRef.current, call reactFlowInstance.setViewport(saved) and
set hasInitialFitRef.current = true; otherwise (no saved viewport) only then
check the existing nodesInitialized && computedNodes.length > 0 condition and
call reactFlowInstance.fitView({ padding: 0.15, duration: 200 }) and set
hasInitialFitRef.current = true; keep references to pendingSavedViewportRef,
nodesInitialized, computedNodes.length and reactFlowInstance so the logic
targets the same symbols.

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 70-73: Wrap each call to cueService.validateYaml(...) (the calls
that currently setIsValid(...) and setValidationErrors(...), including the
occurrences near where setIsValid and setValidationErrors are used) in a
try/catch; in the try keep the existing behavior, and in catch (if not
cancelled) setIsValid(false) and setValidationErrors to a meaningful error entry
(e.g., error.message or String(error)) to ensure Save is gated, then re-throw
the error so it is not silently swallowed (this applies to the validateYaml call
sites around the current setIsValid/setValidationErrors usages and the other two
occurrences mentioned).

In `@src/renderer/hooks/remote/useRemoteIntegration.ts`:
- Around line 895-901: The captureException call inside
remoteTriggerCueSubscription currently sends the full prompt in telemetry;
update the code to avoid sending raw prompt text by redacting or replacing
prompt with a safe diagnostic (e.g., a fixed redacted string, prompt length, or
a hash/checksum) before passing it into captureException's extra metadata; keep
subscriptionName and responseChannel as-is but ensure the prompt field is
replaced (or omitted) to prevent PII/secrets leakage.

---

Outside diff comments:
In `@src/renderer/services/ipcWrapper.ts`:
- Around line 90-104: In createIpcMethod, the current try/catch wraps both
options.call() and options.transform(), so transform errors get swallowed and
converted to defaultValue; change the flow to only catch call() failures: await
options.call() inside a try/catch that logs, conditionally rethrows (respecting
options.rethrow), sends call errors to captureException and returns
options.defaultValue on IPC-call failure, then apply options.transform(result)
outside the catch so any transform exceptions bubble up (do not catch/convert
them); reference createIpcMethod, options.call, options.transform,
options.rethrow, captureException, and options.defaultValue when making the
change.

---

Duplicate comments:
In `@src/renderer/hooks/cue/usePipelineLayout.ts`:
- Around line 154-171: lastWrittenRootsRef is only seeded from resolving agent
nodes via sessions and thus can miss roots when a session was renamed/removed;
update the seeding logic in usePipelineLayout to also incorporate any persisted
written-root info from the saved layout or from node payloads that already
include a root field so stale roots aren't lost. Specifically, when building
loadedRoots (the Set used to initialize lastWrittenRootsRef.current), merge in
roots from the layout's saved written-roots metadata (if present) or from
pipeline.node.data (e.g., a root property on AgentNodeData) before or in
addition to looking up sessionsById/sessionsByName, and ensure
lastWrittenRootsRef.current is set from this combined set so renames/removals do
not drop previously written roots.

---

Nitpick comments:
In `@src/main/ipc/handlers/cue.ts`:
- Around line 221-275: The IPC handler currently performs validation, writes
prompt files, writes the YAML, parses it and prunes orphans — move that
orchestration into a single domain API (e.g. export a function like
writeCueConfigAndPrune(projectRoot, content, promptFiles)) so the handler just
delegates. Implement writeCueConfigAndPrune to perform the path validation (same
checks using path.isAbsolute and promptsBase), call writeCuePromptFile for each
prompt, call writeCueConfigFile, parse via yaml.load to collect
prompt_file/output_prompt_file into a keepPaths Set, and finally call
pruneOrphanedPromptFiles; then replace the large block in the IPC handler with a
single call to the new function and proper error handling. Ensure the new
function uses the existing helpers writeCuePromptFile, writeCueConfigFile,
pruneOrphanedPromptFiles and preserves the current try/catch fallback behavior
for yaml.load.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3d29dc0-bde3-430d-898f-1871027a13e7

📥 Commits

Reviewing files that changed from the base of the PR and between 1d917e5 and 5991ee0.

📒 Files selected for processing (17)
  • src/__tests__/main/cue/cue-session-lifecycle.test.ts
  • src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx
  • src/__tests__/renderer/hooks/cue/usePipelineLayout.test.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/cue/cue-db.ts
  • src/main/cue/cue-fan-in-tracker.ts
  • src/main/cue/cue-run-manager.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/main/ipc/handlers/cue.ts
  • src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/components/SessionList/SessionList.tsx
  • src/renderer/hooks/cue/usePipelineLayout.ts
  • src/renderer/hooks/cue/usePipelineState.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/renderer/hooks/useCue.ts
  • src/renderer/services/ipcWrapper.ts
✅ Files skipped from review due to trivial changes (3)
  • src/main/cue/cue-fan-in-tracker.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/tests/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main/cue/config/cue-config-repository.ts
  • src/renderer/hooks/useCue.ts
  • src/tests/main/cue/cue-session-lifecycle.test.ts
  • src/tests/renderer/hooks/cue/usePipelineLayout.test.ts
  • src/main/cue/cue-run-manager.ts
  • src/renderer/components/SessionList/SessionList.tsx

…te save, immediate viewport restore

Verified each finding and applied fixes where the bug was real:

PII / telemetry safety:
- cue-db.safeRecordCueEvent: stripped event.payload from Sentry payload —
  agent.completed events carry sourceOutput (upstream agent stdout), which
  can include user content / secrets. Send identifiers + payloadSize only.
- useRemoteIntegration.remoteTriggerCueSubscription: never send the raw
  prompt string to telemetry; replaced with promptLength + promptProvided
  so we can correlate failures against payload size without leaking body.

Real bugs:
- ipc/handlers/cue:writeYaml: previous catch around yaml.load fell through
  to pruneOrphanedPromptFiles with a partial keep-set, risking mass-delete
  of prompt files referenced only inside the unparseable YAML. Track
  parseSucceeded; on failure, captureException with context and SKIP the
  prune entirely (next successful save catches up). Parse now happens
  before the YAML write so the keep-set is authoritative when used.
- CueYamlEditor: the validate-on-load and validate-on-refresh catches
  reported to Sentry but never set isValid=false, so the initial-state
  isValid=true left Save enabled even though validation never ran. Now
  setIsValid(false) and setValidationErrors(message) in both catches so
  Save is gated and the user sees what failed.
- ipcWrapper.createIpcMethod: the single try/catch wrapped both
  options.call() and options.transform(), so transform-function exceptions
  were silently converted to defaultValue, masking real bugs. Restructured
  so only call() is in the catch; transform runs after and propagates.
- CuePipelineEditor initial viewport: setViewport (pure x/y/zoom restore)
  doesn't depend on node measurement, but the effect was waiting for
  useNodesInitialized + computedNodes.length>0 before applying. Split the
  effect: saved viewport applies immediately on mount; fitView fallback
  still gates on measurement (that path needs node bounds).
- usePipelineLayout / PipelineLayoutState: lastWrittenRootsRef could miss
  roots when an agent was renamed/removed before editor open (sessionId
  /Name lookup misses the project root, leaving stale YAML uncleared on
  the next save). Added writtenRoots?: string[] to PipelineLayoutState;
  persistLayout writes it from the ref; the load-time seed unions the
  persisted set with the session-resolved set so renames/removals don't
  drop previously-written roots.

Tests:
- initialViewport.test.tsx: saved-viewport tests no longer flip
  mockNodesInitialized. They now assert setViewport fires immediately on
  mount, matching the new contract.

Pushed back on:
- Reviewer asked to re-throw inside CueYamlEditor's validateYaml catches.
  Skipped — re-throwing would propagate to the outer loadYaml catch,
  conflating validateYaml failures with readYaml failures and triggering
  the template fallback. Gating Save (the actual safety concern) is done.
- Reviewer suggested extracting writeCueConfigAndPrune into a domain API.
  Skipped (marked nitpick): the IPC handler is the natural home for path
  validation + multi-write coordination; extracting would create a
  one-call wrapper without separating concerns.

Tests: 25,260 passing.
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/ipc/handlers/cue.ts (1)

224-237: ⚠️ Potential issue | 🟠 Major

Reject .maestro/prompts itself and non-.md prompt targets.

Line 232 only checks lexical containment under .maestro/prompts, so keys like .maestro/prompts or .maestro/prompts/foo.txt still pass. The first can create a regular file where the prompts directory should live; the second creates files that pruneOrphanedPromptFiles() will never clean up because it only manages .md files.

🛠️ Proposed fix
-						if (!target.startsWith(promptsBase + path.sep) && target !== promptsBase) {
+						if (!target.startsWith(promptsBase + path.sep)) {
 							throw new Error(
 								`cue:writeYaml: promptFiles key "${relativePath}" resolves outside the .maestro/prompts directory`
 							);
 						}
+						if (path.extname(target).toLowerCase() !== '.md') {
+							throw new Error(
+								`cue:writeYaml: promptFiles key "${relativePath}" must point to a .md file under .maestro/prompts`
+							);
+						}

I'd mirror the same guard in writeCuePromptFile() as well, since that's the real trust boundary.

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

In `@src/main/ipc/handlers/cue.ts` around lines 224 - 237, Reject promptFiles
entries that resolve exactly to the prompts directory and any targets that do
not end with ".md": in the validation loop around writeCuePromptFile (the block
using promptsBase, relativePath, target) add checks to throw if target ===
promptsBase (rejecting ".maestro/prompts" itself) and if path.extname(target)
!== '.md' (reject non-.md targets); then duplicate the same validation inside
writeCuePromptFile(...) so the function enforces the same trust boundary as
pruneOrphanedPromptFiles and prevents creating non-markdown or
directory-conflicting files.
♻️ Duplicate comments (1)
src/renderer/components/CueYamlEditor/CueYamlEditor.tsx (1)

70-90: ⚠️ Potential issue | 🟠 Major

validateYaml() transport failures still bypass save gating.

cueService.validateYaml() falls back to { valid: true, errors: [] } on IPC errors, so these catches only handle local exceptions. A failed validation round can still leave isValid === true and re-enable Save even though validation never ran. Use a strict/throwing validation path here, or force this workflow to treat the fallback result as invalid.

As per coding guidelines src/**/*.{ts,tsx}: “Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production. Handle expected/recoverable errors explicitly.”

Also applies to: 115-120, 163-176

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

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx` around lines 70 -
90, validateYaml() may return a fallback success on IPC failures, which lets
isValid stay true and re-enable Save; change the handler around
cueService.validateYaml(initial) so transport failures are treated as validation
failures: either call a strict/throwing variant (e.g.,
cueService.validateYamlStrict or cueService.validateYaml(initial, { strict: true
})) so IPC errors propagate, or detect the fallback result shape and, if
present, setIsValid(false) and setValidationErrors([...]) instead of trusting
validationResult.valid; also stop swallowing unexpected exceptions — after
captureException(err, ...) re-throw the error so unhandled exceptions bubble to
Sentry (while still guarding cancelled before calling
setIsValid/setValidationErrors).
🤖 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/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 64-67: The code currently treats cueService.readYaml() returning
null as “no cue.yaml” which hides IPC/read failures; change the read path so
cueService.readYaml() distinguishes a missing file from an IPC error (or, if
changing the service isn't feasible, detect content === null in the caller and
throw or surface an error instead of defaulting to CUE_YAML_TEMPLATE).
Specifically, update the logic around cueService.readYaml(...) /
setYamlContent(...) / originalContent so only an explicit “file not found”
sentinel leads to using CUE_YAML_TEMPLATE, whereas a null return causes an
exception (or propagates the IPC error) so it can be handled by your existing
catch and reported; apply the same fix to the other usages noted (the blocks
around the second/third/fourth read locations).

In `@src/renderer/hooks/cue/usePipelineLayout.ts`:
- Around line 159-188: The persisted writtenRoots are only restored after an
early return when graphSessions is empty; move the saved writtenRoots restore so
it runs even when the live graph is empty. Concretely, in the effect inside
usePipelineLayout (the block that builds loadedRoots from
savedLayout.writtenRoots, sessionsById/sessionsByName, and pipelinesForRoots and
sets lastWrittenRootsRef.current), relocate or duplicate that logic to run
before the early return that checks graphSessions.length === 0 (or otherwise
ensure savedLayout.writtenRoots is applied regardless of graphSessions). Keep
using the same symbols (savedLayout, writtenRoots, loadedRoots,
sessionsById/sessionsByName, pipelinesForRoots, lastWrittenRootsRef) so
orphan-root metadata is rehydrated on startup even with no live pipelines.

In `@src/renderer/hooks/remote/useRemoteIntegration.ts`:
- Line 4: The import of captureException in useRemoteIntegration.ts is using a
wrong relative path; update the import to use the canonical shared Sentry
utility (import captureException from the root Sentry util module) by replacing
the '../../utils/sentry' import with the root module path (src/utils/sentry) so
that the file uses the centralized captureException implementation.

---

Outside diff comments:
In `@src/main/ipc/handlers/cue.ts`:
- Around line 224-237: Reject promptFiles entries that resolve exactly to the
prompts directory and any targets that do not end with ".md": in the validation
loop around writeCuePromptFile (the block using promptsBase, relativePath,
target) add checks to throw if target === promptsBase (rejecting
".maestro/prompts" itself) and if path.extname(target) !== '.md' (reject non-.md
targets); then duplicate the same validation inside writeCuePromptFile(...) so
the function enforces the same trust boundary as pruneOrphanedPromptFiles and
prevents creating non-markdown or directory-conflicting files.

---

Duplicate comments:
In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx`:
- Around line 70-90: validateYaml() may return a fallback success on IPC
failures, which lets isValid stay true and re-enable Save; change the handler
around cueService.validateYaml(initial) so transport failures are treated as
validation failures: either call a strict/throwing variant (e.g.,
cueService.validateYamlStrict or cueService.validateYaml(initial, { strict: true
})) so IPC errors propagate, or detect the fallback result shape and, if
present, setIsValid(false) and setValidationErrors([...]) instead of trusting
validationResult.valid; also stop swallowing unexpected exceptions — after
captureException(err, ...) re-throw the error so unhandled exceptions bubble to
Sentry (while still guarding cancelled before calling
setIsValid/setValidationErrors).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c2d20918-2d06-4ea0-ae5b-703f58d9a331

📥 Commits

Reviewing files that changed from the base of the PR and between 5991ee0 and cb93fea.

📒 Files selected for processing (9)
  • src/__tests__/renderer/components/CuePipelineEditor/CuePipelineEditor.initialViewport.test.tsx
  • src/main/cue/cue-db.ts
  • src/main/ipc/handlers/cue.ts
  • src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx
  • src/renderer/components/CueYamlEditor/CueYamlEditor.tsx
  • src/renderer/hooks/cue/usePipelineLayout.ts
  • src/renderer/hooks/remote/useRemoteIntegration.ts
  • src/renderer/services/ipcWrapper.ts
  • src/shared/cue-pipeline-types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/cue/cue-db.ts
  • src/renderer/components/CuePipelineEditor/CuePipelineEditor.tsx
  • src/renderer/services/ipcWrapper.ts

Comment on lines +64 to 67
const content = await cueService.readYaml(projectRoot);
if (cancelled) return;
const initial = content ?? CUE_YAML_TEMPLATE;
setYamlContent(initial);
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 treat read failures as “no cue.yaml”.

cueService.readYaml() resolves to null on IPC errors, so these catch blocks never see the common failure path. On initial load, that null is converted into CUE_YAML_TEMPLATE and stored as originalContent, which can let a transient read failure overwrite a real cue.yaml on the next save; on refresh, the same failure becomes a silent no-op. This needs a strict read path, or a return shape that distinguishes ENOENT from “failed to read”.

As per coding guidelines src/**/*.{ts,tsx}: “Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production. Handle expected/recoverable errors explicitly.”

Also applies to: 92-95, 154-159, 179-180

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

In `@src/renderer/components/CueYamlEditor/CueYamlEditor.tsx` around lines 64 -
67, The code currently treats cueService.readYaml() returning null as “no
cue.yaml” which hides IPC/read failures; change the read path so
cueService.readYaml() distinguishes a missing file from an IPC error (or, if
changing the service isn't feasible, detect content === null in the caller and
throw or surface an error instead of defaulting to CUE_YAML_TEMPLATE).
Specifically, update the logic around cueService.readYaml(...) /
setYamlContent(...) / originalContent so only an explicit “file not found”
sentinel leads to using CUE_YAML_TEMPLATE, whereas a null return causes an
exception (or propagates the IPC error) so it can be handled by your existing
catch and reported; apply the same fix to the other usages noted (the blocks
around the second/third/fourth read locations).

import { useEffect, useRef } from 'react';
import type { Session, SessionState, ThinkingMode } from '../../types';
import { cueService } from '../../services/cue';
import { captureException } from '../../utils/sentry';
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

Use the canonical Sentry utility import path.

Line 4 imports captureException from ../../utils/sentry, but this path does not point to the required shared utility module. Please import from the root Sentry utility path instead.

Suggested fix
-import { captureException } from '../../utils/sentry';
+import { captureException } from '../../../utils/sentry';

As per coding guidelines, "Use Sentry utilities (captureException, captureMessage) from src/utils/sentry.ts for explicit error reporting with context."

📝 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
import { captureException } from '../../utils/sentry';
import { captureException } from '../../../utils/sentry';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/remote/useRemoteIntegration.ts` at line 4, The import of
captureException in useRemoteIntegration.ts is using a wrong relative path;
update the import to use the canonical shared Sentry utility (import
captureException from the root Sentry util module) by replacing the
'../../utils/sentry' import with the root module path (src/utils/sentry) so that
the file uses the centralized captureException implementation.

…st boundary, orphan-root reseed

Verified each finding and applied fixes where the bug was real:

IPC contract — distinguish "no data" from "transport failed":
- cueService.readYaml: previously `defaultValue: null` collapsed "file
  doesn't exist" and "IPC error" into the same null return, hiding
  transport failures. Switched to `rethrow: true`. Handler still returns
  null for missing file; IPC errors propagate so callers (CueYamlEditor's
  outer catches, handleSave's write-back verification) react correctly.
  handleSave's "did not persist" message is strictly more informative —
  the actual IPC error message now propagates instead.
- cueService.validateYaml: previously `defaultValue: { valid: true,
  errors: [] }` on IPC failure — actively dangerous because a transport
  error would surface as "yaml is valid, save freely". Switched to
  `rethrow: true`. CueYamlEditor's three call sites already catch the
  rejection and gate Save by setting isValid=false.

writtenRoots reseed independent of graphSessions:
- usePipelineLayout: the writtenRoots seeding lived inside the main
  load-layout effect, which early-returns when graphSessions is empty —
  so opening the editor with no live sessions (engine disabled, no
  registered sessions) never restored orphan-root metadata, and a "delete
  the orphaned pipeline" save would leave the stale YAML at that root
  uncleared. Added a standalone effect that runs once on mount and
  unions persisted writtenRoots into lastWrittenRootsRef.current. The
  main effect still re-builds the full set when graphSessions arrive.

Prompt-file trust boundary:
- ipc/handlers/cue:writeYaml: tightened promptFiles entry validation to
  reject targets that resolve to the prompts directory itself
  (`target === promptsBase`) and to non-.md files (the latter would never
  get pruned, accumulating orphans permanently because
  pruneOrphanedPromptFiles only removes .md files).
- writeCuePromptFile: duplicated the same checks inside the function so
  any internal caller hits the same trust boundary as IPC callers (defense
  in depth — the IPC handler's checks were the only enforcement before).

Tests updated for new contracts:
- services/cue.test.ts: readYaml asserts pass-through-null on missing
  file + throws on IPC error. validateYaml asserts throws on IPC error.
- usePipelineLayout.test.ts: "does not restore layout when graphSessions
  is empty" updated — pipeline state is still untouched, but
  loadPipelineLayout may now fire once for the standalone writtenRoots
  reseed effect.

Pushed back on:
- Reviewer claimed `'../../utils/sentry'` in useRemoteIntegration is the
  wrong path. Verified: it correctly resolves to
  src/renderer/utils/sentry.ts, and every other renderer file uses the
  same pattern. There is no src/utils/sentry.ts. No change.

Tests: 25,261 passing.
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/hooks/cue/usePipelineLayout.ts (1)

147-150: ⚠️ Potential issue | 🟠 Major

Invalidate stale restores before the empty-graph return.

Line 148 can return before latestRestoreIdRef is bumped. If graphSessions flips to [] while the previous cueService.loadPipelineLayout() call is still pending, the stale request still passes the reqId guard and re-applies the old pipelines into state.

Suggested fix
 useEffect(() => {
-	if (hasRestoredLayoutRef.current) return;
-	if (!graphSessions || graphSessions.length === 0) return;
-
-	const reqId = ++latestRestoreIdRef.current;
+	const reqId = ++latestRestoreIdRef.current;
+	if (hasRestoredLayoutRef.current) return;
+	if (!graphSessions || graphSessions.length === 0) return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/cue/usePipelineLayout.ts` around lines 147 - 150, Move the
bump of latestRestoreIdRef.current to occur before the early-return check for an
empty graph so stale async restores cannot pass the reqId guard; specifically,
increment latestRestoreIdRef.current (the reqId used to validate
cueService.loadPipelineLayout responses) before checking graphSessions.length
=== 0 and before returning, and ensure hasRestoredLayoutRef.current behavior
remains unchanged so any in-flight loadPipelineLayout responses with older reqId
are ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/renderer/hooks/cue/usePipelineLayout.ts`:
- Around line 147-150: Move the bump of latestRestoreIdRef.current to occur
before the early-return check for an empty graph so stale async restores cannot
pass the reqId guard; specifically, increment latestRestoreIdRef.current (the
reqId used to validate cueService.loadPipelineLayout responses) before checking
graphSessions.length === 0 and before returning, and ensure
hasRestoredLayoutRef.current behavior remains unchanged so any in-flight
loadPipelineLayout responses with older reqId are ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b7027e8-5fa1-468b-b289-03070ea0deb6

📥 Commits

Reviewing files that changed from the base of the PR and between cb93fea and 5848948.

📒 Files selected for processing (6)
  • src/__tests__/renderer/hooks/cue/usePipelineLayout.test.ts
  • src/__tests__/renderer/services/cue.test.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/ipc/handlers/cue.ts
  • src/renderer/hooks/cue/usePipelineLayout.ts
  • src/renderer/services/cue.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/ipc/handlers/cue.ts
  • src/tests/renderer/hooks/cue/usePipelineLayout.test.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/tests/renderer/services/cue.test.ts

@reachrazamair reachrazamair merged commit 0b6bba0 into rc Apr 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant