Skip to content

refactor(cue): split shared contracts, config boundaries, and runtime orchestration#750

Merged
reachrazamair merged 5 commits intorcfrom
cue-polish
Apr 8, 2026
Merged

refactor(cue): split shared contracts, config boundaries, and runtime orchestration#750
reachrazamair merged 5 commits intorcfrom
cue-polish

Conversation

@reachrazamair
Copy link
Copy Markdown
Contributor

@reachrazamair reachrazamair commented Apr 7, 2026

Summary

This PR continues the Cue refactor with a strict behavior-preserving goal.

It reorganizes Cue in three architectural steps:

  1. Unify shared Cue contracts across main, preload, and renderer
  2. Split Cue config loading into distinct repository / normalization / validation concerns
  3. Decompose CueEngine runtime orchestration into focused services while keeping CueEngine as the public facade

The intent is to reduce responsibility overlap in Cue without changing:

  • YAML semantics
  • IPC channel names
  • preload API shape
  • renderer behavior
  • runtime execution behavior

Commit Stack

1. 40029853 refactor(cue): extract shared contracts and config boundaries

Introduces a shared Cue contract layer and moves Cue config responsibilities behind stable facades.

Included changes:

  • added shared Cue contracts under src/shared/cue/
  • converted src/main/cue/cue-types.ts into a compatibility facade over shared types
  • unified Cue types used by preload, renderer hooks, renderer globals, and pipeline graph types
  • split cue-yaml-loader.ts responsibilities into:
    • config repository
    • config normalizer
    • config validator
  • preserved the existing loadCueConfig, watchCueYaml, validateCueConfig, and resolveCueConfigPath entrypoints

2. 1fdd50e6 refactor(cue): extract runtime orchestration services

Decomposes the core runtime logic that had accumulated inside CueEngine.

Included changes:

  • extracted session-state helpers into cue-session-state.ts
  • extracted fan-out / dispatch behavior into cue-dispatch-service.ts
  • extracted completion-chain handling into cue-completion-service.ts
  • extracted query/status/graph/settings projection logic into cue-query-service.ts
  • extracted session lifecycle / setup / teardown / refresh behavior into cue-session-runtime-service.ts
  • kept CueEngine as the compatibility facade for callers while delegating internally to the new services

What Changed

Shared contracts

Cue shared DTOs now live in one place and are consumed consistently by:

  • main process
  • preload
  • renderer
  • pipeline editor shared types

This removes duplicated Cue type declarations and makes the boundary between shared and runtime-only types explicit.

Config boundaries

Cue config handling is now split by responsibility:

  • config file discovery and watching
  • YAML parsing and normalization
  • validation
  • prompt file hydration

The old cue-yaml-loader.ts module remains the stable public facade, so runtime callers and tests do not need a contract change.

Runtime orchestration

CueEngine no longer owns all of the following directly in one file:

  • session runtime state lifecycle
  • status/graph/settings query projection
  • dispatch/fan-out execution policy
  • completion chain routing and fan-in handoff

Those concerns now live in dedicated services, while CueEngine remains the integration surface used by the rest of the app.

Behavior Preservation

This PR is intended to preserve all existing Cue behavior.

Specifically unchanged:

  • .maestro/cue.yaml and legacy config support
  • config hot reload behavior
  • startup trigger behavior
  • scheduled trigger dedupe behavior
  • agent.completed chaining semantics
  • fan-in and fan-out behavior
  • queue/concurrency behavior
  • SSH execution behavior
  • IPC and preload Cue APIs
  • Cue modal status/graph/activity data behavior

Why This Refactor

Cue had become difficult to reason about because the architecture mixed:

  • shared data contracts
  • config parsing
  • runtime session lifecycle
  • query projection
  • dispatch policy
  • completion routing

This PR reduces that coupling so later refactor phases can proceed with less risk and smaller diffs.

Testing

Verified with:

  • npm run lint
  • npm run test -- src/__tests__/main/cue/cue-yaml-loader.test.ts src/__tests__/main/cue/cue-ipc-handlers.test.ts src/__tests__/main/cue/cue-engine.test.ts src/__tests__/main/cue/cue-concurrency.test.ts src/ __tests__/main/cue/cue-completion-chains.test.ts src/__tests__/main/cue/cue-startup.test.ts src/__tests__/main/cue/cue-executor.test.ts src/__tests__/main/cue/cue-sleep-wake.test.ts src/__tests__/main/cue/ cue-sleep-prevention.test.ts src/__tests__/main/cue/cue-session-lifecycle.test.ts src/__tests__/main/cue/cue-multi-hop-chains.test.ts

