Skip to content

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

Open
pedramamini wants to merge 57 commits intomainfrom
maestro-cue-spinout
Open

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
pedramamini wants to merge 57 commits intomainfrom
maestro-cue-spinout

Conversation

@pedramamini
Copy link
Collaborator

@pedramamini pedramamini commented Mar 1, 2026

Auto Run Summary

Documents processed:

  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-01
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-02
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-03
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-04
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-05
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-06
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-07
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-08
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-09
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-10
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-11
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-12
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-13
  • 2026-02-14-Maestro-Cue/MAESTRO-CUE-14

Total tasks completed: 48

Changes

  • MAESTRO: Phase 14 - Named coordination pattern presets and AI pattern awareness
  • MAESTRO: Phase 13 - Event payload filtering for Cue subscriptions
  • MAESTRO: Phase 12 - Sleep/wake reconciliation with heartbeat and event journal
  • MAESTRO: Phase 11 - Per-agent concurrency control and event queuing
  • MAESTRO: Phase 10 - Update documentation tables with Cue references
  • MAESTRO: Phase 10 - Cue status indicator in session list (Encore Feature gated)
  • MAESTRO: Phase 10 - YAML hot reload with config change/removal detection and IPC push
  • MAESTRO: Phase 09 - Agent completion chains with fan-out, fan-in, and session bridging
  • MAESTRO: Phase 08 - Cue Help Modal and auto-discovery of maestro-cue.yaml
  • MAESTRO: Phase 07 - Cue YAML Editor with AI-assisted prompt generation
  • MAESTRO: Phase 06 - Cue Modal dashboard with sessions, active runs, and activity log
  • MAESTRO: Phase 05 - CUE log level test coverage for LogViewer component
  • MAESTRO: Phase 05 - CUE type rendering in History panel and detail modal
  • MAESTRO: Phase 04 - IPC handlers, preload API, and CueEngine initialization for Maestro Cue
  • MAESTRO: Phase 03 - Cue executor for background agent spawning and history recording
  • MAESTRO: Phase 02 - Cue Engine core, YAML loader, and file watcher provider
  • MAESTRO: Phase 01 - Cue foundational types, template variables, logger, and Encore feature flag
  • 0.15.0 RC polish round two (0.15.0 RC polish round two #485)
  • docs: sync release notes for v0.15.0-RC
  • fix: correct zustand useShallow import path in AppModals
  • Round O Polish (Round O Polish #482)
  • test: update clipboard error tests to match safeClipboardWrite behavior
  • fix: resolve Sentry crashes from debug logging, clipboard, and markdown details
  • Merge pull request refactor: App.tsx Tier 1-3 extractions (4,602 → 3,208 lines, −30%) #478 from RunMaestro/code-refactor
  • fix: address PR review round 2 — race conditions, stale state, and empty content guards
  • fix: address PR review findings across hooks, components, and tests
  • fix: add cursor-pointer to SettingsModal tab buttons
  • refactor: Tier 1 component self-sourcing from stores (3,417 → 3,208 lines)
  • refactor: extract Tier 3 inline functions from App.tsx (3,655 → 3,417 lines)
  • refactor: extract 10 Tier 2 hooks from App.tsx (4,602 → 3,655 lines)
  • Merge pull request refactor: extracted 3 domain-specific hooks from App.tsx #470 from RunMaestro/code-refactor
  • fix: address 7 verified bugs across App, hooks, and tests
  • fix: address 9 verified bugs across App, SessionList, hooks, and tests
  • fix: separate lint-staged rules for prettier and eslint
  • chore: widen lint-staged glob to all file types and reformat docs
  • merge: pull docs update from main (worktree auto-run docs)
  • style: fix Prettier formatting in path-prober.ts
  • merge: integrate main while preserving Phase 3 store-subscription pattern
  • docs: add Run in Worktree section to Auto Run and Git Worktrees docs
  • Add PATH prober utility and tests, fix TypeScript build error (Add PATH prober utility and tests, fix TypeScript build error #466)
  • CHANGES

  • fix: keyboard tab cycling skips orphaned tabs not in unifiedTabOrder
  • fix: remove ring outline from active agent dot in collapsed sidebar
  • docs: sync release notes for v0.15.0-RC
  • fix: convert wizard timeouts to activity-based with 20-min inactivity window
  • feat: add Personal AI Infrastructure and fabric to Symphony registry
  • refactor: extract useInterruptHandler hook from App.tsx (~400 lines)
  • refactor: extract useSessionCrud hook from App.tsx (~300 lines)
  • fix: keep loop completion counters accurate before reset (fix: keep auto-run loop completion counters consistent #459)
  • fix: resolve lint warnings and sync test mocks with component prop interfaces

This PR was automatically created by Maestro Auto Run.

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced Maestro Cue: an event-driven automation system with a dedicated dashboard for managing automated workflows
    • Added maestro-cue.yaml configuration support for defining automation triggers and settings
    • New Maestro Cue modal (Cmd+Shift+Q) for monitoring active runs and managing automation sessions
    • Session indicators now display active automation subscriptions with visual badges
    • New CUE history entry type for tracking automation-triggered executions
    • Expanded template variables to provide automation context data

…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.
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.
@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Documentation
CLAUDE-IPC.md, CLAUDE-PATTERNS.md, CLAUDE.md
New sections documenting Maestro Cue API surface, patterns, architecture, configuration file structure, and integration points.
Dependencies
package.json
Added runtime dependencies js-yaml and picomatch, and corresponding TypeScript type declarations.
Core Engine & Types
src/main/cue/cue-engine.ts, src/main/cue/cue-types.ts
New CueEngine class for session orchestration, event dispatch, concurrency/queue management, and fan-out/fan-in coordination. Comprehensive type definitions for events, subscriptions, configuration, and run results.
Configuration & YAML
src/main/cue/cue-yaml-loader.ts
YAML loader with hot-reload watching, validation, and default settings application for maestro-cue.yaml files.
Data Persistence
src/main/cue/cue-db.ts
SQLite-backed database for event journaling, heartbeat tracking, and GitHub seen-state management with lifecycle and query operations.
Execution & Process
src/main/cue/cue-executor.ts
Prompt execution handler with template variable substitution, agent configuration, SSH wrapping, timeout escalation, and history entry recording.
Event Sources
src/main/cue/cue-file-watcher.ts, src/main/cue/cue-github-poller.ts, src/main/cue/cue-task-scanner.ts
File watching with debouncing, GitHub PR/issue polling with seen-state tracking, and task scanner for markdown-based pending tasks.
Utility Modules
src/main/cue/cue-filter.ts, src/main/cue/cue-reconciler.ts
Event payload filtering engine and sleep/wake reconciliation for missed time-interval events.
IPC & Preload
src/main/ipc/handlers/cue.ts, src/main/preload/cue.ts, src/main/preload/index.ts
IPC handler registration and browser-context API surface exposing status, control, YAML operations, and real-time activity subscriptions via window.maestro.cue.
Main Process Integration
src/main/index.ts, src/main/process-listeners/exit-listener.ts, src/main/process-listeners/types.ts, src/main/utils/logger.ts
Engine lifecycle management, exit event notifications for agent completion, new 'cue' log level, and dependency injection wiring.
History Entry Types
src/renderer/preload/files.ts, src/renderer/preload/directorNotes.ts, src/shared/types.ts, src/shared/logger-types.ts
Extended HistoryEntryType and related interfaces to include 'CUE' variant with optional metadata fields.
Renderer UI Components
src/renderer/components/CueModal.tsx, src/renderer/components/CueYamlEditor.tsx, src/renderer/components/CueHelpModal.tsx
Main Maestro Cue dashboard with sessions table, active runs, activity log, YAML editor with pattern presets, and comprehensive help modal.
History UI Integration
src/renderer/components/History/HistoryEntryItem.tsx, src/renderer/components/History/HistoryFilterToggle.tsx, src/renderer/components/HistoryDetailModal.tsx, src/renderer/components/HistoryPanel.tsx
CUE entry rendering with teal styling, Zap icon, metadata display, filtering support, and status indicators in history views.
Log Viewer & Session UI
src/renderer/components/LogViewer.tsx, src/renderer/components/SessionItem.tsx, src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
New cue log level with styling, session cue subscription indicator with Zap icon, and updated default history filters to include CUE.
Modal & UI Wiring
src/renderer/components/AppModals.tsx, src/renderer/components/QuickActionsModal.tsx, src/renderer/components/SessionList/SessionList.tsx, src/renderer/components/SessionList/HamburgerMenuContent.tsx, src/renderer/App.tsx
CueModal integration into modal stack, menu entry with shortcut, hamburger menu item, auto-discovery hook wiring, and feature-gated rendering.
Settings & Features
src/renderer/components/Settings/tabs/EncoreTab.tsx, src/renderer/stores/settingsStore.ts, src/renderer/types/index.ts
New maestroCue Encore feature flag with toggle UI and state management.
Constants & Globals
src/renderer/constants/cuePatterns.ts, src/renderer/constants/modalPriorities.ts, src/renderer/constants/shortcuts.ts, src/renderer/global.d.ts
YAML pattern presets, modal priority values (CUE_HELP, CUE_YAML_EDITOR, CUE_MODAL), maestroCue keyboard shortcut (Meta+Shift+Q), and type definitions.
React Hooks
src/renderer/hooks/useCue.ts, src/renderer/hooks/useCueAutoDiscovery.ts, src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
Cue data fetching and real-time subscription, auto-discovery of projects with maestro-cue.yaml, and keyboard shortcut handling.
Modal Store
src/renderer/stores/modalStore.ts
CueModal state management with open/close actions.
Shared Types
src/shared/templateVariables.ts
Extended TemplateContext with cue fields for event metadata and GitHub/file/task information, plus new CUE_* template variables.
Comprehensive Tests
src/__tests__/main/cue/*.test.ts, src/__tests__/renderer/**/*.test.ts
20+ test files covering engine lifecycle, concurrency, session management, YAML loading, GitHub polling, task scanning, filtering, reconciliation, sleep/wake detection, IPC handlers, executor, and all UI components.
Minor Changes
.prettierignore, symphony-registry.json
Formatting updates; JSON reformatting with no semantic 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)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch maestro-cue-spinout

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Add tabIndex for keyboard accessibility.

The clickable entry card div lacks a tabIndex attribute, which would prevent keyboard-only users from focusing and activating it. As per coding guidelines, components that need keyboard focus should have tabIndex and 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 | 🟠 Major

CUE filter is correctly added, but stats tracking is incomplete.

The 'CUE' filter option works as expected. However, UnifiedHistoryStats only tracks autoCount and userCount, with totalCount being their sum. Since CUE is a legitimate, first-class entry type (recorded by recordCueHistoryEntry() 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 autoCount and userCount (lines 166-167), missing CUE entries entirely. Add a cueCount field to UnifiedHistoryStats and 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 | 🟠 Major

Align CueSessionStatus.projectRoot with the API contract (optional).

Line 39 makes projectRoot required, 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 | 🟠 Major

Don’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 | 🟠 Major

Guard Cue completion notification against engine failures.

A throw inside getCueEngine(), hasCompletionSubscribers(), or notifyAgentCompleted() 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 | 🟠 Major

Use baseSessionId when notifying cue engine of agent completion.

The exit listener extracts baseSessionId for web broadcasting (line 439), but passes the raw suffixed sessionId to cue notifications (lines 452-453). Since hasCompletionSubscribers() and notifyAgentCompleted() match subscriptions against raw sessionId or 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 baseSessionId to both hasCompletionSubscribers() and notifyAgentCompleted() 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 | 🟠 Major

Fix JSX comment-text lint blockers in example strings.

Line 162, Line 211, and Line 310 include /*-like content in JSX text nodes, which triggers Biome noCommentText.

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 | 🟠 Major

Negated glob filters are currently evaluated as exact-string negation.

At Line 57–Line 63, !*.test.ts won’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 | 🟠 Major

Don’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 | 🟠 Major

Reject unknown event values and malformed source_session arrays in validation.

Line 163 only enforces event as a string, so typos are treated as valid config. Also, source_session arrays are not validated for string elements, allowing structurally invalid agent.completed configs 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 | 🟠 Major

Harden 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 | 🟠 Major

Backdrop 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, so onClose does 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 | 🟠 Major

Replace synchronous filesystem calls with async alternatives in IPC handlers.

The cue:readYaml and cue:writeYaml handlers 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 | 🟠 Major

Avoid mutating shared templateContext in 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 | 🟠 Major

Save 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 | 🟠 Major

Validation 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 | 🟠 Major

Risk of infinite recursion in completion chain.

After a Cue run completes, notifyAgentCompleted is called (line 828), which could trigger another subscription listening for that session's completion. If a subscription's source_session matches 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 | 🟡 Minor

Use 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 | 🟡 Minor

Add an accessible label for the Cue badge.

Line 164 relies on title only. That won’t reliably expose the status to screen readers. Add an explicit aria-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 | 🟡 Minor

Remove 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 | 🟡 Minor

Make the sleep-duration assertion deterministic.

Line 276 and Line 278 call Date.now() separately, so gapMs can exceed sleepDuration by 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 | 🟡 Minor

Add cue:getQueueStatus to 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 | 🟡 Minor

Preload Cue types are drifting from the shared core types.

CueSessionStatus here is missing projectRoot, 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 #06b6d4 is hardcoded here and duplicated in HistoryFilterToggle.tsx. Consider defining a shared constant (e.g., CUE_TEAL as seen in CueModal.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-x class, which is an implementation detail of the Lucide icon library. If the library changes its class naming, this test would break silently. Consider adding a data-testid or aria-label to 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 with waitFor() 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 with waitFor() 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.error directly bypasses any structured logging. If a logger utility is available (as suggested by the imports list mentioning src/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 mockTheme object is missing the accentForeground property 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 for type and status fields.

These are currently plain strings; switching to CueEventType/CueRunStatus will 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

📥 Commits

Reviewing files that changed from the base of the PR and between 72c43b4 and 6c2c252.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (78)
  • CLAUDE-IPC.md
  • CLAUDE-PATTERNS.md
  • CLAUDE.md
  • package.json
  • src/__tests__/main/cue/cue-completion-chains.test.ts
  • src/__tests__/main/cue/cue-concurrency.test.ts
  • src/__tests__/main/cue/cue-db.test.ts
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-executor.test.ts
  • src/__tests__/main/cue/cue-file-watcher.test.ts
  • src/__tests__/main/cue/cue-filter.test.ts
  • src/__tests__/main/cue/cue-ipc-handlers.test.ts
  • src/__tests__/main/cue/cue-reconciler.test.ts
  • src/__tests__/main/cue/cue-sleep-wake.test.ts
  • src/__tests__/main/cue/cue-yaml-loader.test.ts
  • src/__tests__/main/process-listeners/exit-listener.test.ts
  • src/__tests__/renderer/components/CueHelpModal.test.tsx
  • src/__tests__/renderer/components/CueModal.test.tsx
  • src/__tests__/renderer/components/CueYamlEditor.test.tsx
  • src/__tests__/renderer/components/History/HistoryEntryItem.test.tsx
  • src/__tests__/renderer/components/History/HistoryFilterToggle.test.tsx
  • src/__tests__/renderer/components/HistoryDetailModal.test.tsx
  • src/__tests__/renderer/components/HistoryPanel.test.tsx
  • src/__tests__/renderer/components/LogViewer.test.tsx
  • src/__tests__/renderer/components/SessionItemCue.test.tsx
  • src/__tests__/renderer/components/SessionList.test.tsx
  • src/__tests__/renderer/hooks/useCue.test.ts
  • src/__tests__/renderer/hooks/useCueAutoDiscovery.test.ts
  • src/__tests__/setup.ts
  • src/__tests__/web/mobile/MobileHistoryPanel.test.tsx
  • src/main/cue/cue-db.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-executor.ts
  • src/main/cue/cue-file-watcher.ts
  • src/main/cue/cue-filter.ts
  • src/main/cue/cue-reconciler.ts
  • src/main/cue/cue-types.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/main/index.ts
  • src/main/ipc/handlers/cue.ts
  • src/main/ipc/handlers/director-notes.ts
  • src/main/ipc/handlers/index.ts
  • src/main/preload/cue.ts
  • src/main/preload/directorNotes.ts
  • src/main/preload/files.ts
  • src/main/preload/index.ts
  • src/main/process-listeners/exit-listener.ts
  • src/main/process-listeners/types.ts
  • src/main/utils/logger.ts
  • src/renderer/App.tsx
  • src/renderer/components/AppModals.tsx
  • src/renderer/components/CueHelpModal.tsx
  • src/renderer/components/CueModal.tsx
  • src/renderer/components/CueYamlEditor.tsx
  • src/renderer/components/DirectorNotes/UnifiedHistoryTab.tsx
  • src/renderer/components/History/HistoryEntryItem.tsx
  • src/renderer/components/History/HistoryFilterToggle.tsx
  • src/renderer/components/HistoryDetailModal.tsx
  • src/renderer/components/HistoryPanel.tsx
  • src/renderer/components/LogViewer.tsx
  • src/renderer/components/QuickActionsModal.tsx
  • src/renderer/components/SessionItem.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/constants/cuePatterns.ts
  • src/renderer/constants/modalPriorities.ts
  • src/renderer/constants/shortcuts.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/agent/useAgentSessionManagement.ts
  • src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/useCue.ts
  • src/renderer/hooks/useCueAutoDiscovery.ts
  • src/renderer/stores/modalStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/index.ts
  • src/shared/logger-types.ts
  • src/shared/templateVariables.ts
  • src/shared/types.ts

@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

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

  • CueEngine orchestrates subscriptions, timers, file watchers, and completion chains
  • Per-agent concurrency control with event queuing to prevent overwhelming agents
  • Sleep/wake reconciliation catches up on missed interval events after laptop sleep
  • Fan-out dispatches prompts to multiple agents; fan-in waits for multiple completions before firing
  • Event payload filtering with glob patterns and numeric comparisons

Infrastructure:

  • SQLite-based event journal tracks heartbeats for sleep detection
  • YAML hot reload detects config changes and removals with automatic refresh
  • SSH remote execution support for spawning agents on remote hosts
  • Comprehensive test coverage (5,368 lines across 11 test files)

UI Components:

  • Cue Modal dashboard displays session status, active runs, and activity log
  • Monaco-based YAML editor with validation, AI-assisted prompt generation, and pattern templates
  • Help modal with detailed documentation and examples
  • Cue status indicator (Zap icon) in session list
  • Keyboard shortcut (⌘⇧U) and command palette integration

Integration:

  • Properly gated behind maestroCue Encore Feature flag
  • 10 new Cue-specific template variables for event data
  • Agent completion notifications trigger downstream subscriptions
  • Named coordination pattern presets (research swarm, debate, sequential chains)

Critical Issue:
The onCueRun callback in src/main/index.ts:344-362 still uses a stub implementation that returns fake "completed" results without executing prompts. The executeCuePrompt function from cue-executor.ts must be imported and called for the feature to actually spawn agent processes and execute automation.

Confidence Score: 2/5

  • This PR contains a critical bug that makes the feature completely non-functional in production
  • While the implementation is well-architected with excellent test coverage, the core executor integration is missing. The onCueRun callback in main/index.ts uses a stub that never spawns agent processes, meaning Cue triggers will fire but no automation will actually execute. This is a blocking issue that must be resolved before merge.
  • Pay close attention to src/main/index.ts — the stub executor must be replaced with the real implementation

Important Files Changed

Filename Overview
src/main/index.ts Critical bug: onCueRun stub never executes prompts, making the feature non-functional
src/main/cue/cue-engine.ts Well-architected engine with concurrency control, fan-out/fan-in, sleep/wake reconciliation, and event queuing
src/main/cue/cue-executor.ts Robust executor with template variable substitution, SSH support, timeout handling, and process management
src/main/ipc/handlers/cue.ts IPC handlers for Cue engine control, status queries, and YAML management
src/renderer/components/CueModal.tsx Dashboard UI with session status, active runs, activity log, and YAML editor integration
src/renderer/components/CueYamlEditor.tsx Monaco-based YAML editor with syntax highlighting, validation, AI prompt generation, and pattern templates
src/shared/templateVariables.ts Added 10 Cue-specific template variables for event data, file paths, and source session output
src/renderer/App.tsx Integrated Cue modal, auto-discovery hook, and Encore Feature gating

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
Loading

Last reviewed commit: 6c2c252

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

79 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +344 to +362
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(),
};
},
Copy link

Choose a reason for hiding this comment

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

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:

Suggested change
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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 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 how pruneCueEvents is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c2c252 and 3916890.

📒 Files selected for processing (5)
  • src/__tests__/main/cue/cue-db.test.ts
  • src/__tests__/main/cue/cue-github-poller.test.ts
  • src/main/cue/cue-db.ts
  • src/main/cue/cue-github-poller.ts
  • src/main/cue/cue-types.ts

Comment on lines +275 to +280
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;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check how createCueGitHubPoller is called in CueEngine
rg -n -B5 -A10 'createCueGitHubPoller' src/main/cue/cue-engine.ts

Repository: 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.ts

Repository: RunMaestro/Maestro

Length of output: 44


🏁 Script executed:

# Check markGitHubItemSeen implementation for guards
rg -n -B2 -A8 'markGitHubItemSeen' src/main/cue/cue-db.ts

Repository: 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 -100

Repository: 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 -50

Repository: 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.ts

Repository: 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 -150

Repository: RunMaestro/Maestro

Length of output: 3591


🏁 Script executed:

# Find initCueDb implementation
rg -n -A30 '^export function initCueDb' src/main/cue/cue-db.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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 -80

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

  1. Adding an isCueDbReady() check in these functions with graceful fallback (skip marking/checking seen state)
  2. 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.

Comment on lines +103 to +104
const items = JSON.parse(stdout);
const isFirstRun = !hasAnyGitHubSeen(subscriptionId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +85 to +96
/** 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;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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:

  1. Adding projectRoot to the preload's CueSessionStatus type
  2. 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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 | 🟠 Major

Escape 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 like CUE_SOURCE_OUTPUT or CUE_GH_BODY contain these sequences, template content can be unintentionally altered. For example, a value containing $1 would 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 | 🔴 Critical

Stopped runs still leak concurrency slots.

Line 263 removes the run, but Line 266 returns without decrementing activeRunCount or calling drainQueue. 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 | 🔴 Critical

Use 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 | 🔴 Critical

Fail fast when SSH is enabled but SSH store is unavailable.

Line 207 silently runs locally when sshRemoteConfig.enabled is true and sshStore is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3916890 and 5c9c638.

📒 Files selected for processing (8)
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/__tests__/main/cue/cue-executor.test.ts
  • src/__tests__/main/cue/cue-yaml-loader.test.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-executor.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/renderer/constants/cuePatterns.ts
  • src/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
- 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.
- 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.
@pedramamini pedramamini self-assigned this Mar 7, 2026
@pedramamini pedramamini added the RC Getting soak time in RC branch now label Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RC Getting soak time in RC branch now

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant