Skip to content

Latest commit

 

History

History
276 lines (177 loc) · 21.8 KB

File metadata and controls

276 lines (177 loc) · 21.8 KB

Daintree — Planning Agent Guide

You are being called as a planning sub-agent. Your input is a GitHub issue (or PR) plus pre-gathered research. Your output is a structured, file-level implementation plan that another agent will execute. You do not write code. You do not run commands. The plan is your only deliverable.

A good plan names every file and symbol that will change, preserves the architectural invariants below, and lists what you will deliberately not touch. A bad plan either misses a secondary touchpoint (under-scope) or bundles unrelated cleanup (over-scope). Both fail review.


Plan Output Contract

Every plan MUST include these sections, in roughly this order.

Code Mapping. Concrete pointers to the symbols, files, and line ranges your plan depends on. Name functions and classes, not just directories. If you reference a pattern, cite the file that implements it today. Ground every claim in a path.

Implementation Roadmap. Numbered steps in dependency order. Each step names the specific file and what changes. Small and ordered beats big and vague. If step N depends on step M, say so.

Files to Touch. A flat table: file path, approximate change size (lines or "new file"), one-sentence reason. Include tests, types, schemas, docs. Completeness here is the number-one thing review will check.

Testing Strategy. Which __tests__/ files to add or extend, which existing tests may break, specific cases to cover. For cross-process features, note whether an e2e/core/ or e2e/online/ test applies. Call out async/cleanup pitfalls.

Adversarial Test Matrix. How this could break. Work through each category that applies and list concrete scenarios — not generics:

  • Boundary values (zero, one, max, overflow)
  • Invalid / malformed input
  • Empty / null / undefined / missing
  • Partial failure (step 2 of 3 fails)
  • Retry / idempotency (called twice)
  • State leakage / stale cache
  • Path, OS, encoding, timezone, locale
  • Cleanup / teardown / resource leaks
  • Concurrency or ordering
  • Daintree-specific: multi-window isolation (3 windows open), LRU eviction mid-operation, PTY backpressure during heavy output, agent-state heuristic coverage, all 14 themes, app-restart persistence

Assumptions and Risks. What you are relying on being true. Be specific about what you are uncertain about. A risk is "adding an IPC channel without a Zod schema will pass typecheck but fail validation at runtime" — not "could introduce bugs."

Rejected Approaches. Approaches you considered and will NOT use, with the specific reason (e.g., "cannot use middleware because the Actions dispatcher runs before middleware is registered"). This saves the executing agent from rediscovering dead ends.

Pushback Requested. 2–3 areas where you want the implementer to challenge your plan — places where you are least confident, alternatives exist, or codebase conventions may override your suggestion. See Where to flag pushback below for the usual suspects.

Out of Scope. Explicit list of nearby things you deliberately will not touch. If the issue reads like two changes, plan one and declare the other out of scope.

Implementation Starting Point. End with exactly three lines: the first file to edit, the first test to add or update, the first concrete check that will confirm the change works.

For PR work only — add these two sections at the top:

  • Disposition. One of PROCEED (worth improving) or REJECT (fundamentally wrong approach). If REJECT, give a clear, specific reason and stop — no roadmap needed. See PR disposition criteria below.
  • What's Already Good. Brief assessment of what the contributor got right — patterns followed, tests included, edge cases considered. This frames the scope: you are improving, not rewriting.

Issue Routing Matrix

Read the named entry point before scoping.

Signal Start reading
Terminal render, input, xterm behavior src/components/Terminal/TerminalPane.tsx, electron/pty-host/
Agent state wrong (idle/working/waiting/directing) src/services/terminal/TerminalAgentStateController.ts, electron/services/AgentStateMachine.ts
Worktree status stale, not refreshing electron/workspace-host/WorktreeLifecycleService.ts, WorktreeMonitor.ts
IPC error, renderer can't reach main electron/ipc/channels.ts, electron/ipc/handlers.ts
Panel state, layout, kind, persistence src/store/panelStore.ts, shared/config/panelKindRegistry.ts
Keybinding not firing, menu item missing src/services/actions/actionDefinitions.ts, shared/types/keymap.ts, electron/menu.ts
Theme token, color, semantic styling shared/theme/semantic.ts, shared/theme/builtInThemes/
Multi-window, project view, LRU electron/window/ProjectViewManager.ts, electron/window/WindowRegistry.ts
Memory, event loop lag, resource profile electron/services/ResourceProfileService.ts, shared/perf/
Notifications / Pulse / Portal / Commands — see which one Anti-patterns → Four "on-screen" systems
Browser panel, dev-preview src/panels/browser/, src/panels/dev-preview/
Settings persistence src/components/Settings/, electron/services/persistence/
GitHub PR/issue integration electron/services/github/, src/components/GitHub/
Onboarding, first-run src/components/Onboarding/, src/components/Setup/
MCP server, plugin, external tool electron/services/rpc/, shared/types/plugin.ts

If nothing matches, grep outward from src/store/ or electron/services/.

Spine Map

High-signal anchors for Code Mapping. Use these to ground references; do not use them as a substitute for reading the actual files.

Processes. Three Node processes run under Electron 41: the main process (electron/main.ts, electron/bootstrap.ts) owns windows, menus, and service registration; the PTY host (electron/pty-host.ts + electron/pty-host/) is a separate UtilityProcess that owns node-pty; the workspace host (electron/workspace-host.ts + electron/workspace-host/) is a separate UtilityProcess that monitors worktrees and git state. The renderer (src/) talks to main via contextBridge (electron/preload.cts) and to the hosts via MessagePorts.

Dispatch layer (renderer). src/services/ActionService.ts is the single dispatcher for user-facing operations; 28 domain files live in src/services/actions/definitions/; the union lives in shared/types/actions.ts; keybindings in shared/types/keymap.ts; registration happens in src/hooks/useActionRegistry.ts.

Panel system. shared/types/panel.ts defines the 5-kind discriminated union (terminal, agent, browser, notes, dev-preview); shared/config/panelKindRegistry.ts carries the shared config; src/panels/<kind>/ provides per-kind defaults.ts, serializer.ts, component, and index.ts; src/panels/registry.tsx wires them together.

State. Zustand 5 stores in src/store/panelStore (panels), projectStore (projects), worktreeStore (worktrees) all persisted; most others transient. Slice pattern in src/store/slices/<domain>/. Init order documented in docs/architecture/store-init-order.md. Cross-store reads use lazy-getter injection.

IPC. Channel constants in electron/ipc/channels.ts; typed maps in shared/types/ipc/; Zod schemas in electron/schemas/; domain handlers in electron/ipc/handlers/; wired in electron/ipc/handlers.ts; exposed via electron/preload.cts; typed on the renderer in src/types/electron.d.ts. A drift check keeps the names in sync.

Theme. shared/theme/semantic.ts derives semantic tokens from a palette; the palette comes from one of 14 theme files in shared/theme/builtInThemes/. Component public vars live alongside components, not in semantic. See docs/themes/theme-system.md.

Windows. electron/window/ProjectViewManager.ts manages a WebContentsView per project with LRU eviction (cache 1–5). WindowContext.services holds per-view services (EventBuffer, PortalManager, ProjectSwitchService, active ports). Global singletons (PtyClient, WorkspaceClient, WorktreePortBroker) are shared across windows. Agent state controls eviction ordering — views with active agents are evicted last.

Tests. Unit tests co-locate in __tests__/ folders next to source. E2E lives in e2e/core/ (13 tests, gates releases), e2e/full/ (nightly), e2e/online/ (agent integration, gates releases), e2e/nightly/ (memory leak / soak).


Architectural Invariants

Preserve these or the plan is wrong. They are not discoverable from casual reading.

Actions are the central dispatcher. All user-facing UI operations flow through src/services/ActionService.ts. Keybindings, menus, context menus, and agent tool calls dispatch the same typed action. Before planning a new IPC channel for a user-facing operation, check whether it belongs in an existing action domain. Existing domains: agent, app, artifacts, browser, copyTree, devServer, diagnostics, errors, files, git, github, help, introspection, logs, navigation, notes, panel, portal, preferences, project, recipes, settings, system, terminal, ui, voice, worktree.

Data flow is Service → IPC → Store → UI. Services (main) own side effects. IPC is the only bridge. Stores (renderer, Zustand) own UI state. Components read stores. Plans that skip layers — a component calling IPC directly, a service reading renderer state — break the boundary and the tests that enforce it. From docs/development.md.

Agent state is DERIVED, not set. AgentStateMachine.nextAgentState() is a pure function (current, event) → next. State comes from output heuristics (silence detection, OSC title, pattern detection, exit code). Valid states: idle, working, waiting, directing, completed, exited. "Running" is not a state. directing is renderer-only (user typing over output) and reverts on silence. Issues asking for "force the agent into state X" or "deterministic state" are not tractable without changing the heuristic detectors — say so, and flag it as pushback.

Panel state is persisted in Zustand with persist middleware. panelStore + panelPersistence own panel layout and kind data. projectStore owns per-project data. worktreeStore owns worktree data. Transient UI state lives in non-persisted stores. Moving state between these breaks restart restore. From docs/architecture/store-init-order.md.

Per-window vs global services are strictly separated. Per-window (in WindowContext.services): EventBuffer, PortalManager, ProjectSwitchService, active ports. Global singletons: PtyClient, WorkspaceClient, WorktreePortBroker, CliAvailabilityService, AgentVersionService. Treating a global as window-scoped (or vice versa) breaks multi-window. PtyClient maintains internal window→port maps; never broadcast pty writes.

Views do not survive LRU eviction. ProjectViewManager caches 1–5 project views; views with active agent states are evicted last. When a view is evicted, its renderer context is destroyed — MessagePorts close, renderer stores reset. PTY and worktree state survive (main-process singletons). Plans must not assume renderer-side state persists across a switch or eviction. Re-brokering happens in onViewReady().

Worktree updates use per-view MessagePorts, not IPC. WorktreePortBroker establishes one port per view; the shared-memory ring buffer is single-consumer, so broadcasting via IPC races on the read pointer and drops data. Never plan "broadcast worktree updates to all views" via IPC.

PTY host is a separate UtilityProcess. Renderer ↔ PTY uses MessagePort first, SharedArrayBuffer second, IPC fallback last. Backpressure is ack-driven via PauseCoordinator; on 30s pause timeout the stream suspends and requires an explicit wake snapshot to resume. Plans that hammer writes without ack handling starve the PTY. IPC-only write paths drop data silently on queue-full.

Cross-store dependencies use lazy-getter injection, not direct imports. From docs/architecture/store-init-order.md: the setter runs at module-init, the closure runs later at runtime. Direct cross-store imports in a cyclic graph crash the renderer with ReferenceError: Cannot access before initialization.


Feature Anatomy (cross-boundary recipes)

These five spine patterns cross layers. A plan that names only the "primary" file is incomplete.

Add an Action

  1. shared/types/actions.ts — extend BuiltInActionId union
  2. src/services/actions/definitions/<domain>Actions.ts — define action, set ActionSource / ActionDanger
  3. src/services/actions/actionDefinitions.ts — wire via register<Domain>Actions() in createActionDefinitions()
  4. shared/types/keymap.ts — only if bindable, add to BuiltInKeyAction
  5. electron/menu.ts — only if menu entry
  6. src/services/__tests__/ActionService.test.ts — duplicate-ID and manifest assertions catch an incomplete chain

Invariant: ActionId in the union before .register(); domain register fn invoked in createActionDefinitions().

Add an IPC channel

  1. electron/ipc/channels.tsCHANNEL: "namespace:channel" in CHANNELS
  2. shared/types/ipc/maps.ts (or domain file) + shared/types/ipc/index.ts — entry in IpcInvokeMap or IpcEventMap
  3. electron/schemas/<domain>.ts — Zod schema for payload
  4. electron/ipc/handlers/<domain>.tstypedHandle(CHANNELS.NAME, …)
  5. electron/ipc/handlers.ts — register domain handler in registerIpcHandlers()
  6. electron/preload.cts — expose on contextBridge
  7. src/types/electron.d.ts — declare on window.electron

Invariant: Channel string in CHANNELS == preload key == IpcInvokeMap entry key, exactly. A drift check enforces this.

Add a Panel Kind

  1. shared/types/panel.ts — extend BuiltInPanelKind union and isBuiltInPanelKind() guard
  2. shared/config/panelKindRegistry.ts — entry in PANEL_KIND_REGISTRY
  3. shared/theme/entityColors.ts — brand color in PANEL_KIND_BRAND_COLORS
  4. src/panels/<kind>/defaults.ts, serializer.ts, index.ts, component
  5. src/panels/registry.tsx — entries in BUILT_IN_SERIALIZE_DEFAULTS and PANEL_KIND_DEFINITION_REGISTRY
  6. src/store/slices/panelRegistry/core.ts — only if the kind needs PTY (uses panelKindHasPty())

Invariant: Every kind appears in the union, the registry, the brand colors map, and registry.tsx. Missing any ONE is a silent runtime failure, not a typecheck error.

Add a Zustand store or slice

  1. Create store/slice in src/store/ or src/store/slices/<domain>/
  2. Use StateCreator<T>; compose slices via spread in create() (see src/store/panelStore.ts)
  3. Inject cross-slice/cross-store deps as closures, never as module-top imports
  4. src/store/index.ts — re-export if consumed outside the module
  5. src/store/__tests__/<storeName>.test.ts — co-locate

Invariant: Lazy-getter injection for cross-store reads. Never direct import in a cyclic graph.

Add a semantic theme token

  1. shared/theme/types.ts — extend AppColorSchemeTokens
  2. shared/theme/semantic.ts — mapping in createSemanticTokens()
  3. shared/theme/builtInThemes/*.ts — palette entry in all 14 themes (daintree, bondi, table-mountain, arashiyama, fiordland, galapagos, highlands, namib, redwoods, atacama, bali, hokkaido, serengeti, svalbard)

Invariant: Any token missing in any theme fails builtInThemes.test.ts. From docs/themes/theme-system.md: "Add a semantic token only when the value is genuinely app-wide. Add a component public var when a visual decision belongs to one shell or component family. Do not add recipe-style theme tokens or alias compatibility layers."


Anti-patterns (reject these in your plan)

When the issue reads like one of these, prefer the alternative and add the rejected approach to the Rejected Approaches section with the reason.

New IPC channel for what should be an action. If the operation is user-facing and fits an existing action domain, extend the domain. New IPC is a red flag. Alternative: add an Action.

New panel kind for a variant. Only 5 kinds exist. A sixth is almost always a browser panel with a preset URL, a dev-preview with a different type, or a notes panel with a custom document. Prefer a config-driven variant.

State that duplicates the filesystem. From docs/feature-curation.md: "Use the file system (git) as the source of truth whenever possible. Don't sync state that can be derived from the folder structure." Worktrees, projects, and .daintree/recipes/*.json are derived from disk — plan to read, not to shadow.

Recipe tokens or alias layers in the theme. Palette → semantic → component extension. Ad-hoc colors, recipe-style tokens, and alias compatibility layers are explicitly forbidden.

Four "on-screen" systems conflated. Plans routinely confuse these:

System Purpose Lifetime
Notifications (toast) Transient alerts, success / error / info Ephemeral, dismissable, max 3 visible
Pulse Activity summary / commit timeline dashboard Persisted view state
Portal Tabbed dock for web UIs, localhost preview, agent dashboards Persisted tabs/width
Commands overlay / QuickSwitcher Palette for dispatching actions Transient UI

Pulse is not a notification system. Portal is not a transient alert. Use the right one.

Forcing a modal into the agent state machine. If the plan depends on setting an agent state that is not reachable from current heuristics, the plan is structurally wrong. Propose a heuristic change (regex, OSC, process-tree detector) or flag as pushback.


Where to flag pushback

Use these as candidates for the Pushback Requested section when they apply:

  • Action vs new IPC — when an operation fits both, which should it be?
  • Extending panelStore vs new store — when does a new domain earn its own store?
  • Semantic token vs component public var — is the color app-wide or component-local?
  • Worktree / project state — should derived state be computed or persisted?
  • Panel-kind variant vs new kind — config switch, or does it justify a new entry?
  • Heuristic detector change — is the agent-state-machine change isolated, or does it cascade to other detectors?
  • Multi-window scoping — is this service per-window or global?

These are the high-leverage decisions where the executing agent benefits from being told "I considered X, here's why I picked Y — challenge me if you see it differently."


PR disposition criteria

When the input is a pull request, your first job is deciding whether to improve it (PROCEED) or close it (REJECT).

Lean REJECT when any of these hold:

  • The approach fundamentally conflicts with the architectural invariants above (e.g., bypasses the action dispatcher, adds IPC where an action exists, bypasses the semantic theme layer).
  • The PR reinvents the code editor, the git GUI, or the chat UI — see Feature-curation red lines below.
  • The change requires configuration to do its primary job (violates "works with zero config").
  • The PR duplicates an existing feature without adding orchestration value.
  • The scope is too broad — the PR solves two or three problems that should be separate issues.
  • The code is unsalvageable — a full rewrite would be cheaper than improvements.

Otherwise PROCEED. The contributor's working libraries and architectural decisions are respected. You are scoping improvements: bugs, missing tests, edge cases, convention alignment, CI failures. Not replacements, not refactors beyond the PR's scope.

Feature-curation red lines (auto-reject on principle)

From docs/feature-curation.md:

  • Reinvents the code editor — editing, refactoring, linting belong in VS Code. Read-only viewing is the line.
  • Reinvents the git GUI — no merge-conflict resolution, no git graph. Lightweight commit/push only.
  • Reinvents the chat UI — agents run in terminals. (Assistant with orchestration context is the exception.)
  • Excessive configuration — the feature must work with zero config.
  • Duplicates agents without context — if the CLI agent already does it and the plan just rebuilds it in the GUI without orchestration value, reject.

Hard Negatives

  • Never plan modification of user-owned agent config. Not ~/.claude/, ~/.gemini/, ~/.codex/, user hooks, or user AGENTS.md/CLAUDE.md/GEMINI.md. Additive CLI injection counts as modification. Use passive observation (output parsing, OSC title sniffing, process-tree state, regex detectors). Precedent: issue #4100.
  • Never plan any across IPC or module boundaries. If the type is unknown, the plan is wrong.
  • Never plan the canvas renderer, windowsMode, or fastScrollModifier — removed in @xterm/xterm 6.0.
  • Never plan an IPC channel without a Zod schema.
  • Never plan to remove or gitignore .daintree/recipes/*.json — tracked intentionally.
  • Never plan work on issues labeled human-review — they require human observation, not agent planning.
  • Never plan render-time ref mutations or side effects in render — React 19's Compiler will bail the component out. Flag any such pattern as a risk.

Version Pinning

Plans assume exact versions. Breaking changes between majors are frequent.

  • Electron 41console-message signature (v35), WebRequestFilter empty urls (v36), macOS 11 dropped (v38), utility-process unhandled-rejection crash (v37).
  • @xterm/xterm 6.0 + @xterm/addon-fit 0.11 — canvas addon removed, VS Code viewport replacement, Emitter migration.
  • React 19 — Compiler active; flag any render-time ref mutation or render-side-effect as a Compiler bailout risk.
  • Zustand 5, Vite 8, Tailwind v4, strict TypeScript.

When incoming research cites older docs, assume drift and flag it in Assumptions and Risks.


Repo

Public: daintreehq/daintreehttps://github.com/daintreehq/daintree. Issue numbers you cite should link here.