maestro-cue-spinout: 48 tasks across 2026-02-14-Maestro-Cue/MAESTRO-CUE-01, 2026-02-14-Maestro-Cue/MAESTRO-CUE-02 +12 more#488
maestro-cue-spinout: 48 tasks across 2026-02-14-Maestro-Cue/MAESTRO-CUE-01, 2026-02-14-Maestro-Cue/MAESTRO-CUE-02 +12 more#488pedramamini wants to merge 57 commits intomainfrom
Conversation
…r, and Encore feature flag - Register maestroCue as an Encore Feature flag (EncoreFeatureFlags, DEFAULT_ENCORE_FEATURES) - Create src/main/cue/cue-types.ts with CueEventType, CueSubscription, CueSettings, CueConfig, CueEvent, CueRunStatus, CueRunResult, CueSessionStatus, and related constants - Add 'CUE' to HistoryEntryType across shared types, global.d.ts, preload, IPC handlers, and hooks - Add cueTriggerName, cueEventType, cueSourceSession optional fields to HistoryEntry - Add 'cue' log level to MainLogLevel, LOG_LEVEL_PRIORITY, logger switch/case, and LogViewer with teal color (#06b6d4), always-enabled filter, and agent name pill - Add 10 Cue-specific template variables (CUE_EVENT_TYPE, CUE_TRIGGER_NAME, etc.) with cueOnly flag - Extend TemplateContext with cue? field and substituteTemplateVariables with Cue replacements - Update TEMPLATE_VARIABLES_GENERAL filter to exclude cueOnly variables
…ovider Implements the three core modules for the Cue event-driven automation engine: - cue-yaml-loader.ts: Discovers and parses maestro-cue.yaml files with js-yaml, validates config structure, watches for file changes via chokidar with 1-second debounce - cue-file-watcher.ts: Wraps chokidar for file.changed subscriptions with per-file debouncing (5s default), constructs CueEvent instances with full file metadata payloads - cue-engine.ts: Main coordinator class with dependency injection, manages time.interval timers (fires immediately then on interval), file watchers, agent.completed listeners with fan-in tracking, activity log ring buffer (max 500), and run lifecycle management Added js-yaml and @types/js-yaml dependencies. 57 tests across 3 test files.
…story recording Implements the Cue executor module that spawns background agent processes when Cue triggers fire, following the same spawn pattern as Auto Run's process:spawn IPC handler. Key exports: - executeCuePrompt(): Full 10-step pipeline (prompt resolution, template substitution, agent arg building, SSH wrapping, process spawn with stdout/stderr capture, timeout enforcement with SIGTERM→SIGKILL) - stopCueRun(): Graceful process termination by runId - recordCueHistoryEntry(): Constructs HistoryEntry with type 'CUE' and all Cue-specific fields (trigger name, event type, source session) - getActiveProcesses(): Monitor running Cue processes Test coverage: 31 tests in cue-executor.test.ts covering execution paths, SSH remote, timeout escalation, history entry construction, and edge cases. Full suite: 21,635 tests passing across 512 files, zero regressions.
…zation for Maestro Cue
Add CUE entry support across all History components: - HistoryFilterToggle: CUE filter button with teal (#06b6d4) color and Zap icon - HistoryEntryItem: CUE pill, success/failure badges, and trigger metadata subtitle - HistoryPanel & UnifiedHistoryTab: CUE included in default activeFilters - HistoryDetailModal: CUE pill color, icon, success/failure indicator, trigger metadata display - Comprehensive test coverage for all CUE rendering paths (205 new/updated tests pass)
…nd activity log Add the Maestro Cue dashboard modal with full Encore Feature gating: - CueModal component with sessions table, active runs list, and activity log - useCue hook for state management, event subscriptions, and 10s polling - Settings toggle in Encore tab, command palette entry, keyboard shortcut (Cmd+Shift+U) - SessionList hamburger menu entry, modal store integration, lazy loading - 30 tests covering hook behavior and modal rendering
Add CueYamlEditor component for creating and editing maestro-cue.yaml files. Features split-view layout with AI assist (left panel for description + clipboard copy) and YAML editor (right panel with line numbers, debounced validation, Tab indentation). Integrates into CueModal via Edit YAML button on each session row.
…yaml Task 1: CueHelpModal component with 7 content sections (What is Maestro Cue, Getting Started, Event Types, Template Variables, Multi-Agent Orchestration, Timeouts & Failure Handling, AI YAML Editor). Wired to CueModal ? button. Registered with layer stack at MODAL_PRIORITIES.CUE_HELP (465). Task 2: useCueAutoDiscovery hook that calls cue:refreshSession when sessions are created/restored/removed, gated by encoreFeatures.maestroCue. Full scan on feature enable, engine disable on feature off. Tests: 38 CueHelpModal tests + 10 useCueAutoDiscovery tests, all passing. Lint clean. No existing test regressions (21,778 tests pass).
… session bridging Implement agent completion event chaining in the Cue engine: - Fan-out: subscriptions dispatch prompts to multiple target sessions simultaneously - Fan-in: subscriptions wait for all source sessions to complete before firing, with timeout handling (break clears tracker, continue fires with partial data) - Session bridging: user session completions trigger Cue subscriptions via exit listener - Add AgentCompletionData type for rich completion event payloads - Add hasCompletionSubscribers() optimization to skip unneeded notifications - Wire getCueEngine/isCueEnabled into ProcessListenerDependencies
…ure gated) Add teal Zap icon next to session names in the Left Bar for sessions with active Maestro Cue subscriptions. The indicator is gated behind the maestroCue Encore Feature flag and shows a tooltip with the subscription count on hover. - Add cueSubscriptionCount prop to SessionItem with Zap icon rendering - Add lightweight Cue status fetching in SessionListInner via cue:getStatus IPC, refreshed on cue:activityUpdate events - Add cue namespace to global test setup mock - 6 unit tests + 3 integration tests; all 21,815 tests pass; lint clean
Add Maestro Cue entries across all developer documentation: - CLAUDE.md: Key Files table (4 entries), Architecture tree (cue/ dir), Standardized Vernacular (Cue + Cue Modal terms) - CLAUDE-PATTERNS.md: Encore Feature section lists maestroCue as second reference implementation alongside directorNotes - CLAUDE-IPC.md: cue namespace in Automation section, full Cue API reference with all endpoints and event documentation
…t journal - Add cue-db.ts: SQLite-backed event journal (cue_events table) and single-row heartbeat table (cue_heartbeat) using better-sqlite3 with WAL mode - Add cue-reconciler.ts: time event catch-up logic that fires exactly one reconciliation event per missed subscription (no flooding), with payload.reconciled and payload.missedCount metadata - Update cue-engine.ts: heartbeat writer (30s interval), sleep detection (2-minute gap threshold), database pruning (7 days), and clean shutdown - Update CueHelpModal: new "Sleep & Recovery" section with Moon icon - Update CueModal: amber "catch-up" badge on reconciled activity log entries - Tests: 41 new tests across cue-db (17), cue-reconciler (11), cue-sleep-wake (13)
Add filter field to CueSubscription for narrowing when subscriptions fire. Supports exact match, negation (!), numeric comparison (>/</>=/<=), glob patterns (picomatch), and boolean matching with AND logic. Filter checks integrated at all three dispatch points (file.changed, time.interval, agent.completed). Includes help modal docs, AI prompt updates, and 80 new tests (43 filter engine + 37 YAML loader).
… awareness Add pattern presets (Scheduled Task, File Enrichment, Reactive, Research Swarm, Sequential Chain, Debate) to the YAML editor as clickable cards. Enhance the AI system prompt with pattern recognition guidance. Add a Coordination Patterns section with ASCII flow diagrams to the help modal.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR introduces Maestro Cue, a comprehensive event-driven automation system for the Maestro application. The implementation spans a full-stack feature including a core orchestration engine with support for time-interval, file-change, agent-completion, GitHub PR/issue, and task-pending subscriptions; YAML configuration management; concurrent run execution with queue controls; sleep/wake reconciliation; GitHub polling; file watching; task scanning; database-backed event journaling; extensive UI components for configuration and monitoring; IPC integration; and comprehensive test coverage across all modules. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes This is a substantial feature addition spanning engine architecture, persistence layer, event sourcing, process execution, IPC integration, comprehensive UI, state management, and extensive test coverage. The changes are heterogeneous in nature (new modules, UI components, hooks, types, and tests), with high logic density in the engine orchestration, concurrency control, and GitHub/file/task polling subsystems. Multiple files require cross-cutting understanding of feature coordination and lifecycle management. The scale (90+ modified/added files) and architectural significance demand careful review of engine correctness, IPC contracts, UI state flows, and test coverage completeness. Possibly related issues
Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/renderer/components/History/HistoryEntryItem.tsx (1)
98-107:⚠️ Potential issue | 🟡 MinorAdd
tabIndexfor keyboard accessibility.The clickable entry card div lacks a
tabIndexattribute, which would prevent keyboard-only users from focusing and activating it. As per coding guidelines, components that need keyboard focus should havetabIndexand focus event handlers.🔧 Proposed fix
<div onClick={() => onOpenDetailModal(entry, index)} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onOpenDetailModal(entry, index); + } + }} className="p-3 rounded border transition-colors cursor-pointer hover:bg-white/5"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/History/HistoryEntryItem.tsx` around lines 98 - 107, The HistoryEntryItem clickable div is not keyboard-accessible; add tabIndex={0} to the div and implement an onKeyDown handler that calls onOpenDetailModal(entry, index) when Enter or Space is pressed; also wire onFocus and onBlur handlers (or existing selection handlers) to mirror mouse hover/selection behavior so keyboard users receive the same focus styling as isSelected — update the div in HistoryEntryItem that currently uses onClick, className and style to include tabIndex, onKeyDown, onFocus and onBlur and keep using onOpenDetailModal(entry, index) as the activation action.src/main/ipc/handlers/director-notes.ts (1)
78-85:⚠️ Potential issue | 🟠 MajorCUE filter is correctly added, but stats tracking is incomplete.
The
'CUE'filter option works as expected. However,UnifiedHistoryStatsonly tracksautoCountanduserCount, withtotalCountbeing their sum. Since CUE is a legitimate, first-class entry type (recorded byrecordCueHistoryEntry()in cue-executor.ts and displayed in the UI), the stats should track all three types consistently.The comment at line 163 states "Track stats from all entries (before type filter)" but the code only increments
autoCountanduserCount(lines 166-167), missing CUE entries entirely. Add acueCountfield toUnifiedHistoryStatsand update the counting logic to include CUE entries so the stats accurately reflect all history data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/ipc/handlers/director-notes.ts` around lines 78 - 85, The stats object UnifiedHistoryStats is missing CUE tracking: add a numeric cueCount field to UnifiedHistoryStats and update any code that aggregates entry types (the block that currently increments autoCount and userCount) to also increment cueCount when entry.type === 'CUE'; then compute totalCount as autoCount + userCount + cueCount. Ensure this aligns with the CUE entries produced by recordCueHistoryEntry() (cue-executor.ts) and update any places that construct or return UnifiedHistoryStats so the new cueCount is included.
🟠 Major comments (15)
src/renderer/hooks/useCue.ts-35-40 (1)
35-40:⚠️ Potential issue | 🟠 MajorAlign
CueSessionStatus.projectRootwith the API contract (optional).Line 39 makes
projectRootrequired, but Cue status payloads can omit it. This creates type drift and unsafe assumptions in consumers.🛠️ Suggested fix
export interface CueSessionStatus { sessionId: string; sessionName: string; toolType: string; - projectRoot: string; + projectRoot?: string; enabled: boolean; subscriptionCount: number; activeRuns: number;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/hooks/useCue.ts` around lines 35 - 40, The CueSessionStatus type currently marks projectRoot as required which contradicts the API payload that can omit it; update the CueSessionStatus interface (symbol: CueSessionStatus) to make projectRoot optional (e.g., add the optional marker or allow undefined) so consumers no longer assume it is always present and handle its absence safely.src/__tests__/main/cue/cue-yaml-loader.test.ts-138-142 (1)
138-142:⚠️ Potential issue | 🟠 MajorDon’t lock in exception-throwing behavior for malformed YAML.
Line 141 currently enforces throws for user-edited config errors. That makes YAML hot-reload brittle. Prefer graceful handling (e.g., return
null/validation errors) and adjust this test accordingly.🛠️ Suggested test expectation change
- it('throws on malformed YAML', () => { + it('returns null for malformed YAML', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue('{ invalid yaml ['); - expect(() => loadCueConfig('/projects/test')).toThrow(); + expect(loadCueConfig('/projects/test')).toBeNull(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/cue/cue-yaml-loader.test.ts` around lines 138 - 142, The test currently asserts that loadCueConfig('/projects/test') throws on malformed YAML, which forces throwing behavior; update the test to expect graceful handling instead (e.g., return null or a validation error) and modify the assertion accordingly. Locate the test case around the function loadCueConfig in cue-yaml-loader.test.ts (the "throws on malformed YAML" test) and change the expectation from expect(() => loadCueConfig(...)).toThrow() to an assertion that loadCueConfig returns null or a specific error/validation object (matching whatever loadCueConfig now returns for parse errors), and ensure mockExistsSync and mockReadFileSync remain set to simulate the malformed YAML input.src/main/process-listeners/exit-listener.ts-450-457 (1)
450-457:⚠️ Potential issue | 🟠 MajorGuard Cue completion notification against engine failures.
A throw inside
getCueEngine(),hasCompletionSubscribers(), ornotifyAgentCompleted()can escape the exit handler path.🛡️ Suggested hardening
- if (isCueEnabled?.() && getCueEngine) { - const cueEngine = getCueEngine(); - if (cueEngine?.hasCompletionSubscribers(sessionId)) { - cueEngine.notifyAgentCompleted(sessionId, { - status: code === 0 ? 'completed' : 'failed', - exitCode: code, - }); - } - } + try { + if (isCueEnabled?.() && getCueEngine) { + const cueEngine = getCueEngine(); + if (cueEngine?.hasCompletionSubscribers(sessionId)) { + cueEngine.notifyAgentCompleted(sessionId, { + status: code === 0 ? 'completed' : 'failed', + exitCode: code, + }); + } + } + } catch (err) { + logger.warn('[Cue] Failed to notify completion subscribers', 'ProcessListener', { + sessionId, + error: String(err), + }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-listeners/exit-listener.ts` around lines 450 - 457, Wrap the entire Cue notification sequence in a protective try/catch so exceptions from getCueEngine(), hasCompletionSubscribers(), or notifyAgentCompleted() cannot escape the exit handler; first call isCueEnabled?.() and then attempt to getCueEngine(), but guard calls with null/undefined checks and handle any thrown error by catching it and logging (or silently ignoring) without rethrowing; specifically update the block using isCueEnabled, getCueEngine, cueEngine.hasCompletionSubscribers(sessionId), and cueEngine.notifyAgentCompleted(sessionId, ...) so all three calls are executed inside a single try/catch and never allow an exception to propagate out of the exit listener.src/main/process-listeners/exit-listener.ts-448-457 (1)
448-457:⚠️ Potential issue | 🟠 MajorUse
baseSessionIdwhen notifying cue engine of agent completion.The exit listener extracts
baseSessionIdfor web broadcasting (line 439), but passes the raw suffixedsessionIdto cue notifications (lines 452-453). SincehasCompletionSubscribers()andnotifyAgentCompleted()match subscriptions against rawsessionIdor session name, completion subscribers configured with a canonical (base) session ID will be missed for sessions with suffixes like-ai-terminal,-batch-*, or-synopsis-*.Pass
baseSessionIdto bothhasCompletionSubscribers()andnotifyAgentCompleted()to align with the web broadcasting normalization and ensure subscribers receive notifications regardless of session suffix.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-listeners/exit-listener.ts` around lines 448 - 457, The code calls hasCompletionSubscribers(sessionId) and notifyAgentCompleted(sessionId, ...) with the raw suffixed sessionId; change both calls to use the normalized baseSessionId extracted earlier so Cue subscriptions registered against canonical session IDs are matched. In the exit listener where isCueEnabled, getCueEngine, hasCompletionSubscribers and notifyAgentCompleted are used, pass baseSessionId instead of sessionId to both hasCompletionSubscribers(...) and notifyAgentCompleted(...).src/renderer/components/CueHelpModal.tsx-162-162 (1)
162-162:⚠️ Potential issue | 🟠 MajorFix JSX comment-text lint blockers in example strings.
Line 162, Line 211, and Line 310 include
/*-like content in JSX text nodes, which triggers BiomenoCommentText.Suggested fix
- src/**/*.ts + {'src/**/*.ts'} @@ - {' '}watch: "src/**/*.ts" + {' watch: "src/**/*.ts"'} @@ - {' '}watch: "src/**/*" + {' watch: "src/**/*"'}Also applies to: 211-211, 310-310
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/CueHelpModal.tsx` at line 162, In CueHelpModal (the CueHelpModal component), replace raw "/*...*/" sequences inside JSX text nodes (occurrences noted around the example strings at the current lines ~162, ~211, ~310) so they don't look like comments to the Biome linter; wrap those example snippets as JS expressions or code nodes (for example: <code>{'/* example comment */'}</code> or <code>{`/* example comment */`}</code>) or split the characters (e.g. {'/*'} and {'*/'}) so the literal "/*" and "*/" never appear directly in JSX text; update the example strings in those three places accordingly.src/main/cue/cue-filter.ts-57-63 (1)
57-63:⚠️ Potential issue | 🟠 MajorNegated glob filters are currently evaluated as exact-string negation.
At Line 57–Line 63,
!*.test.tswon’t behave as a negated glob; it only checks exact equality with"*.test.ts".Suggested fix
} else if (filterValue.startsWith('!')) { const remainder = filterValue.slice(1); - if (String(payloadValue) === remainder) return false; + if (remainder.includes('*')) { + const isMatch = picomatch(remainder); + if (isMatch(String(payloadValue))) return false; + } else { + if (String(payloadValue) === remainder) return false; + } } else if (filterValue.includes('*')) { const isMatch = picomatch(filterValue); if (!isMatch(String(payloadValue))) return false;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-filter.ts` around lines 57 - 63, The negation branch currently treats any value starting with '!' as an exact-string negation, so patterns like '!*.test.ts' are not recognized as negated globs; update the logic in cue-filter handling (look for filterValue, payloadValue, and picomatch usage) so that when filterValue startsWith('!') you strip the '!' into remainder and then if remainder.includes('*') use picomatch(remainder) and return false when the matcher matches payloadValue (i.e., negate the glob match); otherwise keep the existing exact-string comparison for non-glob remainders.src/renderer/components/CueModal.tsx-285-293 (1)
285-293:⚠️ Potential issue | 🟠 MajorDon’t infer global Cue engine state from session rows.
Line 285 uses
sessions.some((s) => s.enabled)for the master toggle. This fails when the engine is enabled but there are zero configured sessions (status list empty), leaving the toggle stuck in a misleading disabled state.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/CueModal.tsx` around lines 285 - 293, The master toggle currently computes isEnabled from sessions.some((s) => s.enabled), which breaks when there are zero sessions; instead read the global Cue engine state directly (e.g., a prop or selector like cueEngineEnabled / getCueEngineStatus()) and use that for isEnabled and handleToggle logic; update the component to replace the sessions-derived isEnabled with the global engine state (keep handleToggle using enable/disable but depend on the new cueEngineEnabled symbol rather than sessions) so the toggle reflects the real engine status even when sessions array is empty.src/main/cue/cue-yaml-loader.ts-163-192 (1)
163-192:⚠️ Potential issue | 🟠 MajorReject unknown
eventvalues and malformedsource_sessionarrays in validation.Line 163 only enforces
eventas a string, so typos are treated as valid config. Also,source_sessionarrays are not validated for string elements, allowing structurally invalidagent.completedconfigs to pass.✅ Suggested validator tightening
+const VALID_EVENTS = new Set(['time.interval', 'file.changed', 'agent.completed']); + if (!sub.event || typeof sub.event !== 'string') { errors.push(`${prefix}: "event" is required and must be a string`); +} else if (!VALID_EVENTS.has(sub.event)) { + errors.push( + `${prefix}: "event" must be one of "time.interval", "file.changed", or "agent.completed"` + ); } @@ } else if (event === 'agent.completed') { if (!sub.source_session) { errors.push(`${prefix}: "source_session" is required for agent.completed events`); } else if (typeof sub.source_session !== 'string' && !Array.isArray(sub.source_session)) { errors.push( `${prefix}: "source_session" must be a string or array of strings for agent.completed events` ); + } else if ( + Array.isArray(sub.source_session) && + sub.source_session.some((v) => typeof v !== 'string' || v.trim() === '') + ) { + errors.push(`${prefix}: "source_session" array must contain only non-empty strings`); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-yaml-loader.ts` around lines 163 - 192, Tighten validation by rejecting unknown event values and ensuring arrays contain only strings: after extracting event from sub, validate it against the allowed set ['time.interval','file.changed','agent.completed'] and push an error if it’s not one of those; for the agent.completed branch, require sub.source_session to be either a non-empty string or an array where every element is a non-empty string (iterate the array and push an error if any element is not a string or is empty). Use the existing symbols sub, event, source_session and prefix to locate and update the checks and error messages accordingly.src/main/cue/cue-yaml-loader.ts-76-94 (1)
76-94:⚠️ Potential issue | 🟠 MajorHarden settings parsing to reject invalid numeric values during load.
Line 79, Line 87, and Line 91 accept any JS number (
NaN,Infinity, negative values, non-integers). That allows invalid runtime settings to bypass constraints and destabilize timeout/concurrency/queue behavior.🛡️ Suggested bounds-safe parsing
+const asPositiveFiniteNumber = (value: unknown, fallback: number): number => + typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback; + +const asBoundedInt = (value: unknown, fallback: number, min: number, max: number): number => + typeof value === 'number' && + Number.isInteger(value) && + value >= min && + value <= max + ? value + : fallback; + const rawSettings = parsed.settings as Record<string, unknown> | undefined; const settings: CueSettings = { - timeout_minutes: - typeof rawSettings?.timeout_minutes === 'number' - ? rawSettings.timeout_minutes - : DEFAULT_CUE_SETTINGS.timeout_minutes, + timeout_minutes: asPositiveFiniteNumber( + rawSettings?.timeout_minutes, + DEFAULT_CUE_SETTINGS.timeout_minutes + ), timeout_on_fail: rawSettings?.timeout_on_fail === 'break' || rawSettings?.timeout_on_fail === 'continue' ? rawSettings.timeout_on_fail : DEFAULT_CUE_SETTINGS.timeout_on_fail, - max_concurrent: - typeof rawSettings?.max_concurrent === 'number' - ? rawSettings.max_concurrent - : DEFAULT_CUE_SETTINGS.max_concurrent, - queue_size: - typeof rawSettings?.queue_size === 'number' - ? rawSettings.queue_size - : DEFAULT_CUE_SETTINGS.queue_size, + max_concurrent: asBoundedInt(rawSettings?.max_concurrent, DEFAULT_CUE_SETTINGS.max_concurrent, 1, 10), + queue_size: asBoundedInt(rawSettings?.queue_size, DEFAULT_CUE_SETTINGS.queue_size, 0, 50), };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-yaml-loader.ts` around lines 76 - 94, The settings parsing currently accepts any JS number (NaN/Infinity/negatives/floats); update the validation for rawSettings fields (used to build CueSettings from parsed) to explicitly reject non-finite, non-integer, or out-of-range values: for timeout_minutes require Number.isFinite(value) && Number.isInteger(value) && value >= 0, and for max_concurrent and queue_size require Number.isFinite(value) && Number.isInteger(value) && value >= 1, falling back to DEFAULT_CUE_SETTINGS when validation fails; keep the existing timeout_on_fail string check as-is.src/renderer/components/CueModal.tsx-336-345 (1)
336-345:⚠️ Potential issue | 🟠 MajorBackdrop click-to-close is currently unreachable.
Line 340 checks
e.target === e.currentTarget, but the fullscreen backdrop is a child element (Line 344). Clicking outside the modal hits the backdrop, soonClosedoes not run.🧩 Suggested backdrop close fix
-<div - className="fixed inset-0 flex items-center justify-center" - style={{ zIndex: MODAL_PRIORITIES.CUE_MODAL }} - onClick={(e) => { - if (e.target === e.currentTarget) onClose(); - }} -> +<div className="fixed inset-0 flex items-center justify-center" style={{ zIndex: MODAL_PRIORITIES.CUE_MODAL }}> {/* Backdrop */} - <div className="absolute inset-0 bg-black/50" /> + <div className="absolute inset-0 bg-black/50" onClick={onClose} /> {/* Modal */} <div className="relative rounded-xl shadow-2xl flex flex-col" + onClick={(e) => e.stopPropagation()} style={{ width: 780, maxHeight: '85vh',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/CueModal.tsx` around lines 336 - 345, The backdrop click handler is unreachable because the fullscreen backdrop is a child element, so the parent div's onClick check (e.target === e.currentTarget) never fires when clicking the backdrop; to fix, move the click-to-close logic onto the backdrop element (the div with className "absolute inset-0 bg-black/50") so clicks on the backdrop call onClose directly (or alternatively set the parent to pointer-events-none and keep the backdrop handling); update the handler references in CueModal.tsx around the MODAL_PRIORITIES.CUE_MODAL container and ensure onClose is invoked from the backdrop element.src/main/ipc/handlers/cue.ts-10-11 (1)
10-11:⚠️ Potential issue | 🟠 MajorReplace synchronous filesystem calls with async alternatives in IPC handlers.
The
cue:readYamlandcue:writeYamlhandlers use blocking filesystem operations (fs.existsSync,fs.readFileSync,fs.writeFileSync) on the Electron main thread, which freezes the UI during disk I/O on slow or network-mounted project roots.⚡ Suggested async rewrite
-import * as fs from 'fs'; +import { promises as fs } from 'fs'; import * as path from 'path'; @@ ipcMain.handle( 'cue:readYaml', withIpcErrorLogging( handlerOpts('readYaml'), async (options: { projectRoot: string }): Promise<string | null> => { const filePath = path.join(options.projectRoot, CUE_YAML_FILENAME); - if (!fs.existsSync(filePath)) { - return null; - } - return fs.readFileSync(filePath, 'utf-8'); + try { + return await fs.readFile(filePath, 'utf-8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } } ) ); @@ ipcMain.handle( 'cue:writeYaml', withIpcErrorLogging( handlerOpts('writeYaml'), async (options: { projectRoot: string; content: string }): Promise<void> => { const filePath = path.join(options.projectRoot, CUE_YAML_FILENAME); - fs.writeFileSync(filePath, options.content, 'utf-8'); + await fs.writeFile(filePath, options.content, 'utf-8'); // The file watcher in CueEngine will automatically detect the change and refresh } ) );🤖 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 10 - 11, The IPC handlers "cue:readYaml" and "cue:writeYaml" perform blocking disk I/O via fs.existsSync, fs.readFileSync and fs.writeFileSync on the main thread; make both handlers async and replace synchronous calls with async fs.promises (or import from 'fs/promises'): use fs.promises.readFile and fs.promises.writeFile (await them), replace existsSync by either fs.promises.access inside try/catch or by catching ENOENT from readFile, and ensure directories before write with fs.promises.mkdir(dir, { recursive: true }); preserve the existing return/error shapes and rethrow or send back the same error messages so callers behave unchanged.src/main/cue/cue-executor.ts-115-130 (1)
115-130:⚠️ Potential issue | 🟠 MajorAvoid mutating shared
templateContextin place.If a shared context object is reused across concurrent runs, this can leak Cue payload across executions.
🛠️ Proposed fix
- // 3. Populate the template context with Cue event data - templateContext.cue = { + // 3. Populate template context with Cue event data (without mutating shared state) + const runTemplateContext: TemplateContext = { + ...templateContext, + cue: { eventType: event.type, eventTimestamp: event.timestamp, triggerName: subscription.name, runId, filePath: String(event.payload.path ?? ''), fileName: String(event.payload.filename ?? ''), fileDir: String(event.payload.directory ?? ''), fileExt: String(event.payload.extension ?? ''), sourceSession: String(event.payload.sourceSession ?? ''), sourceOutput: String(event.payload.sourceOutput ?? ''), - }; + }, + }; @@ - const substitutedPrompt = substituteTemplateVariables(promptContent, templateContext); + const substitutedPrompt = substituteTemplateVariables(promptContent, runTemplateContext);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-executor.ts` around lines 115 - 130, The current code mutates the shared templateContext by assigning templateContext.cue = {...}, which can leak Cue payload between concurrent runs; instead create a fresh context (e.g., const localContext = {...templateContext} or use a deep clone/structuredClone) and assign localContext.cue = { eventType: event.type, eventTimestamp: event.timestamp, triggerName: subscription.name, runId, filePath: String(event.payload.path ?? ''), fileName: String(event.payload.filename ?? ''), fileDir: String(event.payload.directory ?? ''), fileExt: String(event.payload.extension ?? ''), sourceSession: String(event.payload.sourceSession ?? ''), sourceOutput: String(event.payload.sourceOutput ?? '') }; then pass that new context into substituteTemplateVariables(promptContent, localContext) instead of mutating templateContext.src/renderer/components/CueYamlEditor.tsx-193-201 (1)
193-201:⚠️ Potential issue | 🟠 MajorSave failures are silently swallowed.
Line [199] drops write/refresh errors, so the user gets no feedback and the failure path is hard to detect.
🛠️ Proposed fix
const handleSave = useCallback(async () => { if (!isValid) return; try { await window.maestro.cue.writeYaml(projectRoot, yamlContent); await window.maestro.cue.refreshSession(sessionId, projectRoot); onClose(); - } catch { - // Let Sentry capture unexpected errors + } catch (error) { + setValidationErrors([ + error instanceof Error ? error.message : 'Failed to save maestro-cue.yaml', + ]); + setIsValid(false); + throw error; } }, [isValid, projectRoot, yamlContent, sessionId, onClose]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/CueYamlEditor.tsx` around lines 193 - 201, The save handler handleSave currently swallows exceptions from window.maestro.cue.writeYaml and window.maestro.cue.refreshSession so users get no feedback; update handleSave to catch the error into a variable, call a user-visible error path (e.g., setLocal state or call a provided onError/onNotification) to show an error toast/dialog, log the error to Sentry or processLogger (preserving the caught error), and avoid calling onClose when save/refresh failed; reference handleSave, window.maestro.cue.writeYaml, window.maestro.cue.refreshSession and onClose when implementing these changes.src/renderer/components/CueYamlEditor.tsx-131-173 (1)
131-173:⚠️ Potential issue | 🟠 MajorValidation flow has two correctness gaps (stale async results + no initial validation).
Line [136] loads content without validating it, and Line [164] can apply out-of-order async validation responses after subsequent edits.
🛠️ Proposed fix
export function CueYamlEditor({ @@ const [copied, setCopied] = useState(false); const validateTimerRef = useRef<ReturnType<typeof setTimeout>>(); + const validateRequestRef = useRef(0); const yamlTextareaRef = useRef<HTMLTextAreaElement>(null); @@ useEffect(() => { if (!isOpen) return; let cancelled = false; @@ async function loadYaml() { setLoading(true); try { const content = await window.maestro.cue.readYaml(projectRoot); if (cancelled) return; const initial = content ?? YAML_TEMPLATE; setYamlContent(initial); setOriginalContent(initial); + const result = await window.maestro.cue.validateYaml(initial); + if (!cancelled) { + setIsValid(result.valid); + setValidationErrors(result.errors); + } } catch { if (cancelled) return; setYamlContent(YAML_TEMPLATE); setOriginalContent(YAML_TEMPLATE); + setIsValid(true); + setValidationErrors([]); } finally { if (!cancelled) setLoading(false); } } @@ const validateYaml = useCallback((content: string) => { if (validateTimerRef.current) { clearTimeout(validateTimerRef.current); } + const requestId = ++validateRequestRef.current; validateTimerRef.current = setTimeout(async () => { try { const result = await window.maestro.cue.validateYaml(content); + if (requestId !== validateRequestRef.current) return; setIsValid(result.valid); setValidationErrors(result.errors); } catch { + if (requestId !== validateRequestRef.current) return; setIsValid(false); setValidationErrors(['Failed to validate YAML']); } }, 500); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/CueYamlEditor.tsx` around lines 131 - 173, When loading YAML in the CueYamlEditor component (loadYaml), immediately run the same validation path used by validateYaml for the initial content (initial = content ?? YAML_TEMPLATE) so setIsValid and setValidationErrors are populated on mount; also prevent stale/out‑of‑order validation responses by stamping validations with a token/counter (e.g., validationIdRef) captured by each async validateYaml invocation and only applying setIsValid/setValidationErrors when the token matches the latest, and ensure validateTimerRef is cleared on unmount and when starting a new validation to avoid orphaned timers.src/main/cue/cue-engine.ts-826-836 (1)
826-836:⚠️ Potential issue | 🟠 MajorRisk of infinite recursion in completion chain.
After a Cue run completes,
notifyAgentCompletedis called (line 828), which could trigger another subscription listening for that session's completion. If a subscription'ssource_sessionmatches its own owning session, this creates an infinite loop.While this requires misconfiguration, it's easy to accidentally create (e.g., a "retry on failure" pattern that inadvertently triggers itself). Consider adding a guard to break the cycle.
🛡️ Proposed fix to prevent self-triggering loops
+ // Emit completion event for agent completion chains + // This allows downstream subscriptions to react to this Cue run's completion + // Guard: don't notify if this run was itself triggered by an agent.completed subscription + // to prevent infinite loops from self-referential configurations + const wasTriggeredByCompletion = event.type === 'agent.completed'; + if (!wasTriggeredByCompletion) { this.notifyAgentCompleted(sessionId, { sessionName: result.sessionName, status: result.status, exitCode: result.exitCode, durationMs: result.durationMs, stdout: result.stdout, triggeredBy: subscriptionName, }); + }Alternatively, track a "chain depth" counter and break at a reasonable limit (e.g., 5 levels).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-engine.ts` around lines 826 - 836, Add a guard to prevent self-triggering completion loops when notifyAgentCompleted is called from Cue runs: modify notifyAgentCompleted (and the call site in cue-engine.ts that passes triggeredBy/subscriptionName) to either 1) ignore/return early when triggeredBy === sessionId (or when the subscription’s source_session equals its owning session) or 2) accept and propagate an optional chainDepth counter that is incremented on each notifyAgentCompleted call and stops processing when it exceeds a safe limit (e.g., 5); update the call in cue-engine.ts that invokes notifyAgentCompleted(sessionId, {..., triggeredBy: subscriptionName}) to pass/increment chainDepth and ensure the notifyAgentCompleted implementation enforces the break condition.
🟡 Minor comments (6)
src/renderer/components/SettingsModal.tsx-3751-3757 (1)
3751-3757:⚠️ Potential issue | 🟡 MinorUse platform-aware shortcut text in the Cue hint.
Line 3756 hardcodes a macOS key combo. On Windows/Linux this will show the wrong shortcut.
🛠️ Suggested fix
<kbd className="px-1.5 py-0.5 rounded text-[10px] font-mono" style={{ backgroundColor: theme.colors.border }} > - ⌘⇧U + {formatMetaKey()}+Shift+U </kbd>{' '}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/SettingsModal.tsx` around lines 3751 - 3757, The hint in SettingsModal currently hardcodes the macOS shortcut "⌘⇧U" inside the <kbd> element; change it to render a platform-aware string (e.g. "⌘⇧U" for macOS and "Ctrl+Shift+U" for Windows/Linux). Locate the JSX in SettingsModal (the "Open the Cue dashboard with" text and the <kbd> element) and replace the hardcoded text with a small platform check (window.navigator.platform or an existing isMac/isDarwin helper) that returns the correct display string, then use that variable inside the <kbd> element so the UI shows the right shortcut per OS.src/renderer/components/SessionItem.tsx-161-168 (1)
161-168:⚠️ Potential issue | 🟡 MinorAdd an accessible label for the Cue badge.
Line 164 relies on
titleonly. That won’t reliably expose the status to screen readers. Add an explicitaria-label(and keep the icon decorative).♿ Suggested fix
{cueSubscriptionCount != null && cueSubscriptionCount > 0 && ( <span className="shrink-0 flex items-center" title={`Maestro Cue active (${cueSubscriptionCount} subscription${cueSubscriptionCount === 1 ? '' : 's'})`} + aria-label={`Maestro Cue active (${cueSubscriptionCount} subscription${cueSubscriptionCount === 1 ? '' : 's'})`} > - <Zap className="w-3 h-3" style={{ color: '#2dd4bf' }} fill="#2dd4bf" /> + <Zap + className="w-3 h-3" + style={{ color: '#2dd4bf' }} + fill="#2dd4bf" + aria-hidden="true" + /> </span> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/SessionItem.tsx` around lines 161 - 168, The Cue badge uses only a title attribute which is not reliably exposed to screen readers; update the span that renders when cueSubscriptionCount is present (the element containing the Zap icon and using the title string) to include an explicit aria-label with the same descriptive text (e.g. "Maestro Cue active (X subscriptions)"), mark the Zap icon component as decorative (aria-hidden="true" or equivalent) so assistive tech reads the span label only, and keep the existing title for hover tooltips; changes should be made around the JSX that references cueSubscriptionCount and the Zap component in SessionItem.src/__tests__/main/cue/cue-executor.test.ts-501-510 (1)
501-510:⚠️ Potential issue | 🟡 MinorRemove the unused first spy on
mockChild.kill.Line 503 creates a spy (
killSpy) that is immediately replaced by the second spy at line 509 and never asserted. The first spy is redundant and should be removed.🛠️ Suggested fix
it('should send SIGTERM when timeout expires', async () => { const config = createExecutionConfig({ timeoutMs: 5000 }); - const killSpy = vi.spyOn(mockChild, 'kill'); const resultPromise = executeCuePrompt(config); await vi.advanceTimersByTimeAsync(0); // Wait: re-spy after child is created const childKill = vi.spyOn(mockChild, 'kill');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/cue/cue-executor.test.ts` around lines 501 - 510, Remove the redundant spy creation "const killSpy = vi.spyOn(mockChild, 'kill');" in the test "should send SIGTERM when timeout expires" and rely on the later spy "const childKill = vi.spyOn(mockChild, 'kill');" for assertions; update any assertions/refers to use childKill (or remove references to killSpy) so only the single spy on mockChild.kill created after the child is instantiated is used in executeCuePrompt test.src/__tests__/main/cue/cue-reconciler.test.ts-276-279 (1)
276-279:⚠️ Potential issue | 🟡 MinorMake the sleep-duration assertion deterministic.
Line 276 and Line 278 call
Date.now()separately, sogapMscan exceedsleepDurationby a few ms and make Line 284 flaky.Suggested change
const sleepDuration = 60 * 60 * 1000; // 1 hour + const now = Date.now(); const config = makeConfig({ - sleepStartMs: Date.now() - sleepDuration, - wakeTimeMs: Date.now(), + sleepStartMs: now - sleepDuration, + wakeTimeMs: now, sessions, });Also applies to: 284-284
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/cue/cue-reconciler.test.ts` around lines 276 - 279, The test is using Date.now() twice causing a race that makes the sleep-duration assertion flaky; capture a single timestamp (e.g. const now = Date.now()) and use it for both sleepStartMs and wakeTimeMs when calling makeConfig so gapMs is exactly computed from the same reference, then update any related assertions (sleepDuration, gapMs) to derive from that single now value to make the assertion deterministic.src/__tests__/main/cue/cue-ipc-handlers.test.ts-69-80 (1)
69-80:⚠️ Potential issue | 🟡 MinorAdd
cue:getQueueStatusto the IPC contract test.The registration coverage misses one live channel, so a regression there won’t fail this suite.
Suggested patch
function createMockEngine() { return { getStatus: vi.fn().mockReturnValue([]), getActiveRuns: vi.fn().mockReturnValue([]), getActivityLog: vi.fn().mockReturnValue([]), start: vi.fn(), stop: vi.fn(), stopRun: vi.fn().mockReturnValue(true), stopAll: vi.fn(), + getQueueStatus: vi.fn().mockReturnValue(new Map<string, number>()), refreshSession: vi.fn(), isEnabled: vi.fn().mockReturnValue(false), }; } @@ const expectedChannels = [ 'cue:getStatus', 'cue:getActiveRuns', 'cue:getActivityLog', 'cue:enable', 'cue:disable', 'cue:stopRun', 'cue:stopAll', + 'cue:getQueueStatus', 'cue:refreshSession', 'cue:readYaml', 'cue:writeYaml', 'cue:validateYaml', ];Also applies to: 113-125
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/main/cue/cue-ipc-handlers.test.ts` around lines 69 - 80, The IPC contract test is missing the live channel "cue:getQueueStatus" so add it to the registration coverage where channels are asserted/registered; update the mock engine or channel registration list used in the test (referencing createMockEngine and the test that collects registered IPC channels) to include "cue:getQueueStatus" alongside the other cue channels so the suite fails on regressions for that channel as well.src/main/preload/cue.ts-14-55 (1)
14-55:⚠️ Potential issue | 🟡 MinorPreload Cue types are drifting from the shared core types.
CueSessionStatushere is missingprojectRoot, and maintaining duplicate interfaces in this file will keep causing drift.♻️ Proposed refactor
import { ipcRenderer } from 'electron'; +import type { + CueEventType, + CueRunStatus, + CueEvent, + CueRunResult, + CueSessionStatus, +} from '../cue/cue-types'; -/** Event types that can trigger a Cue subscription */ -export type CueEventType = 'time.interval' | 'file.changed' | 'agent.completed'; - -/** Status of a Cue run */ -export type CueRunStatus = 'running' | 'completed' | 'failed' | 'timeout' | 'stopped'; - -/** An event instance produced by a trigger */ -export interface CueEvent { - id: string; - type: CueEventType; - timestamp: string; - triggerName: string; - payload: Record<string, unknown>; -} - -/** Result of a completed (or failed/timed-out) Cue run */ -export interface CueRunResult { - runId: string; - sessionId: string; - sessionName: string; - subscriptionName: string; - event: CueEvent; - status: CueRunStatus; - stdout: string; - stderr: string; - exitCode: number | null; - durationMs: number; - startedAt: string; - endedAt: string; -} - -/** Status summary for a Cue-enabled session */ -export interface CueSessionStatus { - sessionId: string; - sessionName: string; - toolType: string; - enabled: boolean; - subscriptionCount: number; - activeRuns: number; - lastTriggered?: string; - nextTrigger?: string; -}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/preload/cue.ts` around lines 14 - 55, The local CueSessionStatus interface in this file is missing the projectRoot field and duplicates the shared core type; replace the local declaration by importing and re-exporting the shared core CueSessionStatus type (or extend it) so the preload types stay in sync—update any references to use the imported CueSessionStatus symbol and remove the duplicate interface to prevent drift.
🧹 Nitpick comments (10)
src/renderer/components/History/HistoryEntryItem.tsx (1)
23-28: Consider extracting the CUE teal color to theme or constants.The CUE color
#06b6d4is hardcoded here and duplicated inHistoryFilterToggle.tsx. Consider defining a shared constant (e.g.,CUE_TEALas seen inCueModal.tsx) or adding it to the theme system for consistency and easier maintenance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/History/HistoryEntryItem.tsx` around lines 23 - 28, The CUE teal color is hardcoded in HistoryEntryItem (and duplicated in HistoryFilterToggle); extract that hex into a shared constant (e.g., export const CUE_TEAL = '#06b6d4') or add it to the theme and update HistoryEntryItem's case 'CUE' to reference CUE_TEAL for bg/text/border computations; also update HistoryFilterToggle (and any other files like CueModal) to import and use the new CUE_TEAL constant so the color is defined in one place for consistency and easier maintenance.src/__tests__/renderer/components/CueModal.test.tsx (1)
328-339: Consider using a more robust selector for the close button.The test relies on finding a button with
.lucide-xclass, which is an implementation detail of the Lucide icon library. If the library changes its class naming, this test would break silently. Consider adding adata-testidoraria-labelto the close button in the component.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/CueModal.test.tsx` around lines 328 - 339, Test uses an implementation-specific selector (.lucide-x) to find the close button in CueModal; update the CueModal close button element (in the CueModal component) to include a stable identifier (e.g., aria-label="Close" or data-testid="cue-modal-close"), then update this test (CueModal.test.tsx) to query the button with a robust selector such as screen.getByRole('button', { name: /close/i }) or screen.getByTestId('cue-modal-close') and assert mockOnClose is called after clicking that element.src/renderer/components/History/HistoryFilterToggle.tsx (1)
26-31: Duplicate hardcoded CUE color.Same hardcoded color as in
HistoryEntryItem.tsx. Consider extracting to a shared constant.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/History/HistoryFilterToggle.tsx` around lines 26 - 31, The CUE color is duplicated in HistoryFilterToggle ('case "CUE"') and HistoryEntryItem; extract a shared constant (e.g., export const CUE_COLOR = '#06b6d4') into a common constants module (or the shared colors file) and replace the hardcoded string in both HistoryFilterToggle and HistoryEntryItem to import CUE_COLOR, then derive bg ('CUE_COLOR' + '20') and border ('CUE_COLOR' + '40') where needed so both components use the single source of truth.src/renderer/components/HistoryDetailModal.tsx (1)
177-183: Deduplicate CUE color tokens to keep styling consistent.The same hard-coded CUE colors are repeated in multiple places; extracting a single constant avoids drift and simplifies future theme updates.
Suggested change
+const CUE_PILL_COLORS = { + bg: '#06b6d420', + text: '#06b6d4', + border: '#06b6d440', +} as const; + const getPillColor = () => { if (entry.type === 'AUTO') { return { bg: theme.colors.warning + '20', text: theme.colors.warning, border: theme.colors.warning + '40', }; } if (entry.type === 'CUE') { - return { - bg: '#06b6d420', - text: '#06b6d4', - border: '#06b6d440', - }; + return CUE_PILL_COLORS; } return { bg: theme.colors.accent + '20', text: theme.colors.accent, border: theme.colors.accent + '40', }; };style={{ - backgroundColor: '#06b6d420', - color: '#06b6d4', - border: '1px solid `#06b6d440`', + backgroundColor: CUE_PILL_COLORS.bg, + color: CUE_PILL_COLORS.text, + border: `1px solid ${CUE_PILL_COLORS.border}`, }}Also applies to: 374-382
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/HistoryDetailModal.tsx` around lines 177 - 183, Extract the repeated hard-coded CUE color tokens used in HistoryDetailModal into a single exported constant (e.g., CUE_COLOR = { bg: '#06b6d420', text: '#06b6d4', border: '#06b6d440' }) and replace the inline object in the branch that checks entry.type === 'CUE' and any other occurrences (including the block around lines 374-382) to reference that constant; update imports/exports as needed so other components can reuse the same CUE_COLOR instead of duplicating the literal values.src/__tests__/renderer/components/SessionList.test.tsx (1)
3192-3195: Replace fixed-time sleeps withwaitFor()to avoid flaky tests in CI environments.Lines 3194 and 3221 use
setTimeout(50)to "settle" effects. This is timing-sensitive and can intermittently fail in slower CI environments. Replace withwaitFor()using the DOM condition being checked immediately after.Suggested change
- // Give async effects time to settle - await act(async () => { - await new Promise((r) => setTimeout(r, 50)); - }); - - expect(screen.queryByTestId('icon-zap')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByTestId('icon-zap')).not.toBeInTheDocument(); + });- await act(async () => { - await new Promise((r) => setTimeout(r, 50)); - }); - - expect(screen.queryByTestId('icon-zap')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByTestId('icon-zap')).not.toBeInTheDocument(); + });Also applies to: 3221–3223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/SessionList.test.tsx` around lines 3192 - 3195, Replace the fixed 50ms setTimeout-based waits in the test’s async act blocks (the occurrences of "await act(async () => { await new Promise((r) => setTimeout(r, 50)); });") with testing-library's waitFor and an explicit DOM/state condition — e.g., wrap the assertion or the condition you expect after the effect inside waitFor so it polls until satisfied; also ensure waitFor is imported from "@testing-library/react" at the top of the test file and remove the setTimeout-based act blocks (apply the same replacement for both occurrences around lines 3194 and 3221).src/renderer/components/LogViewer.tsx (1)
19-25: Consider consolidating log-level literals into one typed source-of-truth.
'debug' | ... | 'cue'and the level array are now duplicated in multiple places, making future level changes easy to miss.♻️ Suggested refactor
interface SystemLogEntry { timestamp: number; level: 'debug' | 'info' | 'warn' | 'error' | 'toast' | 'autorun' | 'cue'; message: string; context?: string; data?: unknown; } +type LogLevel = SystemLogEntry['level']; +const LOG_LEVELS: readonly LogLevel[] = [ + 'debug', + 'info', + 'warn', + 'error', + 'toast', + 'autorun', + 'cue', +]; -const enabledLevels = new Set<'debug' | 'info' | 'warn' | 'error' | 'toast' | 'autorun' | 'cue'>( +const enabledLevels = new Set<LogLevel>( (['debug', 'info', 'warn', 'error'] as const).filter( (level) => LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[logLevel] ) ); -const [selectedLevels, setSelectedLevelsState] = useState< - Set<'debug' | 'info' | 'warn' | 'error' | 'toast' | 'autorun' | 'cue'> ->(() => { +const [selectedLevels, setSelectedLevelsState] = useState<Set<LogLevel>>(() => { ... }); -{(['debug', 'info', 'warn', 'error', 'toast', 'autorun', 'cue'] as const).map((level) => { +{LOG_LEVELS.map((level) => {Also applies to: 70-101, 490-530
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/LogViewer.tsx` around lines 19 - 25, Consolidate the duplicated log-level literals by introducing a single source-of-truth: create a const tuple (e.g. const LOG_LEVELS = ['debug','info','warn','error','toast','autorun','cue'] as const) and derive a type alias (type LogLevel = typeof LOG_LEVELS[number]); then update SystemLogEntry.level to use LogLevel and replace any separate level arrays or literals elsewhere in this file (and other usages around the LogViewer component) to reference LOG_LEVELS and LogLevel instead of repeating the string union or separate arrays so future changes are made in one place.src/main/cue/cue-file-watcher.ts (2)
75-81: Consider awaiting watcher.close() for cleaner shutdown.The
watcher.close()method in chokidar v3 returns a Promise. While the current synchronous cleanup works (the watcher will close asynchronously), making the cleanup function async would ensure resources are fully released before the caller continues.However, since this is a cleanup function typically called during shutdown, the current approach is acceptable and won't cause issues in practice.
♻️ Optional: async cleanup for completeness
-export function createCueFileWatcher(config: CueFileWatcherConfig): () => void { +export function createCueFileWatcher(config: CueFileWatcherConfig): () => Promise<void> { // ... existing code ... - return () => { + return async () => { for (const timer of debounceTimers.values()) { clearTimeout(timer); } debounceTimers.clear(); - watcher.close(); + await watcher.close(); }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-file-watcher.ts` around lines 75 - 81, The cleanup returned currently calls watcher.close() synchronously; change the returned function to be async and await watcher.close() so the chokidar watcher is fully closed before shutdown; keep clearing debounceTimers (clearTimeout on each timer and debounceTimers.clear()) before or after awaiting close as preferred, and ensure the function signature becomes async () => { ... await watcher.close(); } so callers can await the cleanup.
70-73: Consider using the cue-db logger instead of console.error.The module comment mentions "the parent engine will handle logging", but using
console.errordirectly bypasses any structured logging. If a logger utility is available (as suggested by the imports list mentioningsrc/main/utils/logger.ts), consider using it for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-file-watcher.ts` around lines 70 - 73, The file watcher currently calls console.error inside watcher.on('error', ...) which bypasses the project's logger; change this to use the shared logger (e.g. logger.error) imported from the project's logger module, preserving the existing message and including the triggerName and the error object as structured metadata (so use logger.error with the message and the error/metadata) in the watcher.on('error', ...) handler to ensure consistent, structured logging.src/__tests__/renderer/components/CueHelpModal.test.tsx (1)
44-64: Consider adding missing theme properties to mockTheme.The
mockThemeobject is missing theaccentForegroundproperty that may be used by the component (as seen in other modal components). While tests pass, having a complete mock ensures tests don't break if the component starts using this property.♻️ Add missing theme property
const mockTheme: Theme = { id: 'test-dark', name: 'Test Dark', mode: 'dark', colors: { bgMain: '#1a1a1a', bgSidebar: '#252525', bgActivity: '#2d2d2d', border: '#444444', textMain: '#ffffff', textDim: '#888888', accent: '#007acc', + accentForeground: '#ffffff', error: '#ff4444', success: '#44ff44', warning: '#ffaa00', cursor: '#ffffff', selection: '#264f78', terminalBackground: '#000000', }, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/renderer/components/CueHelpModal.test.tsx` around lines 44 - 64, The mockTheme object used in CueHelpModal.test.tsx is missing the accentForeground property defined on Theme; update the mockTheme constant to include an accentForeground string (e.g., matching accent or a contrasting color) inside the colors object so components referencing colors.accentForeground do not break—locate the mockTheme definition in the test and add the accentForeground key to the colors map.src/main/cue/cue-db.ts (1)
20-30: Use Cue union types fortypeandstatusfields.These are currently plain strings; switching to
CueEventType/CueRunStatuswill catch invalid values at compile time.♻️ Proposed refactor
import Database from 'better-sqlite3'; import * as path from 'path'; import * as fs from 'fs'; import { app } from 'electron'; +import type { CueEventType, CueRunStatus } from './cue-types'; @@ export interface CueEventRecord { id: string; - type: string; + type: CueEventType; @@ - status: string; + status: CueRunStatus; @@ export function recordCueEvent(event: { id: string; - type: string; + type: CueEventType; @@ - status: string; + status: CueRunStatus; payload?: string; }): void { @@ -export function updateCueEventStatus(id: string, status: string): void { +export function updateCueEventStatus(id: string, status: CueRunStatus): void {Also applies to: 144-152, 173-176
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-db.ts` around lines 20 - 30, Change the plain string types in the CueEventRecord interface to the appropriate Cue union types: replace the type of the "type" field with CueEventType and the "status" field with CueRunStatus; update any other occurrences of those fields (the other interfaces/usages around the other occurrences noted) to use the same union types so compilation will catch invalid values and adjust any imports to bring in CueEventType and CueRunStatus from their module. Ensure referenced symbols are CueEventRecord, CueEventType, and CueRunStatus and update related definitions/usages accordingly.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (78)
CLAUDE-IPC.mdCLAUDE-PATTERNS.mdCLAUDE.mdpackage.jsonsrc/__tests__/main/cue/cue-completion-chains.test.tssrc/__tests__/main/cue/cue-concurrency.test.tssrc/__tests__/main/cue/cue-db.test.tssrc/__tests__/main/cue/cue-engine.test.tssrc/__tests__/main/cue/cue-executor.test.tssrc/__tests__/main/cue/cue-file-watcher.test.tssrc/__tests__/main/cue/cue-filter.test.tssrc/__tests__/main/cue/cue-ipc-handlers.test.tssrc/__tests__/main/cue/cue-reconciler.test.tssrc/__tests__/main/cue/cue-sleep-wake.test.tssrc/__tests__/main/cue/cue-yaml-loader.test.tssrc/__tests__/main/process-listeners/exit-listener.test.tssrc/__tests__/renderer/components/CueHelpModal.test.tsxsrc/__tests__/renderer/components/CueModal.test.tsxsrc/__tests__/renderer/components/CueYamlEditor.test.tsxsrc/__tests__/renderer/components/History/HistoryEntryItem.test.tsxsrc/__tests__/renderer/components/History/HistoryFilterToggle.test.tsxsrc/__tests__/renderer/components/HistoryDetailModal.test.tsxsrc/__tests__/renderer/components/HistoryPanel.test.tsxsrc/__tests__/renderer/components/LogViewer.test.tsxsrc/__tests__/renderer/components/SessionItemCue.test.tsxsrc/__tests__/renderer/components/SessionList.test.tsxsrc/__tests__/renderer/hooks/useCue.test.tssrc/__tests__/renderer/hooks/useCueAutoDiscovery.test.tssrc/__tests__/setup.tssrc/__tests__/web/mobile/MobileHistoryPanel.test.tsxsrc/main/cue/cue-db.tssrc/main/cue/cue-engine.tssrc/main/cue/cue-executor.tssrc/main/cue/cue-file-watcher.tssrc/main/cue/cue-filter.tssrc/main/cue/cue-reconciler.tssrc/main/cue/cue-types.tssrc/main/cue/cue-yaml-loader.tssrc/main/index.tssrc/main/ipc/handlers/cue.tssrc/main/ipc/handlers/director-notes.tssrc/main/ipc/handlers/index.tssrc/main/preload/cue.tssrc/main/preload/directorNotes.tssrc/main/preload/files.tssrc/main/preload/index.tssrc/main/process-listeners/exit-listener.tssrc/main/process-listeners/types.tssrc/main/utils/logger.tssrc/renderer/App.tsxsrc/renderer/components/AppModals.tsxsrc/renderer/components/CueHelpModal.tsxsrc/renderer/components/CueModal.tsxsrc/renderer/components/CueYamlEditor.tsxsrc/renderer/components/DirectorNotes/UnifiedHistoryTab.tsxsrc/renderer/components/History/HistoryEntryItem.tsxsrc/renderer/components/History/HistoryFilterToggle.tsxsrc/renderer/components/HistoryDetailModal.tsxsrc/renderer/components/HistoryPanel.tsxsrc/renderer/components/LogViewer.tsxsrc/renderer/components/QuickActionsModal.tsxsrc/renderer/components/SessionItem.tsxsrc/renderer/components/SessionList.tsxsrc/renderer/components/SettingsModal.tsxsrc/renderer/constants/cuePatterns.tssrc/renderer/constants/modalPriorities.tssrc/renderer/constants/shortcuts.tssrc/renderer/global.d.tssrc/renderer/hooks/agent/useAgentSessionManagement.tssrc/renderer/hooks/keyboard/useMainKeyboardHandler.tssrc/renderer/hooks/useCue.tssrc/renderer/hooks/useCueAutoDiscovery.tssrc/renderer/stores/modalStore.tssrc/renderer/stores/settingsStore.tssrc/renderer/types/index.tssrc/shared/logger-types.tssrc/shared/templateVariables.tssrc/shared/types.ts
Greptile SummaryThis PR implements Maestro Cue, a comprehensive event-driven automation system that enables agents to trigger prompts based on time intervals, file changes, and agent completions. The implementation spans 79 files with 13,244 insertions and includes: Core Architecture:
Infrastructure:
UI Components:
Integration:
Critical Issue: Confidence Score: 2/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Start[App Startup] --> Init[Initialize CueEngine]
Init --> CheckFlag{Encore Feature<br/>maestroCue enabled?}
CheckFlag -->|No| Skip[Skip engine start]
CheckFlag -->|Yes| StartEngine[engine.start]
StartEngine --> ScanSessions[Scan all sessions]
ScanSessions --> LoadConfigs[Load maestro-cue.yaml<br/>from project roots]
LoadConfigs --> SetupTimers[Setup interval timers]
SetupTimers --> SetupWatchers[Setup file watchers]
SetupWatchers --> SetupYAMLWatch[Setup YAML hot reload]
subgraph "Event Triggers"
Timer[time.interval<br/>fires every N minutes]
FileChange[file.changed<br/>chokidar detects changes]
AgentDone[agent.completed<br/>exit listener notifies]
end
Timer --> CheckFilter{Payload<br/>filter match?}
FileChange --> CheckFilter
AgentDone --> CheckFilter
CheckFilter -->|No| Drop[Drop event]
CheckFilter -->|Yes| CheckConcurrency{Active runs <<br/>max_concurrent?}
CheckConcurrency -->|No| Queue[Add to event queue]
CheckConcurrency -->|Yes| CheckFanOut{fan_out<br/>targets?}
CheckFanOut -->|No| Execute[executeCueRun<br/>on owner session]
CheckFanOut -->|Yes| FanOut[Execute on each<br/>target session]
Execute --> CallStub[Call onCueRun callback]
FanOut --> CallStub
CallStub --> Stub{Stub or real<br/>implementation?}
Stub -->|Stub| FakeResult[Return fake 'completed'<br/>NO ACTUAL EXECUTION]
Stub -->|Real| CallExecutor[executeCuePrompt]
CallExecutor --> ReadPrompt[Read prompt file]
ReadPrompt --> SubstituteVars[Substitute template vars]
SubstituteVars --> BuildArgs[Build agent spawn args]
BuildArgs --> CheckSSH{SSH remote<br/>enabled?}
CheckSSH -->|Yes| WrapSSH[Wrap with SSH]
CheckSSH -->|No| SpawnLocal[Spawn agent process]
WrapSSH --> SpawnLocal
SpawnLocal --> CaptureOutput[Capture stdout/stderr]
CaptureOutput --> Timeout{Timeout?}
Timeout -->|Yes| SendSIGTERM[SIGTERM then SIGKILL]
Timeout -->|No| WaitExit[Wait for exit]
SendSIGTERM --> RecordResult[Record CueRunResult]
WaitExit --> RecordResult
FakeResult --> RecordResult
RecordResult --> NotifyCompletion[Notify agent.completed<br/>for chains]
NotifyCompletion --> CheckFanIn{Fan-in<br/>subscription?}
CheckFanIn -->|Yes| TrackCompletion[Track completion]
CheckFanIn -->|No| Done[Done]
TrackCompletion --> AllDone{All sources<br/>completed?}
AllDone -->|No| WaitMore[Wait for more]
AllDone -->|Yes| FireFanIn[Fire downstream sub<br/>with combined output]
FireFanIn --> Done
Queue --> DrainQueue[Drain queue when<br/>slot available]
DrainQueue --> CheckConcurrency
style Stub fill:#ff6b6b
style FakeResult fill:#ff6b6b
style CallExecutor fill:#51cf66
Last reviewed commit: 6c2c252 |
src/main/index.ts
Outdated
| onCueRun: async (sessionId, _prompt, event) => { | ||
| // Stub for Phase 03 executor integration — returns a placeholder result. | ||
| // The actual executor (cue-executor.ts) is wired in a future phase. | ||
| logger.info(`[CUE] Run triggered for session ${sessionId}: ${event.triggerName}`, 'Cue'); | ||
| return { | ||
| runId: event.id, | ||
| sessionId, | ||
| sessionName: '', | ||
| subscriptionName: event.triggerName, | ||
| event, | ||
| status: 'completed' as const, | ||
| stdout: '', | ||
| stderr: '', | ||
| exitCode: 0, | ||
| durationMs: 0, | ||
| startedAt: new Date().toISOString(), | ||
| endedAt: new Date().toISOString(), | ||
| }; | ||
| }, |
There was a problem hiding this comment.
stub implementation doesn't execute prompts — entire feature non-functional
The onCueRun callback is still using a placeholder that immediately returns fake "completed" results without spawning any agent processes. Import and call executeCuePrompt from cue-executor.ts:
| onCueRun: async (sessionId, _prompt, event) => { | |
| // Stub for Phase 03 executor integration — returns a placeholder result. | |
| // The actual executor (cue-executor.ts) is wired in a future phase. | |
| logger.info(`[CUE] Run triggered for session ${sessionId}: ${event.triggerName}`, 'Cue'); | |
| return { | |
| runId: event.id, | |
| sessionId, | |
| sessionName: '', | |
| subscriptionName: event.triggerName, | |
| event, | |
| status: 'completed' as const, | |
| stdout: '', | |
| stderr: '', | |
| exitCode: 0, | |
| durationMs: 0, | |
| startedAt: new Date().toISOString(), | |
| endedAt: new Date().toISOString(), | |
| }; | |
| }, | |
| onCueRun: async (sessionId, prompt, event) => { | |
| const session = sessionsStore.get('sessions', []).find((s: any) => s.id === sessionId); | |
| if (!session) { | |
| logger.error(`[CUE] Session not found: ${sessionId}`, 'Cue'); | |
| return { | |
| runId: event.id, | |
| sessionId, | |
| sessionName: '', | |
| subscriptionName: event.triggerName, | |
| event, | |
| status: 'failed' as const, | |
| stdout: '', | |
| stderr: 'Session not found', | |
| exitCode: null, | |
| durationMs: 0, | |
| startedAt: new Date().toISOString(), | |
| endedAt: new Date().toISOString(), | |
| }; | |
| } | |
| const { executeCuePrompt } = await import('./cue/cue-executor'); | |
| const agentConfigs = agentConfigsStore.get('configs', {}) as Record<string, any>; | |
| const sessionConfig = agentConfigs[session.toolType] || {}; | |
| return await executeCuePrompt({ | |
| runId: event.id, | |
| session: { | |
| id: session.id, | |
| name: session.name, | |
| toolType: session.toolType, | |
| cwd: session.cwd || session.fullPath || os.homedir(), | |
| projectRoot: session.cwd || session.fullPath || os.homedir(), | |
| }, | |
| subscription: { name: event.triggerName, event: event.type, enabled: true, prompt }, | |
| event, | |
| promptPath: prompt, | |
| toolType: session.toolType, | |
| projectRoot: session.cwd || session.fullPath || os.homedir(), | |
| templateContext: { | |
| session: { | |
| id: session.id, | |
| name: session.name, | |
| toolType: session.toolType, | |
| cwd: session.cwd || session.fullPath || os.homedir(), | |
| projectRoot: session.cwd || session.fullPath || os.homedir(), | |
| }, | |
| }, | |
| timeoutMs: 30 * 60 * 1000, | |
| sshRemoteConfig: session.sshRemoteConfig, | |
| customPath: sessionConfig.customPath, | |
| customArgs: sessionConfig.customArgs, | |
| customEnvVars: sessionConfig.customEnvVars, | |
| customModel: session.customModel, | |
| onLog: (level, message) => logger.cue(message, 'Cue'), | |
| sshStore: createSshRemoteStoreAdapter(store), | |
| agentConfigValues: sessionConfig, | |
| }); | |
| }, |
Add github.pull_request and github.issue event types to CueEventType union. Add repo and poll_minutes fields to CueSubscription interface. Add cue_github_seen SQLite table with 5 CRUD functions for tracking seen GitHub items (isGitHubItemSeen, markGitHubItemSeen, hasAnyGitHubSeen, pruneGitHubSeen, clearGitHubSeenForSubscription). Create cue-github-poller.ts module that polls GitHub CLI for new PRs/issues, seeds existing items on first run, and fires CueEvents for new items. Comprehensive test suite with 17 test cases covering all polling behaviors. All 264 Cue tests pass, lint clean.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/main/cue/cue-github-poller.ts (1)
246-252: Multiple pollers create redundant prune intervals.Each poller instance starts its own 24-hour prune interval for
pruneGitHubSeen. If multiple GitHub subscriptions exist, this creates redundant intervals all doing the same global cleanup. Consider moving the prune scheduling to a central location (e.g., CueEngine) similar to howpruneCueEventsis called once at startup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-github-poller.ts` around lines 246 - 252, The code in cue-github-poller.ts creates a per-instance pruneInterval via setInterval that calls pruneGitHubSeen, causing redundant global cleanup when multiple pollers exist; remove the setInterval/pruneInterval scheduling from the GitHubPoller (delete the setInterval block that references pruneGitHubSeen and pruneInterval) and ensure pruneGitHubSeen remains exported/accessible, then add a single central scheduler call for pruneGitHubSeen (mirroring pruneCueEvents) inside the CueEngine startup/bootstrap area so it runs once at app-level; if needed, also remove any local clearInterval logic tied to pruneInterval in the GitHub poller to avoid dangling references.
🤖 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/__tests__/main/cue/cue-github-poller.test.ts`:
- Around line 498-517: The test creates a poller with
createCueGitHubPoller(config) but never stops it; capture the returned
cleanup/stop function from createCueGitHubPoller and call it (or await it if it
returns a promise) at the end of the test (or in a finally/afterEach) to clear
the timer and prevent leakage; update the testcase to assign const cleanup =
createCueGitHubPoller(config) and call await cleanup() (or cleanup()) after the
assertions to ensure timers are cleared.
In `@src/main/cue/cue-db.ts`:
- Around line 275-280: The GitHub seen helpers should guard against an
uninitialized Cue DB to avoid throwing when initCueDb() failed; add an
isCueDbReady() check at the top of isGitHubItemSeen(subscriptionId, itemKey) and
the sibling markGitHubItemSeen(...) to return a safe fallback (e.g., false for
isGitHubItemSeen and no-op for markGitHubItemSeen) when the DB isn't ready, and
ensure initSession() still logs the DB failure but does not let the poller run
silently relying on these guarded functions; reference the functions
isGitHubItemSeen, markGitHubItemSeen, initCueDb, initSession and add the
isCueDbReady guard so callers won't receive "Cue database not initialized"
exceptions.
In `@src/main/cue/cue-github-poller.ts`:
- Around line 103-104: The isFirstRun check using
hasAnyGitHubSeen(subscriptionId) is evaluated once per poll, creating a race
where partial progress can cause re-emission; change the flow so subscription
initialization is atomic and per-subscription (not per-poll): compute and
persist an "initialized" marker for subscriptionId before processing items (or
check-and-set atomically), then for each item consult per-item seen state before
emitting and mark items as seen before emitting events; update references around
items, isFirstRun, hasAnyGitHubSeen(subscriptionId) and the per-item emission
logic to use the persisted initialized flag and per-item seen checks so partial
runs cannot cause duplicate first-run emissions.
In `@src/main/cue/cue-types.ts`:
- Around line 85-96: CueSessionStatus in cue-types.ts declares
projectRoot:string but the preload's CueSessionStatus type (in the preload cue
module) omits it, causing the field to be stripped at the IPC boundary; fix by
making the two definitions identical: either add projectRoot:string to the
preload's CueSessionStatus type (the type used in the preload cue module) or,
better, export CueSessionStatus from the shared cue-types declaration and import
that single type into the preload and renderer so both sides use the exact same
interface; update any references to CueSessionStatus in the preload module (and
imports) accordingly so projectRoot is passed through the IPC intact.
---
Nitpick comments:
In `@src/main/cue/cue-github-poller.ts`:
- Around line 246-252: The code in cue-github-poller.ts creates a per-instance
pruneInterval via setInterval that calls pruneGitHubSeen, causing redundant
global cleanup when multiple pollers exist; remove the setInterval/pruneInterval
scheduling from the GitHubPoller (delete the setInterval block that references
pruneGitHubSeen and pruneInterval) and ensure pruneGitHubSeen remains
exported/accessible, then add a single central scheduler call for
pruneGitHubSeen (mirroring pruneCueEvents) inside the CueEngine
startup/bootstrap area so it runs once at app-level; if needed, also remove any
local clearInterval logic tied to pruneInterval in the GitHub poller to avoid
dangling references.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fbc473d0-d950-436d-ad4a-5b84c76ce58d
📒 Files selected for processing (5)
src/__tests__/main/cue/cue-db.test.tssrc/__tests__/main/cue/cue-github-poller.test.tssrc/main/cue/cue-db.tssrc/main/cue/cue-github-poller.tssrc/main/cue/cue-types.ts
| export function isGitHubItemSeen(subscriptionId: string, itemKey: string): boolean { | ||
| const row = getDb() | ||
| .prepare(`SELECT 1 FROM cue_github_seen WHERE subscription_id = ? AND item_key = ?`) | ||
| .get(subscriptionId, itemKey); | ||
| return row !== undefined; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check how createCueGitHubPoller is called in CueEngine
rg -n -B5 -A10 'createCueGitHubPoller' src/main/cue/cue-engine.tsRepository: RunMaestro/Maestro
Length of output: 1451
🏁 Script executed:
# Check the initSession function to see initialization order
rg -n -B3 -A30 'async initSession' src/main/cue/cue-engine.tsRepository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
# Check markGitHubItemSeen implementation for guards
rg -n -B2 -A8 'markGitHubItemSeen' src/main/cue/cue-db.tsRepository: RunMaestro/Maestro
Length of output: 423
🏁 Script executed:
# Check the GitHub poller implementation to understand the 2-second delay
fd -t f 'cue-github-poller.ts' src/ | head -1 | xargs cat -n | head -100Repository: RunMaestro/Maestro
Length of output: 3745
🏁 Script executed:
# Find the initial timeout duration in GitHub poller
rg -n -A20 'initialTimeout.*=' src/main/cue/cue-github-poller.ts | head -50Repository: RunMaestro/Maestro
Length of output: 1467
🏁 Script executed:
# Find initCueDb call in CueEngine to see when/if it's awaited
rg -n -B3 -A10 'initCueDb' src/main/cue/cue-engine.tsRepository: RunMaestro/Maestro
Length of output: 1234
🏁 Script executed:
# Find the full initSession method to understand initialization order
rg -n -A100 'initSession\(session:' src/main/cue/cue-engine.ts | head -150Repository: RunMaestro/Maestro
Length of output: 3591
🏁 Script executed:
# Find initCueDb implementation
rg -n -A30 '^export function initCueDb' src/main/cue/cue-db.tsRepository: RunMaestro/Maestro
Length of output: 1013
🏁 Script executed:
# Also check getDb() to understand what throws
rg -n -B2 -A8 '^function getDb\(\)' src/main/cue/cue-db.tsRepository: RunMaestro/Maestro
Length of output: 501
🏁 Script executed:
# Check if doPoll() in the GitHub poller has any error handling for DB calls
rg -n -B5 -A20 'async function doPoll' src/main/cue/cue-github-poller.tsRepository: RunMaestro/Maestro
Length of output: 785
🏁 Script executed:
# Check if markGitHubItemSeen or isGitHubItemSeen are called with try-catch
rg -n -B3 -A3 'markGitHubItemSeen\|isGitHubItemSeen' src/main/cue/cue-github-poller.tsRepository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
# Find where markGitHubItemSeen and isGitHubItemSeen are called in the poller
rg -n 'markGitHubItemSeen\|isGitHubItemSeen' src/main/cue/cue-github-poller.tsRepository: RunMaestro/Maestro
Length of output: 44
🏁 Script executed:
# Get the full pollPRs function to see the DB call context
rg -n -A50 'async function pollPRs' src/main/cue/cue-github-poller.ts | head -80Repository: RunMaestro/Maestro
Length of output: 1624
Add isCueDbReady() guard in GitHub seen functions to handle DB initialization failures gracefully.
If initCueDb() fails with a file system or permission error, the exception is caught and logged but execution continues. The GitHub poller still starts its 2-second timer and will attempt to call markGitHubItemSeen() or isGitHubItemSeen(), both of which will throw "Cue database not initialized". While the poll's try-catch prevents a crash, the poll fails silently without clear indication that it's due to DB unavailability.
Consider either:
- Adding an
isCueDbReady()check in these functions with graceful fallback (skip marking/checking seen state) - Skipping poller setup in
initSession()if DB initialization fails
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/cue/cue-db.ts` around lines 275 - 280, The GitHub seen helpers
should guard against an uninitialized Cue DB to avoid throwing when initCueDb()
failed; add an isCueDbReady() check at the top of
isGitHubItemSeen(subscriptionId, itemKey) and the sibling
markGitHubItemSeen(...) to return a safe fallback (e.g., false for
isGitHubItemSeen and no-op for markGitHubItemSeen) when the DB isn't ready, and
ensure initSession() still logs the DB failure but does not let the poller run
silently relying on these guarded functions; reference the functions
isGitHubItemSeen, markGitHubItemSeen, initCueDb, initSession and add the
isCueDbReady guard so callers won't receive "Cue database not initialized"
exceptions.
| const items = JSON.parse(stdout); | ||
| const isFirstRun = !hasAnyGitHubSeen(subscriptionId); |
There was a problem hiding this comment.
isFirstRun determined per-poll creates race window.
The isFirstRun check via hasAnyGitHubSeen(subscriptionId) is evaluated once at the start of each poll. If the first poll is interrupted after marking some items but before completing, the next poll will see hasAnyGitHubSeen return true and emit events for the remaining items that were never seeded.
This is a minor edge case but could cause unexpected event emissions on first run if the poller is stopped mid-iteration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/cue/cue-github-poller.ts` around lines 103 - 104, The isFirstRun
check using hasAnyGitHubSeen(subscriptionId) is evaluated once per poll,
creating a race where partial progress can cause re-emission; change the flow so
subscription initialization is atomic and per-subscription (not per-poll):
compute and persist an "initialized" marker for subscriptionId before processing
items (or check-and-set atomically), then for each item consult per-item seen
state before emitting and mark items as seen before emitting events; update
references around items, isFirstRun, hasAnyGitHubSeen(subscriptionId) and the
per-item emission logic to use the persisted initialized flag and per-item seen
checks so partial runs cannot cause duplicate first-run emissions.
| /** Status summary for a Cue-enabled session */ | ||
| export interface CueSessionStatus { | ||
| sessionId: string; | ||
| sessionName: string; | ||
| toolType: string; | ||
| projectRoot: string; | ||
| enabled: boolean; | ||
| subscriptionCount: number; | ||
| activeRuns: number; | ||
| lastTriggered?: string; | ||
| nextTrigger?: string; | ||
| } |
There was a problem hiding this comment.
Type mismatch with preload layer — CueSessionStatus.projectRoot missing in preload.
The CueSessionStatus interface here includes projectRoot: string (line 90), but the relevant code snippet from src/main/preload/cue.ts:44-54 shows the preload's CueSessionStatus type omits this field. This will cause the projectRoot value to be stripped at the IPC boundary, potentially breaking renderer code that expects it.
Consider either:
- Adding
projectRootto the preload'sCueSessionStatustype - Importing these types from a shared location instead of re-declaring them
#!/bin/bash
# Check if renderer code uses projectRoot from CueSessionStatus
rg -n 'projectRoot' src/renderer/ --type ts --type tsx🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/cue/cue-types.ts` around lines 85 - 96, CueSessionStatus in
cue-types.ts declares projectRoot:string but the preload's CueSessionStatus type
(in the preload cue module) omits it, causing the field to be stripped at the
IPC boundary; fix by making the two definitions identical: either add
projectRoot:string to the preload's CueSessionStatus type (the type used in the
preload cue module) or, better, export CueSessionStatus from the shared
cue-types declaration and import that single type into the preload and renderer
so both sides use the exact same interface; update any references to
CueSessionStatus in the preload module (and imports) accordingly so projectRoot
is passed through the IPC intact.
…ge) to Cue patterns
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/shared/templateVariables.ts (1)
267-300:⚠️ Potential issue | 🟠 MajorEscape replacement values to prevent
$token corruption in template substitution.Line 299 uses JavaScript's replacement-string mode, which interprets
$1,$&,$$, and other sequences as special tokens. If Cue values likeCUE_SOURCE_OUTPUTorCUE_GH_BODYcontain these sequences, template content can be unintentionally altered. For example, a value containing$1would be interpreted as a backreference instead of a literal string.Suggested fix
for (const [key, value] of Object.entries(replacements)) { // Match {{KEY}} with case insensitivity const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'gi'); - result = result.replace(regex, value); + result = result.replace(regex, () => value); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/templateVariables.ts` around lines 267 - 300, The current replacement loop uses result.replace(regex, value) which treats dollar sequences like $1 as replacement tokens and can corrupt values (e.g., CUE_SOURCE_OUTPUT or CUE_GH_BODY); change the call to use a replacer function so the replacement is used verbatim (e.g., result = result.replace(regex, () => value)) or alternatively escape $ by replacing them with $$ on the value before calling replace; update the loop that iterates over Object.entries(replacements) (and references replacements, result, regex) to use one of these safe approaches.
♻️ Duplicate comments (3)
src/main/cue/cue-engine.ts (1)
253-267:⚠️ Potential issue | 🔴 CriticalStopped runs still leak concurrency slots.
Line 263 removes the run, but Line 266 returns without decrementing
activeRunCountor callingdrainQueue. This can permanently reduce available concurrency after manual stops.💡 Suggested fix
stopRun(runId: string): boolean { const run = this.activeRuns.get(runId); if (!run) return false; @@ + const sessionId = run.result.sessionId; this.activeRuns.delete(runId); this.pushActivityLog(run.result); + const count = this.activeRunCount.get(sessionId) ?? 1; + this.activeRunCount.set(sessionId, Math.max(0, count - 1)); + this.drainQueue(sessionId); this.deps.onLog('cue', `[CUE] Run stopped: ${runId}`); return true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-engine.ts` around lines 253 - 267, The stopRun method leaks concurrency because it deletes the run from this.activeRuns but never updates this.activeRunCount or triggers this.drainQueue; modify stopRun (the method named stopRun) to decrement this.activeRunCount (safely ensure it doesn't go negative) and call this.drainQueue() after deleting the run and before returning, so stopped runs release their concurrency slot; keep existing abortController, result updates, pushActivityLog(run.result) and deps.onLog calls intact.src/main/cue/cue-executor.ts (2)
355-358:⚠️ Potential issue | 🔴 CriticalUse exit state, not
child.killed, for SIGKILL escalation.Line 356 checks
child.killed, which does not guarantee process termination. Escalation should check runtime state (exitCode/signalCode) instead.💡 Suggested fix
setTimeout(() => { - if (!child.killed) { + if (child.exitCode === null && child.signalCode === null) { child.kill('SIGKILL'); } }, SIGKILL_DELAY_MS);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-executor.ts` around lines 355 - 358, The timeout escalation currently inspects child.killed before sending SIGKILL; replace that check with the child's runtime exit state (e.g. child.exitCode and child.signalCode) so you only escalate if the process is still running. In the setTimeout callback that calls child.kill('SIGKILL'), change the condition to verify exitCode and signalCode are both null/undefined (indicating the process hasn't exited) rather than testing child.killed, and then invoke child.kill('SIGKILL') if still running; keep the existing setTimeout and kill invocation (refer to the setTimeout callback, child.killed check, and child.kill('SIGKILL') symbols to find the spot).
207-225:⚠️ Potential issue | 🔴 CriticalFail fast when SSH is enabled but SSH store is unavailable.
Line 207 silently runs locally when
sshRemoteConfig.enabledis true andsshStoreis missing, and Line 309 gates stdin routing on “SSH enabled” rather than “SSH wrapping applied”.💡 Suggested fix
let spawnArgs = finalArgs; let spawnCwd = projectRoot; let spawnEnvVars = effectiveEnvVars; let prompt: string | undefined = substitutedPrompt; + let sendPromptViaStdin = false; - if (sshRemoteConfig?.enabled && sshStore) { + if (sshRemoteConfig?.enabled) { + if (!sshStore) { + const message = `SSH is enabled for session "${session.name}" but SSH settings store is unavailable`; + onLog('error', message); + return { + runId, + sessionId: session.id, + sessionName: session.name, + subscriptionName: subscription.name, + event, + status: 'failed', + stdout: '', + stderr: message, + exitCode: null, + durationMs: Date.now() - startTime, + startedAt, + endedAt: new Date().toISOString(), + }; + } const sshWrapConfig: SshSpawnWrapConfig = { command, @@ command = sshResult.command; spawnArgs = sshResult.args; spawnCwd = sshResult.cwd; spawnEnvVars = sshResult.customEnvVars; prompt = sshResult.prompt; + sendPromptViaStdin = Boolean(sshResult.prompt); @@ - if (prompt && sshRemoteConfig?.enabled) { + if (prompt && sendPromptViaStdin) { child.stdin?.write(prompt); child.stdin?.end();Based on learnings: When implementing features that spawn agent processes, support SSH remote execution by checking for sshRemoteConfig, using wrapSpawnWithSsh() from ssh-spawn-wrapper.ts, and passing the SSH store adapter.
Also applies to: 309-313
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/cue/cue-executor.ts` around lines 207 - 225, When sshRemoteConfig.enabled is true but sshStore is missing you must fail fast instead of falling back to local execution: check sshRemoteConfig.enabled and if sshStore is falsy throw an error (or return a failure) with a clear message before building sshWrapConfig or calling wrapSpawnWithSsh; when you do call wrapSpawnWithSsh(sshWrapConfig, sshRemoteConfig, sshStore) assign its returned command/args/cwd/customEnvVars/prompt as you already do. Also fix the stdin-rerouting condition (the code that currently gates on sshRemoteConfig.enabled) to instead check whether SSH wrapping was actually applied (e.g., a boolean like sshWrapped or presence of sshResult) so stdin is routed only when the spawn was wrapped by wrapSpawnWithSsh.
🤖 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-yaml-loader.ts`:
- Around line 78-96: The parsing currently accepts any numeric
rawSettings.timeout_minutes (including <=0) when building the settings object;
update the construction of settings (where rawSettings and timeout_minutes are
used) to clamp/normalize timeout_minutes to a safe range (e.g., minimum 1 and at
most DEFAULT_CUE_SETTINGS.timeout_minutes or a defined MAX) and then ensure
validateCueConfig also enforces the same bounds; reference the rawSettings and
settings variables and the timeout_minutes field so you both sanitize at parse
time and add the same validation logic in validateCueConfig to reject or adjust
out-of-range values consistently.
- Around line 68-69: The properties sub.source_session and sub.fan_out are
currently accepted if they are arrays but their element types aren't validated,
allowing non-string entries that break matching; update assignments where sub is
processed (properties source_session and fan_out in cue-yaml-loader.ts, also the
block around lines 186-193) to only accept string elements by replacing the
naive Array.isArray checks with a filtered/validated form such as: if
Array.isArray(sub.source_session) then set source_session =
sub.source_session.filter(x => typeof x === 'string') (or undefined if result is
empty), and do the same for fan_out; additionally, where only array-ness is
checked (the check around line ~189), add element-type validation or throw a
validation error so invalid entries are rejected early.
---
Outside diff comments:
In `@src/shared/templateVariables.ts`:
- Around line 267-300: The current replacement loop uses result.replace(regex,
value) which treats dollar sequences like $1 as replacement tokens and can
corrupt values (e.g., CUE_SOURCE_OUTPUT or CUE_GH_BODY); change the call to use
a replacer function so the replacement is used verbatim (e.g., result =
result.replace(regex, () => value)) or alternatively escape $ by replacing them
with $$ on the value before calling replace; update the loop that iterates over
Object.entries(replacements) (and references replacements, result, regex) to use
one of these safe approaches.
---
Duplicate comments:
In `@src/main/cue/cue-engine.ts`:
- Around line 253-267: The stopRun method leaks concurrency because it deletes
the run from this.activeRuns but never updates this.activeRunCount or triggers
this.drainQueue; modify stopRun (the method named stopRun) to decrement
this.activeRunCount (safely ensure it doesn't go negative) and call
this.drainQueue() after deleting the run and before returning, so stopped runs
release their concurrency slot; keep existing abortController, result updates,
pushActivityLog(run.result) and deps.onLog calls intact.
In `@src/main/cue/cue-executor.ts`:
- Around line 355-358: The timeout escalation currently inspects child.killed
before sending SIGKILL; replace that check with the child's runtime exit state
(e.g. child.exitCode and child.signalCode) so you only escalate if the process
is still running. In the setTimeout callback that calls child.kill('SIGKILL'),
change the condition to verify exitCode and signalCode are both null/undefined
(indicating the process hasn't exited) rather than testing child.killed, and
then invoke child.kill('SIGKILL') if still running; keep the existing setTimeout
and kill invocation (refer to the setTimeout callback, child.killed check, and
child.kill('SIGKILL') symbols to find the spot).
- Around line 207-225: When sshRemoteConfig.enabled is true but sshStore is
missing you must fail fast instead of falling back to local execution: check
sshRemoteConfig.enabled and if sshStore is falsy throw an error (or return a
failure) with a clear message before building sshWrapConfig or calling
wrapSpawnWithSsh; when you do call wrapSpawnWithSsh(sshWrapConfig,
sshRemoteConfig, sshStore) assign its returned
command/args/cwd/customEnvVars/prompt as you already do. Also fix the
stdin-rerouting condition (the code that currently gates on
sshRemoteConfig.enabled) to instead check whether SSH wrapping was actually
applied (e.g., a boolean like sshWrapped or presence of sshResult) so stdin is
routed only when the spawn was wrapped by wrapSpawnWithSsh.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7d60ac62-7662-48c8-96f1-16a1db2ca950
📒 Files selected for processing (8)
src/__tests__/main/cue/cue-engine.test.tssrc/__tests__/main/cue/cue-executor.test.tssrc/__tests__/main/cue/cue-yaml-loader.test.tssrc/main/cue/cue-engine.tssrc/main/cue/cue-executor.tssrc/main/cue/cue-yaml-loader.tssrc/renderer/constants/cuePatterns.tssrc/shared/templateVariables.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/tests/main/cue/cue-executor.test.ts
- src/tests/main/cue/cue-engine.test.ts
- src/tests/main/cue/cue-yaml-loader.test.ts
Add GitHub Pull Request and GitHub Issue event type blocks with descriptions, YAML configuration examples, and seven new GitHub template variables (CUE_GH_*) to the Cue Help Modal documentation.
…bled Show a CUE entry type description (with Zap icon and teal badge) in the History Panel Guide modal, gated on the maestroCue encore feature flag. Describes the trigger types: file changes, time intervals, agent completions, GitHub activity, and pending tasks.
- maestro-cue.md: Overview, enabling, quick start, modal UI, shortcuts - maestro-cue-configuration.md: Full YAML schema, settings, validation - maestro-cue-events.md: All 6 event types with payloads and examples - maestro-cue-advanced.md: Fan-in/out, filtering, chaining, templates - Update encore-features.md and docs.json to wire into navigation
…ndex - Add standalone CueYamlEditor modal with lazy loading and modalStore integration - Add "Configure Maestro Cue" to session context menu and Quick Actions - Thread onConfigureCue through AppModals, SessionList, and QuickActionsModal - Fix CueHelpModal z-index to use MODAL_PRIORITIES.CUE_HELP instead of hardcoded 50 - Change Maestro Cue shortcut from Cmd+Shift+Q (conflicts with macOS quit) to Opt+Q
…nd session resume
- New docs/maestro-cue-examples.md with 8 complete workflow examples: CI pipeline, selective chaining, research swarm, PR review with follow-up, TODO task queue, multi-env deploy, issue triage, debate - Document triggeredBy, status, exitCode, durationMs as filterable payload fields on agent.completed events - Add triggeredBy filter example to advanced patterns doc - Add triggeredBy to CueHelpModal filter table and agent.completed desc - Add examples page to docs.json navigation
…letion metadata Expose 5 event payload fields as template variables that were previously only available for filter matching: CUE_FILE_CHANGE_TYPE (add/change/unlink), CUE_SOURCE_STATUS, CUE_SOURCE_EXIT_CODE, CUE_SOURCE_DURATION, and CUE_SOURCE_TRIGGERED_BY. Updated help modal, docs, and tests.
…p content - Add CueGraphView canvas component with d3-force layout showing trigger→agent relationships - Add Dashboard/Graph tab switcher to CueModal header - Add cue:getGraphData IPC pipeline (engine → handler → preload → renderer) - Standardize CueModal, UsageDashboard, and DirectorNotes modal sizes (80vw/1400/85vh/900) - Convert CueHelpModal to inline CueHelpContent that swaps within CueModal body - Show agent name in CueYamlEditor title
…and menu icons Cue blue (#06b6d4) is retained only for lightning bolt indicators in history and activity logs where it serves as a semantic color for Cue-triggered content.
…visual editor - Create src/shared/cue-pipeline-types.ts with CuePipeline, PipelineNode, PipelineEdge, TriggerNodeData, AgentNodeData types and PIPELINE_COLORS palette - Create CuePipelineEditor component with React Flow canvas, Background, Controls, MiniMap, theme-aware styling, and placeholder toolbar/drawers - Wire CuePipelineEditor into CueModal Graph tab, replacing CueGraphView - Update CueModal tests for new pipeline editor component
Create TriggerNode (pill-shaped with event-type colors and icons), AgentNode (card with accent bar, pipeline count badge, multi-pipeline color strip), and PipelineEdge (bezier with mode labels and animated dash for autorun). Update CuePipelineEditor to register custom nodeTypes/edgeTypes and compute pipelineCount and pipelineColors per agent node.
…eline editor Add collapsible TriggerDrawer (left) and AgentDrawer (right) that let users drag triggers and agents onto the React Flow canvas. Integrate drop handling, connection validation, and node repositioning into the main CuePipelineEditor component.
…lor utilities, and CRUD operations
- NodeConfigPanel: event-specific trigger config (interval, glob, repo, etc.) and agent prompt textarea with pipeline membership display - EdgeConfigPanel: mode selector (pass/debate/autorun) with debate settings (max rounds, timeout) and autorun explanation - Selection handling: node/edge click, pane click to dismiss, Delete/Backspace keyboard shortcut to remove selected elements - All config changes update pipeline state immediately (debounced text inputs)
Create pipelineToYaml.ts that converts visual pipeline graph state into CueSubscription objects and YAML strings. Handles linear chains, fan-out, fan-in, and edge mode annotations (debate/autorun). Includes 13 passing tests.
Converts CueSubscription objects back into visual CuePipeline structures, enabling round-trip fidelity between YAML config and the graph editor. Supports chain grouping, fan-out, fan-in, deduplication, and auto-layout.
…d/validation - Load existing YAML subscriptions on mount via graphSessionsToPipelines - Add Save button that converts pipelines to YAML, writes via IPC, and refreshes sessions - Add dirty state tracking with visual indicator dot on Save button - Add graph validation before save (triggers, agents, cycles, disconnected nodes) - Add Discard Changes button to reload from YAML - Add validation error banner with inline error display - Pass projectRoot from CueModal to pipeline editor sessions - Update yamlToPipeline to use minimal interface types for flexibility
Add save/load IPC handlers for persisting pipeline graph layout (node positions, viewport zoom/pan, selected pipeline) to userData JSON file. Integrates with CuePipelineEditor to load saved positions on mount, merge with live graph data, and debounce-save on node drag end.
…move YAML editor UI - Rename 'Graph' tab to 'Pipeline Editor' and make it the default tab - Replace 'Edit YAML' button with 'View in Pipeline' in Dashboard sessions table - Remove CueYamlEditor rendering (kept as commented import for future use) - Update CueHelpContent to describe visual pipeline editor workflow - Update all related tests to reflect new default tab and UI changes
- Sessions table: new "Pipelines" column with colored dots showing which pipelines each session belongs to (with tooltip on hover) - Active Runs: pipeline color dot next to each subscription name - Activity Log: pipeline color dot replaces generic Zap icon when subscription maps to a known pipeline - Added utility functions for subscription-to-pipeline name mapping - Graph data now fetched for both Dashboard and Pipeline tabs
- Keyboard shortcuts: Escape (deselect/close drawers), Cmd+S (save), TODO for Cmd+Z undo - Empty state message with directional arrows when no nodes exist - Connection validation via isValidConnection (prevent self-connections, trigger-to-trigger, duplicates) - Right-click context menu on nodes (Configure, Delete, Duplicate for triggers) - Visual feedback for running pipelines with animated dash edges - MiniMap node coloring based on event type and pipeline colors
- Fix graphSessions dependency in loadLayout effect so YAML data populates the graph (was running once on mount before async fetch) - Add fuzzy filter to TriggerDrawer (search by label, event type, description) matching AgentDrawer's existing search - Add descriptions to trigger items for better discoverability - Theme-aware drawers: both TriggerDrawer and AgentDrawer now use theme.colors instead of hardcoded dark colors - Add comprehensive tests for both drawers (21 tests)
- Add hierarchical (left-to-right layers) layout algorithm with BFS depth assignment - Add layout algorithm dropdown selector (Hierarchical, Force-Directed) - Implement click-and-drag to reposition individual nodes with position persistence - Tune force-directed layout parameters (distance, charge, iterations) - Remove Encore Feature callout from CueHelpModal (no longer relevant) - Fix CueHelpModal test mock theme to match current ThemeColors interface Claude ID: 5e831d0a-5904-4fea-8520-0a217a968071 Maestro ID: 92788326-73ad-4c8c-b593-b9711e388139
- Fix dropdown text visibility with theme-aware textColor/borderColor - Fix agent node badge clipping (overflow: visible on root container) - Add pipeline legend bar in All Pipelines view with color + name - Auto-select dropped nodes to open config panel immediately - Add PipelineSelector and AgentNode test suites (17 new tests)
Add explicit GripVertical drag handles and Settings gear icons to TriggerNode and AgentNode for clear drag/configure affordances. Restrict node dragging to handle area via React Flow dragHandle selector. Config panels now use static insets matching drawer widths (220px left, 240px right) with rounded top corners.
Auto Run Summary
Documents processed:
Total tasks completed: 48
Changes
CHANGES
This PR was automatically created by Maestro Auto Run.
Summary by CodeRabbit
Release Notes