Manual Regression Checklist

  • Open Cue modal and confirm sessions, activity log, active runs, and queue status still load.
  • Toggle Cue off and on and confirm no duplicate startup firing.
  • Load a project with .maestro/cue.yaml and confirm Cue is discovered.
  • Load a project with legacy maestro-cue.yaml and confirm fallback still works.
  • Edit Cue YAML and confirm hot reload still logs and updates state.
  • Delete the Cue config and confirm Cue logs Config removed and tears down the session.
  • Recreate the Cue config and confirm the session is rediscovered.
  • Trigger a file.changed subscription and confirm exactly one run fires.
  • Trigger a time.heartbeat subscription and confirm interval behavior still works.
  • Trigger a time.scheduled subscription and confirm no same-minute double-fire regression.
  • Verify app.startup still fires once per boot only.
  • Verify single-source agent.completed chaining still passes {{CUE_SOURCE_OUTPUT}}.
  • Verify fan-in still waits for all required source sessions.
  • Verify fan-out still sends runs to all configured targets.
  • Verify prompt_file and output_prompt_file still resolve correctly.
  • If using SSH remote sessions, verify Cue-triggered runs still execute remotely.
  • If using GitHub-triggered subscriptions, verify polling still behaves as before.

Summary by CodeRabbit

  • New Features

    • YAML-based Cue configs with normalization, validation, canonical/legacy resolution and live watching for automatic reloads.
    • New runtime services for subscription dispatch (including fan-out), agent-completion handling, session runtime management, and query endpoints for status/graph/settings.
  • Refactor

    • Core Cue engine split into modular services; Cue types centralized and developer-facing API typings aligned across preload/renderer.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Modularizes the CUE subsystem: adds shared Cue contracts, config repo/normalizer/validator, session runtime/state, dispatch/completion/query services, refactors cue-engine/yaml loader to delegate to new services, and consolidates Cue types across preload/renderer.

Changes

Cohort / File(s) Summary
Shared contracts & exports
src/shared/cue/contracts.ts, src/shared/cue/index.ts
Add centralized Cue domain types, constants, DEFAULT_CUE_SETTINGS, and re-export them via shared entrypoint.
Config: repo / normalize / validate
src/main/cue/config/cue-config-repository.ts, src/main/cue/config/cue-config-normalizer.ts, src/main/cue/config/cue-config-validator.ts
Add config path resolution, synchronous read, chokidar-based watch+debounce, YAML→CUE normalization (prompt file resolution, filter/type coercion, settings fallback) and structural validation with aggregated errors.
Session runtime & state
src/main/cue/cue-session-runtime-service.ts, src/main/cue/cue-session-state.ts
Introduce per-session SessionState management, init/refresh/teardown/remove flows, YAML watcher lifecycle, sleep-prevention hooks, startup firing, and helpers for counts/next-trigger computation.
Dispatch & completion services
src/main/cue/cue-dispatch-service.ts, src/main/cue/cue-completion-service.ts
Add dispatching with fan-out (per-target prompt selection) and run execution calls; completion handling matches sources, enforces chain depth, validates filters, truncates source output, and delegates multi-source fan-in handling.
Query service
src/main/cue/cue-query-service.ts
Add status/graph/settings aggregation that reads session states and can load configs for offline sessions.
Engine & YAML loader integration
src/main/cue/cue-engine.ts, src/main/cue/cue-yaml-loader.ts
Refactor CueEngine to delegate lifecycle/dispatch/completion/query to services; YAML loader now delegates read/parse/materialize/validate to config modules and re-exports resolveCueConfigPath.
Subscription setup & triggers
src/main/cue/cue-subscription-setup.ts
Centralize dispatch vs execute decision via dispatchOrExecuteSubscription; setup helpers accept CueSubscription and route fan-out via dispatch service.
Type consolidation across boundaries
src/main/cue/cue-types.ts, src/main/preload/cue.ts, src/renderer/global.d.ts, src/renderer/hooks/useCue.ts, src/shared/cue-pipeline-types.ts
Replace duplicated local Cue type definitions with imports/re-exports from shared contracts and update IPC/preload/renderer typings and hook types.
Tests
src/__tests__/main/cue/cue-engine.test.ts
Adjust scheduled-prompt resolution test to assert inline hydrated prompt takes precedence over prompt_file path.

Sequence Diagram(s)

sequenceDiagram
  participant Agent as Agent (session)
  participant Completion as CueCompletionService
  participant FanIn as FanInTracker
  participant Dispatch as CueDispatchService
  participant RunMgr as RunManager

  Agent->>Completion: notifyAgentCompleted(sessionId, completionData)
  Completion->>Completion: deps.enabled() & chainDepth check
  alt multiple matching sources
    Completion->>FanIn: handleCompletion(subscription, matchingSources, completionData)
    FanIn->>Dispatch: decide target dispatches
    Dispatch->>RunMgr: executeRun(...) per resolved target
  else single matching source
    Completion->>Dispatch: dispatchSubscription(ownerSessionId, sub, event, sourceSessionName, chainDepth)
    Dispatch->>RunMgr: executeRun(...) (fan-out expanded if configured)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

approved

Poem

🐰 I nibble prompts beneath the moon,

configs tidy, types in tune.
Watchers hum and services spring,
fan-out flutters, completions sing—
a rabbit hops for CUE's new tune.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main refactoring: splitting shared contracts, config boundaries, and runtime orchestration in the Cue system.

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

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

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 8, 2026

Greptile Summary

This PR refactors the Cue subsystem in two passes: (1) centralizing shared DTOs under src/shared/cue/contracts.ts so main, preload, and renderer consume a single source of truth, and (2) decomposing the monolithic CueEngine into focused service modules (cue-dispatch-service, cue-completion-service, cue-query-service, cue-session-runtime-service, cue-session-state) while keeping CueEngine as the public facade.

The refactor is behavior-preserving — IPC channel names, preload API shape, YAML semantics, and runtime execution logic are unchanged.

Confidence Score: 5/5

Safe to merge — all changes are structural refactoring with no behavioral regressions found.

All four P2 findings are non-blocking: a documentation gap in getSettings(), an || vs ?? inconsistency in fan-out dispatch, an O(n×m) loop that can be simplified, and a silent filter-discard behaviour in the normalizer. None affect correctness. The extraction of services is clean, initialization order in the CueEngine constructor is correct, startupFiredKeys persistence semantics are preserved, and all public entrypoints remain compatible.

No files require special attention for merge safety.

Vulnerabilities

No security concerns identified. This is a pure refactoring with no new IPC endpoints, no changes to input validation logic, and no introduction of new external calls or secret handling.

Important Files Changed

Filename Overview
src/shared/cue/contracts.ts New shared contract layer centralizing all Cue DTOs — runtime-agnostic, free of Node/Electron deps, and re-exported cleanly via index.ts.
src/main/cue/cue-types.ts Converted to a compatibility facade re-exporting all shared types plus main-process-only helpers (createCueEvent, AgentCompletionData, legacy filename constants).
src/main/cue/config/cue-config-repository.ts Cleanly extracted file-discovery and chokidar watching from the old yaml-loader; debounce cleanup on watcher close is correct.
src/main/cue/config/cue-config-normalizer.ts YAML parsing + normalization split out correctly; minor concern that normalizeFilter silently discards the whole filter object on the first invalid value rather than dropping just the bad key.
src/main/cue/config/cue-config-validator.ts Validation logic extracted unchanged; picomatch glob validation, event-specific field checks, and settings bounds all intact.
src/main/cue/cue-dispatch-service.ts Fan-out and single-target dispatch correctly extracted; minor inconsistency using
src/main/cue/cue-completion-service.ts agent.completed chain handling extracted correctly; fan-in delegation to fanInTracker, chain-depth guard, and filter checks all preserved.
src/main/cue/cue-query-service.ts Status/graph/settings projection correctly extracted; getSettings() undocumented first-session tie-breaking is a minor style concern.
src/main/cue/cue-session-runtime-service.ts Session lifecycle (init, refresh, remove, teardown, clearAll) correctly extracted with pendingYamlWatchers; teardownSession has an O(n×m) scheduled-key cleanup loop that could be simplified.
src/main/cue/cue-session-state.ts Session state helpers (countActiveSubscriptions, getEarliestNextTriggerIso, toSessionStatus) extracted cleanly with no logic changes.
src/main/cue/cue-engine.ts CueEngine is now a lean coordinator/facade; constructor wires all services, startupFiredKeys persistence across stop/start is preserved, and service initialization order is correct.
src/main/cue/cue-yaml-loader.ts Now a stable public facade delegating to the three config modules; all original entrypoints preserved.
src/main/preload/cue.ts Updated to import types from shared/cue instead of duplicating them; re-exports the correct shared subset for the renderer.
src/renderer/global.d.ts Cue API types in MaestroAPI now reference shared/cue contracts directly — consistent with preload and hook changes.
src/renderer/hooks/useCue.ts Hook imports migrated to shared/cue with no behavioral changes to polling, activity update subscription, or state management.
src/shared/cue-pipeline-types.ts CueGraphSession is now a type alias for the shared contract rather than a local definition; CueEventType imported from shared/cue.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph shared["src/shared/cue/"]
        contracts["contracts.ts\n(CueConfig, CueSubscription,\nCueEvent, CueRunResult, ...)"]
        index["index.ts\n(barrel re-export)"]
        contracts --> index
    end

    subgraph config["src/main/cue/config/"]
        repo["cue-config-repository.ts\nfile discovery + chokidar watching"]
        norm["cue-config-normalizer.ts\nYAML parse + normalize"]
        val["cue-config-validator.ts\nvalidation rules"]
    end

    subgraph yaml_loader["cue-yaml-loader.ts (facade)"]
        loadCueConfig["loadCueConfig()"]
        watchCueYaml["watchCueYaml()"]
        validateCueConfig["validateCueConfig()"]
    end

    subgraph services["Runtime Services"]
        state["cue-session-state.ts\nstate helpers"]
        dispatch["cue-dispatch-service.ts\nfan-out / single dispatch"]
        completion["cue-completion-service.ts\nagent.completed chains"]
        query["cue-query-service.ts\nstatus / graph / settings"]
        runtime["cue-session-runtime-service.ts\nsession lifecycle"]
    end

    subgraph engine["CueEngine (facade)"]
        cueengine["CueEngine\nstart/stop/refresh\ngetStatus/getGraphData\nhasCompletionSubscribers"]
    end

    index --> yaml_loader
    index --> services
    repo --> yaml_loader
    norm --> yaml_loader
    val --> yaml_loader

    yaml_loader --> runtime
    state --> runtime
    state --> query

    dispatch --> cueengine
    completion --> cueengine
    query --> cueengine
    runtime --> cueengine

    subgraph consumers["Consumers"]
        preload["preload/cue.ts"]
        renderer["renderer/hooks/useCue.ts\nglobal.d.ts\ncue-pipeline-types.ts"]
    end

    index --> preload
    index --> renderer
    cueengine -->|IPC| preload
Loading

Comments Outside Diff (1)

  1. src/main/cue/cue-config-normalizer.ts, line 50-56 (link)

    P2 normalizeFilter silently drops the entire filter on the first invalid value

    If any single filter key has a non-primitive value (e.g. an array), the whole filter object is discarded and undefined is returned. In contrast, the validator (cue-config-validator.ts) reports a per-key error and keeps validating other keys. A user whose config passes validation would still see a silently-dropped filter at load time.

    Consider emitting a warning for the dropped key (consistent with initSession's prompt-file warning style) or adopting the same defensive approach as the validator — skip the bad key rather than discarding the whole object.

Reviews (1): Last reviewed commit: "refactor(cue): extract runtime orchestra..." | Re-trigger Greptile

Comment on lines +114 to +119
getSettings(): CueSettings {
for (const [, state] of deps.getSessionStates()) {
return { ...state.config.settings };
}
return { ...DEFAULT_CUE_SETTINGS };
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 getSettings() silently returns the first session's config

When multiple sessions have active Cue configs with different settings blocks, only the first entry in the Map is returned. The method has no documentation about this tie-breaking policy, so callers can't reason about what they'll get.

Consider adding a comment that documents this intentional behaviour (e.g., "returns the first active session's settings as a global defaults view") so future readers don't assume it merges or is deterministic across sessions.

Suggested change
getSettings(): CueSettings {
for (const [, state] of deps.getSessionStates()) {
return { ...state.config.settings };
}
return { ...DEFAULT_CUE_SETTINGS };
},
getSettings(): CueSettings {
// Returns settings from the first active session as a representative global view.
// If sessions have different settings, there is no merge — the first wins.
for (const [, state] of deps.getSessionStates()) {
return { ...state.config.settings };
}
return { ...DEFAULT_CUE_SETTINGS };
},

Comment on lines +62 to +64
const perTargetPrompt = sub.fan_out_prompts?.[i];
const prompt = perTargetPrompt || sub.prompt_file || sub.prompt;
deps.executeRun(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Inconsistent nullish-check operators between fan-out and non-fan-out paths

The fan-out path uses || (falsy check) while the non-fan-out path on line 79 uses ?? (nullish check). For this data shape both behave identically (prompt_file is either a non-empty string or undefined), but the inconsistency is a readability trap for future edits.

Suggested change
const perTargetPrompt = sub.fan_out_prompts?.[i];
const prompt = perTargetPrompt || sub.prompt_file || sub.prompt;
deps.executeRun(
const perTargetPrompt = sub.fan_out_prompts?.[i];
const prompt = perTargetPrompt ?? sub.prompt_file ?? sub.prompt;

Comment on lines +189 to +196
for (const sub of state.config.subscriptions) {
for (const key of deps.scheduledFiredKeys) {
if (key.startsWith(`${sessionId}:${sub.name}:`)) {
deps.scheduledFiredKeys.delete(key);
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 O(n × m) key cleanup on every teardown

The outer loop iterates over each subscription, and for each subscription the full scheduledFiredKeys Set is re-scanned. The Set can be iterated once per teardown instead of once per subscription:

const prefix = `${sessionId}:`;
for (const key of deps.scheduledFiredKeys) {
    if (key.startsWith(prefix)) {
        deps.scheduledFiredKeys.delete(key);
    }
}

This also cleans up orphaned keys from subscriptions renamed between hot-reloads (the per-subscription prefix check misses those).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/main/cue/cue-session-runtime-service.ts (1)

265-267: Consider returning a shallow copy of the sessions Map.

getSessionStates() returns the internal sessions Map directly, allowing callers to mutate internal state. This could lead to subtle bugs if a consumer accidentally modifies it.

♻️ Defensive copy suggestion
 getSessionStates(): Map<string, SessionState> {
-	return sessions;
+	return new Map(sessions);
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/cue-session-runtime-service.ts` around lines 265 - 267,
getSessionStates currently returns the internal sessions Map directly, allowing
external code to mutate internal state; change getSessionStates to return a
shallow copy (e.g., construct and return a new Map from the existing sessions)
so callers receive a snapshot while leaving the internal sessions Map
unmodified; ensure the method signature still returns Map<string, SessionState>
and references the same sessions variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/cue/config/cue-config-validator.ts`:
- Around line 132-147: The current checks for sub.poll_minutes only test typeof
and < 1, which allow non-finite numbers like NaN and Infinity; update the
validation in the validator that handles sub.poll_minutes (the branch
referencing sub.poll_minutes, event, and pushing to errors) to also require
Number.isFinite(sub.poll_minutes) (or equivalent) so non-finite values are
rejected and an error is pushed to errors (same message as existing) for both
the task.pending and github.pull_request/github.issue branches; ensure you apply
this change where sub.poll_minutes is validated so broken intervals are not
accepted.
- Around line 116-123: In the agent.completed validation branch inside
cue-config-validator.ts (the block checking event === 'agent.completed'),
tighten the checks on sub.source_session so that a string value must be
non-empty and an array value must be non-empty and contain only non-empty
strings; update the logic around the existing checks (the conditional that
currently does Array.isArray(sub.source_session)) to push errors for
source_session: [] and for any array element that is not a string or is an empty
string, and also push an error when sub.source_session is a string but is empty,
using the same prefix/error message pattern as the surrounding validator code.

In `@src/main/cue/cue-dispatch-service.ts`:
- Around line 62-63: The prompt resolution is inverted and may send a file path
because materializeCueConfig puts the resolved file contents into sub.prompt
while sub.prompt_file remains the path; update occurrences that pick a prompt so
they prefer sub.prompt over sub.prompt_file: change the per-target selection
(currently perTargetPrompt || sub.prompt_file || sub.prompt) to perTargetPrompt
|| sub.prompt || sub.prompt_file, and change the other fallback (currently
sub.prompt_file ?? sub.prompt) to sub.prompt ?? sub.prompt_file; locate these in
cue-dispatch-service (look for perTargetPrompt, sub.fan_out_prompts,
sub.prompt_file, sub.prompt) and make the two swaps so the resolved content is
used first.

---

Nitpick comments:
In `@src/main/cue/cue-session-runtime-service.ts`:
- Around line 265-267: getSessionStates currently returns the internal sessions
Map directly, allowing external code to mutate internal state; change
getSessionStates to return a shallow copy (e.g., construct and return a new Map
from the existing sessions) so callers receive a snapshot while leaving the
internal sessions Map unmodified; ensure the method signature still returns
Map<string, SessionState> and references the same sessions variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84d0e030-6ae5-4391-b174-4ae87ad81fdf

📥 Commits

Reviewing files that changed from the base of the PR and between b07b378 and 1fdd50e.

📒 Files selected for processing (17)
  • src/main/cue/config/cue-config-normalizer.ts
  • src/main/cue/config/cue-config-repository.ts
  • src/main/cue/config/cue-config-validator.ts
  • src/main/cue/cue-completion-service.ts
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/cue-engine.ts
  • src/main/cue/cue-query-service.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/cue-session-state.ts
  • src/main/cue/cue-types.ts
  • src/main/cue/cue-yaml-loader.ts
  • src/main/preload/cue.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/useCue.ts
  • src/shared/cue-pipeline-types.ts
  • src/shared/cue/contracts.ts
  • src/shared/cue/index.ts

@reachrazamair
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

✅ Actions performed

Review triggered.

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

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

Inline comments:
In `@src/main/cue/config/cue-config-validator.ts`:
- Around line 224-226: The validator currently treats arrays as valid for
cfg.settings because typeof [] === 'object'; update the validation in the
cue-config-validator (the block checking cfg.settings) to explicitly reject
arrays by adding an Array.isArray check (i.e., ensure cfg.settings is an object,
not null, and not an array) and push the same '"settings" must be an object'
error when an array is provided.

In `@src/main/cue/cue-dispatch-service.ts`:
- Around line 62-63: The code uses logical OR when resolving the final prompt
which treats an empty string as missing; update the resolution in
cue-dispatch-service.ts to use nullish coalescing so empty prompt content is
preserved — specifically change the expression that sets prompt (currently using
perTargetPrompt || sub.prompt || sub.prompt_file || '') to use ?? between
perTargetPrompt, sub.prompt, and sub.prompt_file (so perTargetPrompt ??
sub.prompt ?? sub.prompt_file ?? '') thus keeping empty strings from sub.prompt
or a resolved prompt file rather than falling back to the file path.

In `@src/main/cue/cue-session-runtime-service.ts`:
- Around line 75-79: initSession currently returns early when
loadCueConfig(session.projectRoot) is falsy, which prevents watchCueYaml and the
fallback watcher from being registered for sessions that start without cue.yaml;
change initSession so it does not return when config is missing and instead
always invoke the watcher registration logic (call watchCueYaml with the
session/projectRoot or register the fallback watcher path) so the code will
detect a newly created cue.yaml; keep using loadCueConfig to set up
watchers/config when present but ensure the fallback watcher path (the existing
watcher-registration code that runs after a prior config) is executed
unconditionally so sessions that boot without config still get hot-reload
discovery.
- Around line 160-162: The code currently calls deps.onPreventSleep only when
hasTimeBasedSubscriptions(config, session.id) is true but always calls
deps.onAllowSleep later, which can release a sleep block that was never
acquired; change the teardown logic so that deps.onAllowSleep is invoked only if
the corresponding onPreventSleep was previously called for this session (e.g.,
compute a local boolean like preventedSleep = hasTimeBasedSubscriptions(config,
session.id) before calling deps.onPreventSleep and then wrap the subsequent
deps.onAllowSleep(`cue:schedule:${session.id}`) in a conditional that checks
that same boolean), or alternatively track acquisition per-session and match
onAllowSleep to onPreventSleep using the same key `cue:schedule:${session.id}`
so you only release what you acquired.
- Around line 110-115: SubscriptionSetupDeps currently omits
dispatchSubscription causing heartbeat/scheduled/file/task/GitHub subscription
setups to call executeCueRun directly and bypass fan-out logic; add
dispatchSubscription to the SubscriptionSetupDeps type, pass the existing
dispatchSubscription implementation into setupDeps, and update the subscription
setup helpers for heartbeat/scheduled/file/task/GitHub to invoke
dispatchSubscription (which checks sub.fan_out and fan_out_prompts) when a
subscription has fan_out set, falling back to executeCueRun only when no fan-out
is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f149792-b417-4997-a470-e547126745c9

📥 Commits

Reviewing files that changed from the base of the PR and between 1fdd50e and 819c9bf.

📒 Files selected for processing (3)
  • src/main/cue/config/cue-config-validator.ts
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/cue-session-runtime-service.ts

Comment on lines +75 to +79
function initSession(session: SessionInfo): void {
if (!deps.enabled()) return;

const config = loadCueConfig(session.projectRoot);
if (!config) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sessions that start without cue.yaml never get a hot-reload watcher.

Returning at Line 79 skips watchCueYaml(), and the fallback watcher path in Lines 231-236 only runs after a session previously had config. A session that boots without Cue config will not auto-discover a newly created cue.yaml until some external full refresh happens.

💡 Suggested change
 	function initSession(session: SessionInfo): void {
 		if (!deps.enabled()) return;
 
+		const pendingWatcher = pendingYamlWatchers.get(session.id);
+		if (pendingWatcher) {
+			pendingWatcher();
+			pendingYamlWatchers.delete(session.id);
+		}
+
 		const config = loadCueConfig(session.projectRoot);
-		if (!config) return;
+		if (!config) {
+			const yamlWatcher = watchCueYaml(session.projectRoot, () => {
+				deps.onRefreshRequested(session.id, session.projectRoot);
+			});
+			pendingYamlWatchers.set(session.id, yamlWatcher);
+			return;
+		}

Also applies to: 231-236

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

In `@src/main/cue/cue-session-runtime-service.ts` around lines 75 - 79,
initSession currently returns early when loadCueConfig(session.projectRoot) is
falsy, which prevents watchCueYaml and the fallback watcher from being
registered for sessions that start without cue.yaml; change initSession so it
does not return when config is missing and instead always invoke the watcher
registration logic (call watchCueYaml with the session/projectRoot or register
the fallback watcher path) so the code will detect a newly created cue.yaml;
keep using loadCueConfig to set up watchers/config when present but ensure the
fallback watcher path (the existing watcher-registration code that runs after a
prior config) is executed unconditionally so sessions that boot without config
still get hot-reload discovery.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@src/main/cue/config/cue-config-validator.ts`:
- Around line 55-58: The validator currently only requires at least one of
sub.prompt or sub.prompt_file to be a string, allowing an invalid sibling to
slip through; update the logic in the function that sets hasPrompt/hasPromptFile
(the validation block using variables hasPrompt, hasPromptFile, sub.prompt,
sub.prompt_file and errors.push with prefix) to also error when a sibling field
exists but is the wrong type: if sub.prompt is present and typeof sub.prompt !==
'string' push a descriptive error, and if sub.prompt_file is present and typeof
sub.prompt_file !== 'string' push a descriptive error; keep the existing check
that at least one of them must be provided and only treat a field as valid when
it is a string.

In `@src/main/cue/cue-session-runtime-service.ts`:
- Around line 175-178: The log is counting all config.subscriptions instead of
only those wired to this session; replace the filter logic used in the
deps.onLog calls (around the initialization and the later block at lines
~233-240) with the same predicate used by countActiveSubscriptions(...) in
cue-session-state.ts so the log and activeCount reflect subscriptions actually
targeted to this session (use countActiveSubscriptions(session, config) or
replicate its predicate when computing the number to log), and update both
occurrences to use that function/predicate.

In `@src/main/cue/cue-subscription-setup.ts`:
- Around line 85-97: The non-fan-out direct-execute path in
dispatchOrExecuteSubscription is using sub.prompt_file ?? sub.prompt, which
flips the intended precedence and passes file paths into executeCueRun instead
of hydrated prompt contents; change the argument order to sub.prompt ??
sub.prompt_file so executeCueRun receives the hydrated prompt when present
(mirroring the precedence used by cue-dispatch-service.ts), leaving
dispatchSubscription behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d339739a-e53b-46a0-8b61-ed09a3dc0a68

📥 Commits

Reviewing files that changed from the base of the PR and between 819c9bf and db6a7be.

📒 Files selected for processing (5)
  • src/main/cue/config/cue-config-validator.ts
  • src/main/cue/cue-dispatch-service.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/cue-session-state.ts
  • src/main/cue/cue-subscription-setup.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/cue/cue-dispatch-service.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/main/cue/cue-session-runtime-service.ts (1)

305-308: Consider renaming internal function to clarify intent.

The public removeSession method correctly delegates to the internal removeSession function (scope resolution finds the outer function at line 263), but the identical naming could confuse future maintainers reading this code.

♻️ Rename internal function for clarity
-	function removeSession(sessionId: string): void {
+	function removeSessionInternal(sessionId: string): void {
 		teardownSession(sessionId);
 		sessions.delete(sessionId);
 		deps.clearQueue(sessionId);
@@
 		removeSession(sessionId: string): void {
-			removeSession(sessionId);
+			removeSessionInternal(sessionId);
 			deps.onLog('cue', `[CUE] Session removed: ${sessionId}`);
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/cue-session-runtime-service.ts` around lines 305 - 308, The
public method removeSession currently calls an identically named inner function
removeSession (shadowing the outer method) which can be confusing; rename the
internal function (the one declared around the earlier scope, e.g., the function
currently referenced by removeSession at line ~263) to a clearer name such as
removeSessionInternal or destroySession, update this public method to call that
new name, and update any other call sites that reference the old internal
function name to the new identifier to avoid breaking references.
src/main/cue/config/cue-config-validator.ts (2)

197-207: The app.startup branch is dead code.

Since 'app.startup' is included in CUE_EVENT_TYPES (per src/shared/cue/contracts.ts:34), the unknown-event check at lines 199-207 would never fire for it anyway. The explicit branch is harmless but redundant.

♻️ Simplify by removing the no-op branch
-			} else if (event === 'app.startup') {
-				// No additional required fields for the startup trigger.
-			} else if (
+			} else if (
 				sub.event &&
 				typeof sub.event === 'string' &&
 				!CUE_EVENT_TYPES.includes(event as any)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/config/cue-config-validator.ts` around lines 197 - 207, Remove
the redundant no-op branch that checks for event === 'app.startup' in the
validation chain: delete the else if (event === 'app.startup') { /* No
additional required fields for the startup trigger. */ } block so the subsequent
validation that uses sub.event, typeof sub.event, and
CUE_EVENT_TYPES.includes(event) runs directly; ensure the remaining conditions
referencing event, sub.event, CUE_EVENT_TYPES, prefix and errors remain intact
and the if/else chain still compiles.

62-66: Empty string prompt / prompt_file values are accepted.

The type validation at lines 55-60 correctly rejects non-string types, but prompt: "" and prompt_file: "" will satisfy hasPrompt/hasPromptFile and pass validation despite being unusable.

🔧 Proposed tightening
-			const hasPrompt = typeof sub.prompt === 'string';
-			const hasPromptFile = typeof sub.prompt_file === 'string';
+			const hasPrompt = typeof sub.prompt === 'string' && sub.prompt.trim().length > 0;
+			const hasPromptFile = typeof sub.prompt_file === 'string' && sub.prompt_file.trim().length > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/cue/config/cue-config-validator.ts` around lines 62 - 66, The
validator currently treats empty strings as valid because
hasPrompt/hasPromptFile only check typeof string; change those checks in
cue-config-validator (the hasPrompt and hasPromptFile checks) to require
non-empty trimmed strings (e.g., typeof sub.prompt === 'string' &&
sub.prompt.trim().length > 0 and similarly for sub.prompt_file), then rely on
the existing branch that calls errors.push(`${prefix}: "prompt" or "prompt_file"
is required`) so empty values are rejected as unusable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/cue/config/cue-config-validator.ts`:
- Around line 197-207: Remove the redundant no-op branch that checks for event
=== 'app.startup' in the validation chain: delete the else if (event ===
'app.startup') { /* No additional required fields for the startup trigger. */ }
block so the subsequent validation that uses sub.event, typeof sub.event, and
CUE_EVENT_TYPES.includes(event) runs directly; ensure the remaining conditions
referencing event, sub.event, CUE_EVENT_TYPES, prefix and errors remain intact
and the if/else chain still compiles.
- Around line 62-66: The validator currently treats empty strings as valid
because hasPrompt/hasPromptFile only check typeof string; change those checks in
cue-config-validator (the hasPrompt and hasPromptFile checks) to require
non-empty trimmed strings (e.g., typeof sub.prompt === 'string' &&
sub.prompt.trim().length > 0 and similarly for sub.prompt_file), then rely on
the existing branch that calls errors.push(`${prefix}: "prompt" or "prompt_file"
is required`) so empty values are rejected as unusable.

In `@src/main/cue/cue-session-runtime-service.ts`:
- Around line 305-308: The public method removeSession currently calls an
identically named inner function removeSession (shadowing the outer method)
which can be confusing; rename the internal function (the one declared around
the earlier scope, e.g., the function currently referenced by removeSession at
line ~263) to a clearer name such as removeSessionInternal or destroySession,
update this public method to call that new name, and update any other call sites
that reference the old internal function name to the new identifier to avoid
breaking references.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4434cee7-26c5-4b98-bd39-5e6855bc99b3

📥 Commits

Reviewing files that changed from the base of the PR and between db6a7be and 7bf38fe.

📒 Files selected for processing (4)
  • src/__tests__/main/cue/cue-engine.test.ts
  • src/main/cue/config/cue-config-validator.ts
  • src/main/cue/cue-session-runtime-service.ts
  • src/main/cue/cue-subscription-setup.ts
✅ Files skipped from review due to trivial changes (1)
  • src/tests/main/cue/cue-engine.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/cue/cue-subscription-setup.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant