Skip to content
Agentmart
Submit a tool

CLAUDE.md · RunMaestro/Maestro

CLAUDE.md from RunMaestro/Maestro: 775 lines covering Documentation Index, Before Writing New Code - Check Existing Utilities, Constitution: If the User Can…

Template by RunMaestro

IndexedAGPL-3.0★ 3,490
Write a review

Install

Works with Claude Code. Read the repository before you give any tool access to your data.

curl -fsSL https://raw.githubusercontent.com/RunMaestro/Maestro/main/CLAUDE.md -o CLAUDE.md

Read it and adapt it to your project before use.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

The CLAUDE.md file that RunMaestro/Maestro (3,489 stars) keeps for coding agents. It has 775 lines in 14 sections: Documentation Index; Before Writing New Code - Check Existing Utilities; Constitution: If the User Can Do It, maestro-cli Can Do It; Agent Behavioral Guidelines; Standardized Vernacular; Code Style; Do Not Edit: docs/releases.md; Project Overview; Quick Commands; Architecture at a Glance; Key Files for Common Tasks; Critical Implementation Guidelines; Debugging; MCP Server. Use it as a model for your own project's instructions.

Use cases: prompt-engineering

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

What's in it

A CLAUDE.md file: project memory Claude Code loads at the start of every session.

For
Claude Code
Length
775 lines
File
CLAUDE.md

Sections

  1. Documentation Index
  2. Before Writing New Code - Check Existing Utilities
  3. Constitution: If the User Can Do It, maestro-cli Can Do It
  4. Agent Behavioral Guidelines
  5. Standardized Vernacular
  6. Code Style
  7. Do Not Edit: docs/releases.md
  8. Project Overview
  9. Quick Commands
  10. Architecture at a Glance
  11. Key Files for Common Tasks
  12. Critical Implementation Guidelines
  13. Debugging
  14. MCP Server

First lines (shown under the repo's AGPL-3.0 license)

# CLAUDE.md

Essential guidance for working with this codebase. For detailed architecture, see [ARCHITECTURE.md](ARCHITECTURE.md). For development setup and processes, see [CONTRIBUTING.md](CONTRIBUTING.md).

## Documentation Index

This guide has been split into focused sub-documents for progressive disclosure:

| Document                                   | Description                                                                                                                                                              |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [[CLAUDE-PATTERNS.md]]                     | Core implementation patterns (process management, settings, modals, themes, Auto Run, SSH, Encore Features)                                                              |
| [[CLAUDE-IPC.md]]                          | IPC API surface (`window.maestro.*` namespaces)                                                                                                                          |
| [[CLAUDE-PERFORMANCE.md]]                  | Performance best practices (React optimization, debouncing, batching)                                                                                                    |
| [[CLAUDE-WIZARD.md]]                       | Onboarding Wizard, Inline Wizard, and Tour System                                                                                                                        |
| [[CLAUDE-FEATURES.md]]                     | Usage Dashboard and Document Graph features                                                                                                                              |
| [[CLAUDE-AGENTS.md]]                       | Supported agents and capabilities                                                                                                                                        |
| [[CLAUDE-SESSION.md]]                      | Session interface (agent data model) and code conventions                                                                                                                |
| [[CLAUDE-PLATFORM.md]]                     | Cross-platform concerns (Windows, Linux, macOS, SSH remote)                                                                                                              |
| [[CLAUDE-CUE.md]]                          | Cue automation engine: architecture, dispatch flow, lifecycle, gotchas (read before editing `src/main/cue/`)                                                             |
| [[CLAUDE-PLUGINS.md]]                      | Plugin system architecture: tiers, sandbox, broker, capabilities, contributions, signing, trust model (read before editing `src/main/plugins/` or `src/shared/plugins/`) |
| [[CLAUDE-SETTINGS.md]]                     | Settings modal style guide: typography, color, dimming, spacing, primitives, registration checklist (read before editing `src/renderer/components/Settings/`)            |
| [PROVIDER-SUPPORT.md](PROVIDER-SUPPORT.md) | Detailed agent integration guide                                                                                                                                         |

---

## Before Writing New Code - Check Existing Utilities

**MANDATORY:** Before creating any new utility function, helper, hook, component, type, or constant, check the guide docs in `docs/agent-guides/` to see if it already exists. Duplicated code is the #1 source of maintenance burden in this codebase - there are already grep-verified instances of 20+ duplicate format helpers, 60+ ad-hoc mock factories, and 500+ manual modal-layer registrations. Don't add to the pile.

| Before creating...                                 | Check this guide first                                           |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| Utility function (formatting, IDs, paths, strings) | [SHARED-UTILS.md](docs/agent-guides/SHARED-UTILS.md)             |
| IPC handler or preload bridge                      | [IPC-PATTERNS.md](docs/agent-guides/IPC-PATTERNS.md)             |
| Store action, selector, or hook                    | [STATE-PATTERNS.md](docs/agent-guides/STATE-PATTERNS.md)         |
| Agent parser, storage, or error pattern            | [AGENT-INFRA.md](docs/agent-guides/AGENT-INFRA.md)               |
| UI component, modal, or theme usage                | [UI-PATTERNS.md](docs/agent-guides/UI-PATTERNS.md)               |
| Stat card, chart, sparkline, or input control      | [WIDGET-LIBRARY.md](docs/agent-guides/WIDGET-LIBRARY.md)         |
| Test mock, factory, or setup pattern               | [TEST-PATTERNS.md](docs/agent-guides/TEST-PATTERNS.md)           |
| Renderer service or constant                       | [RENDERER-SERVICES.md](docs/agent-guides/RENDERER-SERVICES.md)   |
| Process spawning or listener                       | [PROCESS-SYSTEM.md](docs/agent-guides/PROCESS-SYSTEM.md)         |
| Browser (web-desktop) UI, PWA, or touch hook       | [WEB-MOBILE.md](docs/agent-guides/WEB-MOBILE.md)                 |
| CLI command or playbook feature                    | [CLI-PLAYBOOKS.md](docs/agent-guides/CLI-PLAYBOOKS.md)           |
| CLI verb mirroring a point-and-click action        | [CLI-UI-PARITY.md](docs/agent-guides/CLI-UI-PARITY.md)           |
| Group chat or Symphony feature                     | [GROUP-CHAT.md](docs/agent-guides/GROUP-CHAT.md)                 |
| Stats, analytics, or dashboard                     | [STATS-ANALYTICS.md](docs/agent-guides/STATS-ANALYTICS.md)       |
| Prompt template or SpecKit/OpenSpec                | [PROMPTS-SPECS.md](docs/agent-guides/PROMPTS-SPECS.md)           |
| Cue pipeline feature                               | [CUE-PIPELINE.md](docs/agent-guides/CUE-PIPELINE.md)             |
| App lifecycle, updater, or power mgmt              | [MAIN-LIFECYCLE.md](docs/agent-guides/MAIN-LIFECYCLE.md)         |
| Plugin, sandbox capability, or contribution        | [PLUGIN-DEVELOPMENT.md](docs/agent-guides/PLUGIN-DEVELOPMENT.md) |
| React context, renderer type, or Symphony runner   | [REMAINING-SYSTEMS.md](docs/agent-guides/REMAINING-SYSTEMS.md)   |
| Release step, tag, bump, or announcement           | [RELEASE-RUNBOOK.md](docs/agent-guides/RELEASE-RUNBOOK.md)       |
| Vulnerability report, advisory, CVE, or credit     | [SECURITY-RUNBOOK.md](docs/agent-guides/SECURITY-RUNBOOK.md)     |

### Commonly-reimplemented functions (do NOT add new copies)

Grep-verified 2026-09-04 (`npm run docs:verify` re-checks every path). This is the INDEX: name, canonical symbols, home file. The full entry for each - the failure it replaced, its invariants, and the traps that made the duplicates wrong - lives in [CANONICAL-UTILITIES.md](docs/agent-guides/CANONICAL-UTILITIES.md). If a name below matches what you are about to write, import the canonical one; read its full entry before extending or working around it.

- **ID generation:** `generateId()`, `generateUUID()` in `src/renderer/utils/ids.ts`
- **Format file size:** `formatSize()` in `src/shared/formatters.ts`
- **Truncate prose without splitting a word:** `truncateAtWordBoundary()` in `src/shared/formatters.ts`
- **Format numbers:** `formatNumber()`, `formatCount()` in `src/shared/formatters.ts`
- **Format tokens:** `formatTokens()`, `formatTokensCompact()` in `src/shared/formatters.ts`
- **Format elapsed time:** `formatElapsedTimeColon()` in `src/shared/formatters.ts`
- **Humanized durations:** everything in `src/shared/duration.ts` (re-exported from `formatters.ts`). `humanizeDuration(ms, opts)` is the one engine; `formatDurationHuman` / `Compact` / `Verbose` / `Parts` / `Decimal` / `Long` / `Words`, plus `formatActiveTime`, `formatElapsedTime`, `formatElapsedTicker` (thinking pill: `"0m 3s"`), and `formatElapsedTickerCompact` (the same ladder without the padded lead, for a counter inside a chip: `"3s"`, `"20m 4s"`), are presets over it. **Never hand-roll another `Math.floor(ms / 86400000)` ladder** - a dozen near-identical copies had already drifted apart on where the ladder stops, whether a zero segment is padded (`"2h 0m"` vs `"2h"`), and whether a countdown rounds up. Those are options (`units`, `maxUnits`, `style`, `keepZeroUnits`, `keepLeadingZero`, `adjacentUnits`, `round`, `fallback`), so pick a preset or call the engine. Unit sizes are in `DURATION_MS` - don't redeclare `const DAY = 86400000`. See [SHARED-UTILS.md → Durations](docs/agent-guides/SHARED-UTILS.md#durations-srcshareddurationts---both).
- **Format profiling spans (ms):** `formatDuration()` in `src/shared/performance-metrics.ts` (NOT formatters.ts - common mistake). Two-decimal `"123.45ms"` / `"1.23s"` for perf traces only; it caps at seconds, so a multi-day span comes out as a giant seconds float. For anything user-facing use `src/shared/duration.ts`.
- **Format relative time:** `formatRelativeTime()` in `src/shared/formatters.ts`
- **Format a calendar day:** `formatCalendarDay()` in `src/shared/formatters.ts` renders a `YYYY-MM-DD` string as `"Jul 10, 2026"`. It reads the parts out of the string and builds a LOCAL date, because `new Date('2026-07-10')` is parsed as UTC midnight and renders the day before everywhere west of Greenwich.
- **Local calendar day keys:** `toLocalYmd()`, `parseLocalYmd()`, `addLocalDays()` in `src/shared/localDay.ts`. Never key a day with `toISOString()` (UTC) or step one with `+ 86400000` (drifts across DST).
- **Format an hour of day:** `formatHourOfDay()` in `src/shared/formatters.ts` (`13` -> `"1 PM"`).
- **Format cost:** `formatCost()` in `src/shared/formatters.ts`
- **Token pricing and Codex token totals:** `resolveModelPricing()` / `calculateModelCost()` in `src/shared/modelPricing.ts`, and `CodexTokenCounts` in `src/shared/codexTokenUsage.ts`. Pricing is not Claude-only: `resolveModelPricing` matches the OpenAI families first (`gpt-5*` / any `codex` slug, split nano / mini / frontier), since a Codex slug carries no Anthropic family word and used to fall through to the Sonnet default. The two providers also disagree about what `input_tokens` MEANS - Anthropic reports cache reads beside it, OpenAI inside it - so `PricingConfig.CACHE_READ_SUBSET_OF_INPUT` rides on the tier and `calculateWithPricing` bills only the uncached remainder; without it a cache hit costs MORE than a miss. On the parsing side, Codex's `info.total_token_usage` is a running SESSION TOTAL while `turn.completed.usage` and `info.last_token_usage` are per-turn, so summing the first makes a session report tokens proportional to the SQUARE of its turn count (1.3B real input tokens were reported as 75B). Every Codex transcript scan goes through `CodexTokenCounts`; do NOT hand-roll another sum, and remember that a derivation fix like this is invisible to the mtime/size fingerprints, so it needs `TOKEN_USAGE_CACHE_VERSION`, `GLOBAL_STATS_CACHE_VERSION` **and `CODEX_SESSION_CACHE_VERSION`** (`src/main/storage/codex-session-storage.ts`) bumped or the old numbers are served forever - that third one guards a per-session cache reused while `fileMtimeMs` still covers the file, so it is the one most easily missed.
- **Claude's cost and per-model usage for one turn:** `claudeTurnCost()`, `readClaudeTurnBaseline()`, `parseCostStateRow()` in `src/shared/claudeCostTracker.ts`. A result's `modelUsage` and `total_cost_usd` are SESSION running totals on `--resume`; subtract the transcript's last `cost-state` row, never record them raw.
- **Usage, cost and message count of a Claude transcript:** `computeClaudeSessionUsage()` / `computeClaudeUsageWithSubagents()` / `readClaudeSubagentTranscripts()` in `src/shared/claudeSessionUsage.ts`, `countClaudeTranscriptMessages()` in `src/shared/claudeTranscriptMessages.ts`. One response is written as one line per content block with its `usage` repeated (dedupe by `message.id` + `requestId`), subagent calls live in `<sessionId>/subagents/*.jsonl` and not in the parent, and a line is not a message (tool results are `"type":"user"`). Never sum or count raw lines; a derivation fix also needs the stats caches upgraded in place (`rescaleArchivedEntries` in `src/main/utils/statsCache.ts`), never just bumped, or archived sessions are lost.
- **Which account dir an agent runs under:** `resolveAgentAccountDir()` in `src/shared/agentAccountDir.ts` (pure; main's store-reading wrapper of the same name is in `src/main/agents/provider-account-dirs.ts`). The session browser lists and totals the agent's OWN `CLAUDE_CONFIG_DIR` pool, since claude only resumes from the dir it was started with; do not hardcode `~/.claude`.
- **Capping stored tool output:** `compactToolOutput()` / `compactSessionToolOutputs()` in `src/shared/toolOutput.ts`. Tool results cross the renderer store, disk persistence, and the web-desktop bootstrap, so cap them at ingestion and again at both storage boundaries. Do NOT add a parser-local substring limit or send the raw value to a browser client: one 67MB one-line result made the bootstrap frame 126MB and left iOS on `Loading your agents...`.
- **Merging browser-deferred conversations:** `projectWebSession()` / `readDeferredContent()` / `mergeDeferredSessionContent()` in `src/main/stores/deferred-session-content.ts`, over `mergeDeferredItems()` / `MAX_PERSISTED_SESSION_LOGS` in `src/shared/deferredSessionContent.ts`. Browser bootstrap leaves transcripts and command history in main until selected; `sessions:setMany` and `sessions:setAll` must preserve those fields while a browser client saves other agent changes. Retry controls raise `requestWebBridgeReconcile()` (`src/renderer/services/webBridgeReconcile.ts`), never a hand-rolled dispatch.
- **Timestamp for a generated file name:** `fileTimestampSlug(dateOrTimestamp?)` in `src/shared/formatters.ts` - `20260713-142530`, filesystem-safe everywhere and chronologically sortable. LOCAL time on purpose: the name is read by the human who pasted the image at that wall-clock moment, so a UTC stamp reads as the wrong hour to everyone outside UTC. Both the renderer's `saveImageToProject()` and the CLI's `image save` name files with it, so a screenshot saved by hand and the same one saved by an agent cannot come out under two different conventions.
- **Path utilities:** `truncatePath()`, `getParentDir()`, `truncateCommand()`, `isAbsolutePath()`, `getBasename()`, `joinPath()` in `src/shared/formatters.ts` (`joinPath` is the renderer-safe `path.join`: it uses whichever separator the base path speaks, so Windows and SSH-remote POSIX paths both survive)
- **Worktree path comparisons:** `normalizeWorktreePath(path, posix?)`, `isPathAtOrUnderRoot()` in `src/shared/worktreePaths.ts`; `worktreeDedup.ts` re-exports the normalizer as `normalizePath` for renderer callers.
- **Remote worktree aliases:** `resolveWorktreeAliasesRemote()` in `src/main/utils/remote-git.ts` batches saved and registered paths with independent physical, missing, or unresolved results.
- **Reading a zip from disk:** `readZipArchive(filePath, options?)`, `extractZipTo()`, `isUnsafeZipEntryName()` in `src/main/utils/zip-archive.ts`. Playbook import and Cue backup only need names and bytes. Pass `names` or `filter` so a manifest-only read does not inflate the rest; default caps refuse a zip bomb. `extractZipTo` walks every path component under the dest root so an intermediate symlink cannot escape. Do not bring back `adm-zip` (`extractAllTo` follows dest symlinks; no patched release).
- **Classifying a file by extension:** `getFileCategory()`, `isPreviewableFile()`, `matchesFileCategory()`, `getFileExtension()` in `src/shared/fileCategories.ts`. One table answers both "can Maestro open this?" and "which bucket is it in?" - `isPreviewableFile` is DERIVED from `getFileCategory`, so a file cannot show up under a filter pill and then refuse to open, nor be openable while invisible under every pill. Audio and video are NOT listed there; they resolve through `getMediaKind()` in `mediaTypes.ts`, which is the single source of truth for what Chromium can decode. Do NOT hand-roll another `TEXT_EXTENSIONS` set - the private copies inside `FileSearchModal` are exactly what this replaced. Distinct from the sets in `src/renderer/utils/fileExplorerIcons/shared.ts`, which exist to pick an ICON and split far more finely (lockfiles, test folders); pick by question and do not merge them.
- **Guarding a serialized JSON payload before it overwrites a file:** `assertSerializedJsonIsSafe(serialized, target)` in `src/shared/jsonUtils.ts`. The failure it stops is `JSON.stringify` returning `undefined` (for `undefined`, a function, or a symbol), which `writeFile` persists as the literal text `"undefined"` - unparseable, and a silent way to clobber a good store. It does NOT unconditionally round-trip parse: above 1MB it falls back to a balanced-delimiter check, because `JSON.stringify` cannot emit malformed JSON (it throws instead) so the parse only ever catches truncation, which cannot happen to a string still in memory. That re-parse cost ~40ms per write on the 20MB agent-runs store, paid on the Electron main thread - the same thread that answers every IPC call. Two rival copies of this guard existed: one in `atomic-json-store.ts` and a bare `JSON.parse(content)` inline in `agent-run-store.ts`. Do NOT add a third, and do NOT "harden" it back into an unconditional parse.
- **Serializing async work on one entity:** `createKeyedWriteQueue()` in `src/shared/keyedWriteQueue.ts` (re-exported from `src/main/utils/atomic-json-store.ts`, which is still the import site every main-process caller uses). Each key gets its OWN promise chain, so two callers racing on the same entity cannot interleave a read-modify-write while different keys stay concurrent, and settled entries are dropped so the backing Map stays bounded in a process that runs for days. It lives in `shared/` rather than beside `atomicWriteJson` because BOTH processes need it and the renderer cannot import a module that pulls in `fs/promises` - the same hoist, for the same reason, as `assertSerializedJsonIsSafe`. **One queue is not enough when the work crosses a process boundary.** A remote tab rename is ordered TWICE and the two queues answer different questions: `tabCallbacks.ts` orders the PROTOCOL (it is what puts `rename_tab_result` frames back on the wire in request order) while `useRemoteIntegration.ts` orders the WORK (provider metadata write, history relabel, store patch), because the server's queue stops waiting after a bounded delay so an unresponsive renderer cannot wedge a tab - and it then dispatches the next rename while the abandoned one is still running, which is exactly the lost update both queues exist to prevent. A queue held in one process cannot order work performed in another. Both key on `${sessionId}:${tabId}`; a renderer-side queue must be at MODULE scope, never a ref, since the listener is registered from an effect that can re-run and a rebuilt queue forgets the work still in flight. Do NOT hand-roll another `Map<string, Promise<void>>` chain.
- **Loading an image for canvas compositing:** `loadImageElement(src)` in `src/renderer/utils/loadImage.ts`. Wraps `new Image()` + `onload` / `onerror` in a promise and REJECTS on failure, so a caller can never draw a half-decoded element and then report success. `compositeAnnotatedImage` and `cropImageDataUrl` both use it, which is what keeps a crop and the save that follows it from disagreeing about whether the source loaded. Accepts anything an `<img src>` accepts, including `maestro-image://` store refs, and the resolved element is decoded so `naturalWidth` / `naturalHeight` are safe to read immediately. Two other loaders in the renderer look like duplicates and are NOT - do not "consolidate" them without reading why: `loadImage()` in `AchievementShareButton.tsx` resolves `null` on purpose (a missing avatar or GitHub logo falls back to a drawn placeholder rather than failing the whole share card), and `svgToPngDataUrl()` in `imageExport.ts` sets `img.decoding = 'async'` before assigning `src`, a hint this helper does not expose. Reach for `loadImageElement` when a failed load must abort the operation.
- **Saving generated image bytes to disk:** `saveImageDataUrlToDisk(dataUrl, defaultName?)` in `src/renderer/utils/imageExport.ts`. The sibling `saveImageElementToDisk()` exports an image already in the DOM; this one takes bytes produced on the fly (a page screenshot, a canvas render) that have no element to point at. Both go through the native save dialog and then `fs.writeImageFile` - `fs.writeFile` is UTF-8 and would corrupt raster bytes - and both fall back to a browser download when the dialog is unavailable (web renderer). A cancelled dialog reports `{ saved: false }` with no `error`, so a caller must not treat every non-save as a failure toast. Unlike `saveImageToProject()` it does not call `requestFileTreeRefresh`, because the user usually points it outside any workspace.
- **Screenshotting a surface exactly as it is painted:** `window.maestro.shell.capturePage(rect?)` (preload in `src/main/preload/system.ts`, handler `window:capturePage` in `src/main/ipc/handlers/system.ts`). Reach for it instead of serializing a canvas or rasterizing DOM whenever the thing on screen is composed of several render paths - the Document Graph is a `<canvas>`, a React markdown preview pane, and an HTML legend, and only the compositor knows what the user is actually looking at. Three things the handler already solves, so do not re-solve them: it captures `event.sender` rather than the main window, so a surface opened in a second window shoots itself; it floors and clamps the rect, because `getBoundingClientRect()` returns floats and Chromium answers an out-of-range region with an empty image; and it resolves `null` rather than an empty data URL when there is nothing to capture. Callers must dismiss any modal of their own and wait two animation frames first, or the shot includes the dialog that asked for it. Feature-detect (`!!window.maestro?.shell?.capturePage`) before offering the affordance - the web renderer has no bridge.
- **Strip ANSI / collapse progress overwrites:** `stripAnsiCodes()`, `processCarriageReturns()` in `src/shared/stringUtils.ts`
- **Reading what a TUI painted from a raw capture:** `replayTerminalScreen()`, `capturedAlternateScreen()` in `src/maestro-p/screen-replay.ts`
- **Shell escape:** `shellEscape()`, `shellEscapeArgs()`, `shellEscapeForDoubleQuotes()` in `src/main/utils/shell-escape.ts`
- **A path handed to a REMOTE shell (`cd`, `ls`, `stat` over SSH):** `shellEscapeRemotePath()` in `src/main/utils/shell-escape.ts`. `shellEscape()` single-quotes, and a single-quoted `'~/proj'` never expands: the remote looks for a directory literally named `~`, and nothing local can substitute the remote user's home (`expandTilde()` produced `/Users/<local>/git-projects`, which validated green and then did not exist on the host). A leading `~` or `$HOME` is rendered as `"$HOME/rest"`; every other path falls through to `shellEscape()` byte for byte. Every remote `cd` rides it - both SSH command builders, the terminal tab handler, `SshCommandRunner`, and all of `remote-fs.ts` - because four private copies of the rule had already drifted, and one of them defaulted to a quoted `'~'` that could not work. Do NOT hand-roll another `startsWith('~/')` branch at a new call site.
- **SSH connection options (`ssh -o`):** `resolveSshOptions()` / `buildSshOptionArgs()` / `validateSshOption()` in `src/shared/sshOptions.ts`. FOUR sites used to build this list by hand - the agent spawn builder, the Test Connection probe, `SshCommandRunner`, and the interactive terminal tab in `ipc/handlers/process.ts` - and they had already drifted: only one set `LogLevel`, and the terminal path omitted `BatchMode` entirely, so a remote could test green and then connect with a different option set when an agent actually ran on it. The merge is CASE-INSENSITIVE, which is the whole reason it is one function: OpenSSH treats keywords case-insensitively but applies `-o` FIRST-WINS, so a naive spread emitting both `-o ConnectTimeout=10` and `-o connecttimeout=45` silently keeps the 10 and the user's override does nothing. A command-line `-o` also outranks `~/.ssh/config`, so `SshRemoteConfig.sshOptions` is the ONLY way a user can change one of these defaults - that is what makes an exotic transport (a tailcat / cloudflared / Teleport `ProxyCommand`, a `ProxyJump` bastion) expressible without a field per transport. `RequestTTY` is RESERVED: it is derived per command from whether the remote agent speaks stream-json, and a forced TTY corrupts that stream, so it is a property of the command and pinning it per host silently breaks agents. The `'interactive'` context drops `BatchMode`, `RequestTTY` and `LogLevel` (the user is at the keyboard and can answer a passphrase prompt, the caller passes `-t`, and SSH's own warnings are the diagnostics someone watching a terminal needs) and an override may NOT reinstate one - adding it back re-breaks the terminal it was dropped for. Do NOT hand-roll another `-o` list; the module is import-free so the CLI bundle can use it too.
- **HTTP request from the main process:** `fetchWithTimeout(url, options?, timeoutMs?)` in `src/main/utils/fetchWithTimeout.ts`, with `isFetchTimeoutError()` and `DEFAULT_FETCH_TIMEOUT_MS`. A bare `fetch()` has no timeout, so a stalled socket hangs the caller forever: in the main process that is an IPC handler that never settles and a renderer spinner that never stops. Do NOT hand-roll `new AbortController()` + `setTimeout`, and do NOT add another local `fetchWithTimeout` - there used to be three functions with that exact name and three different signatures. It composes a caller-supplied `signal` rather than overwriting it, so passing your own cancellation still works. Callers needing extra behaviour wrap it locally (see `fetchBmadResource()` in `src/main/bmad-manager.ts`).
- **Timing out a supervised agent process:** `createIdleWatchdog({ idleMs, maxMs?, onIdle, onMax? })` in `src/main/utils/idle-watchdog.ts`, fed by the event list in `src/main/utils/agent-liveness.ts`. **The budget is SILENCE, not wall clock.** A plain `setTimeout` armed at spawn measures TOTAL duration, so an agent emitting output every second and one that died a minute in look identical to it - which is how a Group Chat participant was declared dead at ten minutes while its transcript showed 19-41 events per minute straight through the cutoff. The caller `touch()`es on every proof of life and the clock restarts; the optional `maxMs` ceiling is a SECOND, non-resettable timer, because an agent stuck in a tool loop chatters forever and can never satisfy the idle budget. What counts as proof of life is `AGENT_LIVENESS_EVENTS` (`data`, `thinking-chunk`, `tool-execution`, `usage`), and getting that list wrong silently reintroduces the wall-clock bug: for a `--print` stream-json run `StdoutHandler` emits `data` ONLY on the terminal result, so a budget armed on `data` alone is a hard deadline that never re-arms. Both the cross-agent consult router and the Group Chat router shipped exactly that. `raw-stdout` is deliberately excluded from the shared list (it fires per chunk for every process in the app) and opted into by name where wanted. **A timeout must kill before it reports**: changing only Maestro's state tells the room the turn failed while the process it gave up on goes on editing files, committing, and pushing. Kill with the FULL spawned session id - `getModeratorSessionId()` returns the per-chat PREFIX that every turn appends a timestamp to, and `ProcessManager.kill` matches exact keys, so the prefix kills nothing. Group Chat's budgets are re-armed by `setupGroupChatLivenessListener` (`src/main/process-listeners/`), which hands session ids to `noteGroupChatActivity()`; the router itself never grows an EventEmitter. Do NOT hand-roll another `setTimeout` supervisor for a spawned agent.
- **A git query the UI polls in the background:** `execGitReadOnly()`, `isGitTimeout()`, `READ_ONLY_GIT_TIMEOUT_MS` in `src/main/utils/remote-git.ts`
- **Whether a working directory can be spawned into:** `unusableCwdReason()` in `src/main/process-manager/utils/spawnCwd.ts`. node-pty's Windows path throws ERROR_DIRECTORY from an async callback nothing can catch, so `ProcessManager.spawn()` refuses a missing cwd up front rather than falling back to home.
- **Encore Feature flags and their defaults:** `ENCORE_FEATURE_DEFAULTS`, `resolveEncoreFeatures()` in `src/shared/encoreFeatureDefaults.ts`
- **Maestro's data directory, without Electron:** `resolveUserDataDir(options?)`, `assertUserDataDirExists(dir)` in `src/shared/userDataDir.ts`. Anything holding Electron keeps using `app.getPath('userData')`; this is for the CLI and the standalone Cue runner, which must land on the SAME directory. `MAESTRO_USER_DATA` wins (the app publishes its resolved path at startup); the fallback name differs between an unpackaged run (`maestro`) and an install (`Maestro`), with a `maestro-dev` hop in development, so a process without Electron must `assertUserDataDirExists` rather than create an empty database beside the real one. Three older fallbacks disagree on that name; move them here rather than adding a fourth.
- **Where the synced stores live (custom Storage Location):** `resolveSyncDir(userDataDir)`, `isValidSyncPath()`, `BOOTSTRAP_STORE_FILE` in `src/shared/syncPath.ts`; the CLI's `getSyncDir()` in `src/cli/services/storage.ts`. Settings -> General -> Storage Location moves settings, agents, groups, session origins, pasted images and group chats out of the user-data directory, and records the choice in `maestro-bootstrap.json`, which stays behind. A process without Electron has to read that file to find the rest: the CLI used to read every store from the user-data directory alone, so it listed only the agents that existed before the move and answered `Agent not found` for every one created since. Two directories, not one: history, playbooks, agent configs and `cli-server.json` never move, so do NOT point `MAESTRO_USER_DATA` at the sync folder - the CLI then finds the agents and loses the running app. The validator is shared with the desktop's `getCustomSyncPath()` because the app silently falls back to the user-data directory for a path it rejects, and a reader that honored the path anyway would be reading files the app is not writing.
- **Platform detection:** `isWindows()`, `isMacOS()`, `isLinux()` in `src/shared/platformDetection.ts` (works in both processes). In renderer-only code prefer `isWindowsPlatform()`, `isMacOSPlatform()`, `isLinuxPlatform()` in `src/renderer/utils/platformUtils.ts`. Never read `process.platform` in the renderer: the `process` polyfill (`src/renderer/public/process-shim.js`) reports the sentinel `'browser'`, and `navigator.platform` is deprecated and unreliable.
- **Modifier-key display text:** `formatKey()`, `formatShortcutKeys()`, `formatMetaKey()` (symbol: `⌘` / `Ctrl`), `formatMetaKeyName()` (prose: `Command` / `Ctrl`), `formatAltKeyName()` (prose: `Option` / `Alt`) in `src/renderer/utils/shortcutFormatter.ts`. Do NOT hard-code `⌘`, `Cmd+`, `Ctrl+`, `Option`, or `Alt` in UI copy, tooltips, or setting descriptions - it renders the wrong key on the other platform. Outside the renderer (CLI, main process) use `formatShortcutKeysFor(keys, isMac)` / `formatKeyFor()` in `src/shared/shortcutKeys.ts`, which owns the key maps; the renderer file is just the platform binding over it.
- **Advertising a shortcut next to the control that fires it:** `ShortcutHint` (`variant="chip"` for an inline key-cap, `variant="row"` for a hint header pinned above a dropdown's option list) and `shortcutSuffix(keys)` (for a `title=` tooltip) in `src/renderer/components/ui/ShortcutHint.tsx`. Maestro is keyboard-first, so every clickable route to an action that ALSO has a chord should show that chord where the user is already looking; this is the one component that draws it. Four byte-identical local `ShortcutHint` definitions had accumulated across the tab-bar components before it was extracted. Two invariants: read the chord from the LIVE shortcut map (`useSettingsStore(s => s.shortcuts)` / `s.tabShortcuts`) rather than spelling a default combo into the string, or a user who rebinds sees our chord instead of theirs; and both helpers render NOTHING for an empty or missing `keys`, because an action can ship unbound and a blank key-cap advertises a combo that does nothing. When a row already has a right-aligned badge, wrap the badge and the key-cap in ONE `ml-auto` container - two auto margins on siblings split the free space and strand the badge mid-row.
- **Font-family fallback chain:** `withMonoFallback()` / `resolveSurfaceFont()` over `MONO_FALLBACK_STACK` and `SANS_FALLBACK_STACK` in `src/shared/fontStack.ts`. The font pickers store a BARE family name (`Roboto Mono`) with no generic on the end, so a name that is not installed and not web-loaded resolves to the browser's document default, which is a proportional SERIF - the whole app renders in Times. Apply the fallback where the setting becomes a CSS value, never at the setting source: the picker's `<select>` has to match its options against the raw stored name. Every surface font (terminal, chat, file preview, file editor) stores the empty string to mean "inherit the interface font", and `resolveSurfaceFont` is what that empty string means - re-deriving it and forgetting the `.trim()` renders a whitespace-only family, which resolves to nothing. Do NOT add a surface-local fallback chain; a second chain drifts the first time one of them gains a platform, and that surface then degrades to different faces than the rest of the app. `XTerminal.tsx` shipped exactly that duplicate (`ensureMonospaceFallback`) and it was folded back in. **A fallback chain is not the whole answer for the terminal**, which additionally MEASURES: a configured font can resolve perfectly and simply not be fixed-pitch, so the appended generic is never reached while xterm draws every glyph on the pitch of `W` (`Cl aude`). That half is `isFixedPitchStack()` / `resolveTerminalFontFamily()` in `src/renderer/utils/fixedPitchFont.ts` (see the entry below), and it composes with the shared chain rather than replacing it. The MAESTRO wordmark is deliberately NOT derived from any setting - it uses `WORDMARK_FONT_STACK`, because a brand mark that changes identity with the reading font is a bug.
- **Fixed-pitch font for shell text:** `resolveFixedPitchFontFamily()` / `resolveTerminalFontFamily()` / `isFixedPitchStack()` in `src/renderer/utils/fixedPitchFont.ts`, wrapped for React by `useFixedPitchFont()`. Every surface that renders shell text rides it - the terminal, the command-mode composer, and the shell command card - so a proportional UI font cannot silently break one while the others measure. It COMPOSES with the entry above rather than restating it: the fallback half is `withMonoFallback` / `MONO_FALLBACK_STACK` from `src/shared/fontStack.ts`, and this module shipped a local duplicate of that chain (`ensureMonospaceFallback`, `FIXED_PITCH_FALLBACK_STACK`) before it was folded back in. Do NOT reintroduce one. When a surface overrides the composer font this way, any decoration layered under it takes the SAME override - the mention-chip overlay is measured against the textarea's font, so a monospace textarea over a proportional overlay lands every chip off its word.
- **Saving the user's own font setup:** `captureTypographySnapshot(values)`, `typographySnapshotPatch(snapshot)`, `typographySnapshotMatches(snapshot, values)` in `src/shared/typographySnapshot.ts`, persisted under the single `typographySnapshot` setting and driven by `saveTypographySnapshot()` / `restoreTypographySnapshot()` in `src/renderer/stores/settingsThemeSlice.ts`. It is what makes the Factory Reset presets safe to try: they overwrite all six surfaces at once, so without a saved copy a user could not even LOOK at Default or Hacker without destroying a hand-tuned setup. Derive the keys from `TYPOGRAPHY_SURFACE_LIST` (`src/shared/typography.ts`), never from a hand-written list - a second list is how a newly registered surface silently stops being saved. Zoom is deliberately NOT part of a snapshot, matching `resetTypography` leaving it alone.
- **Whether a chord may be bound at all:** `findReservedShortcutCombo(keys)` over `RESERVED_SHORTCUT_COMBOS` in `src/shared/shortcutKeys.ts`. `Cmd/Ctrl+Shift+Arrow` is the OS's select-to-end inside a text field, and a Maestro binding on one wins in every input in the app, so the user loses the native behavior with no way to tell that apart from a broken text box. It is checked at BOTH ends and the second half is the one that is easy to skip: the recorder (`ShortcutsTab`) refuses a fresh binding, and `migrateShortcuts()` in `settingsStore.ts` strips one already on disk and falls back to the bundled default. A recorder-only guard leaves anyone who already bound the chord broken forever. The table is written in terms of `Meta` and comparison collapses `Ctrl`/`Command` into it, matching `eventMatchesShortcutKeys`, so one entry covers every platform - do NOT add a separate `Ctrl` row.
- **Whether a keystroke belongs to a text field (guard before `preventDefault`):** `isTextInputTarget()`, `isTextEntryTarget()` in `src/renderer/utils/messageScrollNavigation.ts`; `isEditingTextTarget()`, `isEditingTextFocused()` in `src/renderer/utils/editableTarget.ts`
- **How much of the keyboard the user has mastered:** `collectBoundShortcuts(...maps)` (denominator) and `countUsedBoundShortcuts(bound, usedIds)` (numerator) in `src/renderer/constants/keyboardMastery.ts`. Every mastery figure - the help modal's bar, the Usage Dashboard ring and its Unused Shortcuts list, the leaderboard payload, and the level-up check in `settingsStore` - runs the LIVE maps (`shortcuts`, `tabShortcuts`, `FIXED_SHORTCUTS`) through these, so they cannot disagree about the total. **A shortcut with no chord bound is excluded from both ends**: it can never be fired, so counting one in the denominator makes Keyboard Maestro unreachable forever, and listing one as "unused" tells the user to go press a chord that does not exist. The numerator is not `usedShortcuts.length` either - that list keeps ids whose binding was later cleared, which reports over 100%. Do NOT hand-roll another `Object.keys(DEFAULT_SHORTCUTS).length + ...` total; there were already two rival copies that disagreed with the surfaces rendering the number.
- **Naming the OS file manager in copy:** `getFileManagerName(platform)` / `fileManagerName()` in `src/renderer/utils/platformUtils.ts` return the bare noun (`Finder` / `Explorer` / `File Manager`), and `getRevealLabel()` / `getOpenInLabel()` are built from it. Do NOT hard-code `Finder` in a hint, tooltip, or menu label - it reads wrong on Windows, and the failure is invisible on a Mac dev machine. Same class of bug as hard-coding `⌘`.
- **Parsing a Codex assistant directive:** `parseCodexDirective()`, `findCodexDirectives()`, `KNOWN_CODEX_DIRECTIVES` in `src/shared/codexDirectives.ts`; `findCodexDirectives()` also returns U+E200 content references (`findCodexContentReferences()`, `KNOWN_CODEX_CONTENT_REFERENCES`)
- **Rendering a Codex `visualize` reference inline:** `loadCodexVisualization()` in `src/main/codex-visualization.ts` (assets read from the agent's own CODEX_HOME, served on the Concerto `visualization` surface), drawn by `CodexVisualizationFrame` (`src/renderer/components/Markdown/components/CodexVisualizationFrame.tsx`)
- **Resuming a provider session a background synopsis may still hold:** `awaitSynopsisWriters()` in `src/main/process-manager/utils/synopsisWriterGate.ts`, already called by `process:spawn`; do NOT add a per-send-path busy check
- **Acting on a clicked Codex followup chip:** `requestCodexFollowup()`, `resolveCodexFollowup()`, `codexDirectivesLiveForSource()` and `CODEX_FOLLOWUP_EVENT` in `src/renderer/services/codexFollowup.ts`, drawn by `FollowupChip` (`src/renderer/components/Markdown/components/FollowupChip.tsx`) and answered by one `useEventListener` in `App.tsx`. A Codex agent ends a turn by OFFERING its next moves as directives inside its own markdown, and `LogItem.tsx` renders `<MarkdownRenderer>` at SIX call sites - so an `onFollowup` callback would be threaded through `MainPanel`, `TerminalOutput`, `LogItem`, `MarkdownRenderer`, `Markdown` and the component map purely as a conduit, and the next surface that renders assistant markdown would silently drop its chips. One app-level `CustomEvent` instead, the same shape as `requestHeadingPalette` and `requestOpenStagedImagesOrganizer`. Do NOT prop-drill it. Three rules ride it, and each guards a failure the user cannot undo. **The request's session and tab are CHECKED, never trusted:** a chip is only drawn in the tab on screen, so a mismatch means the store moved between the click and the handler, and the prompt is AGENT-authored - landing one in a conversation that never offered it is not recoverable, so a moved conversation degrades to a prefill plus a `notifyCenterFlash` saying why. That decision is `resolveCodexFollowup()`, a pure function rather than a branch inside the listener, because it is the consequential half and `App.tsx` is not a surface a test can reach - the listener is left as the event binding it should be. **The prefill goes through `loadAiDraft`, not `setInputValue`:** text, command mode and owning tab are one triple, so writing the text alone drops an agent prompt into a bang composer where Enter runs it as a shell command. **Both halves of the chip are agent-authored and nothing checks that they agree**, so the full prompt is always on the hover overlay AND on the `aria-label` (the overlay for a mouse, the label for a keyboard user and a screen reader, where `HoverTooltip` never fires) - a friendly label over a hostile prompt must not be able to hide it. An Alt-click prefills rather than sends, and an empty prompt renders the label as plain text, because a button that sends nothing reads as broken. Which BODIES get chips is `codexDirectivesLiveForSource()` (`stdout` / `ai` only, the pair `useAgentExitListener` treats as the agent's own output), applied by `LogItem`: a directive in a user message is text the user typed - handing a live push button to somebody who merely QUOTED the syntax is the worst failure in the feature - and one in the AI-command body, an error body, a thinking block or tool output describes the format rather than offering an action. It is separate from the provider check (`session.toolType === 'codex'`, applied in `TerminalOutput`), and both have to hold: the provider decides whether the syntax is a convention here at all, this decides whose words these are.
- **Running a shell code fence from an agent's reply:** `requestCodeFenceCommand()` / `CODE_FENCE_COMMAND_EVENT` in `src/renderer/services/codeFenceCommand.ts`, `isRunnableShellLanguage()` / `commandFromShellFence()` in `src/renderer/utils/shellCommandInput.ts`, drawn by `<CodeRunButton>`. Rides `resolveCodexFollowup()` for targeting and `dispatchShellCommand()` for the run; do NOT add a second run path.
- **Touch primitives:** `isCoarsePointer()`, `triggerHaptic()`, `supportsHaptics()`, `HAPTIC_PATTERNS`, `GESTURE_THRESHOLDS`, `MIN_TOUCH_TARGET` in `src/renderer/utils/touch.ts` (do NOT re-derive `navigator.vibrate` calls or `matchMedia('(pointer: coarse)')` queries)
- **Long-press gesture:** `useLongPress()` in `src/renderer/hooks/utils/useLongPress.ts` (scroll-aware touch long-press that opens right-click affordances like context menus and tab overlays; do NOT hand-roll `setTimeout` + `touchmove`)
- **Color math and contrast:** `readableTextOn()`, `isReadableOn()`, `contrastRatio()`, `relativeLuminance()`, `blendColors()`, `transparentize()`, `adjustBrightness()`, `hexToRgb()` in `src/shared/colorContrast.ts`. Whenever a foreground and its background are BOTH derived from theme colors (diagram fills, chart labels, badges, generated SVG), run the foreground through `readableTextOn(preferred, backgrounds)` so a theme whose accent sits near its text color can't paint near-identical colors on top of each other. It returns the preferred color untouched when it already clears WCAG AA and nudges the theme's own color otherwise, so nothing snaps to hard-coded black/white.
- **Diagram labels on fills the AUTHOR chose:** `repairDiagramLabelContrast(svg)` in `src/renderer/utils/diagramLabelContrast.ts`, over `cssColorToHex()` in `src/shared/colorContrast.ts`. Theme variables cannot protect a `style X fill:#f9e79f` or `classDef` with no `color:`, so both mermaid render paths (`MermaidRenderer`, the Fast tier's `mermaidRenderer.ts`) measure every node and cluster as painted, after the SVG is attached, and rewrite any label under AA. A new diagram surface must call it after attaching, or the light-on-pale regression comes back.
- **Agent display name:** `getAgentDisplayName()` in `src/shared/agentMetadata.ts`
- **Two-letter labels for a set of agents:** `buildAgentInitials(agents)`, `agentInitialCandidates(name)` in `src/shared/agentInitials.ts`
- **Which providers a user can pick:** `AGENT_PICKER_META` / `PICKABLE_AGENT_IDS` / `getAgentPickerMeta()` in `src/shared/agentMetadata.ts`. ONE registry feeds all three provider pickers: the New Agent modal (`SUPPORTED_AGENTS` re-exports it), the New Agent Wizard's tile strip (`AGENT_TILES` is derived from it), and the Group Chat moderator dropdown (the same tiles, filtered by what detection found). Do NOT hand-write a fourth list of agent ids for a new picker: the three used to be written by hand, and Grok and Qwen3 Coder shipped selectable in the New Agent modal while being absent from the wizard and un-pickable as a moderator. The record is keyed by `AgentId`, so a new id fails to compile until it is either given metadata or explicitly set to `null` (internal or unshipped). **Order is derived, not authored:** `PICKABLE_AGENT_IDS` sorts by display name, so every picker shows one predictable alphabetical list and a new entry can go anywhere in the record. Do NOT re-sort the record by hand to move a provider, and do NOT default a picker to the first entry - that hands a fresh Group Chat to whichever beta provider sorts first. Auto-selection reads `AGENT_AUTOSELECT_ORDER` (a preference list) and picks the first entry the user actually has installed. Adding a provider also means a `case` in `AgentLogo` and a glyph in `AGENT_ICONS`, or the tile draws a blank fallback ring. What a picker SHOWS is narrowed again at render time by detection - see `filterToAvailableProviders` below - so a provider missing from a picker may simply not be installed on that machine. Full checklist: [PROVIDER-SUPPORT.md → Step 2.6](PROVIDER-SUPPORT.md#step-26-register-the-provider-in-the-pickers).
- **Which account dirs exist for a provider (main):** `getProviderAccountDirs(toolType, options?)` in `src/main/agents/provider-account-dirs.ts`
- **Reading a Codex account's credentials off disk:** `readCodexAuth()`, `codexAuthHeaders()`, `extractEmailFromJwt()` in `src/main/agents/codex-auth.ts`. `CODEX_HOME/auth.json` is the one place a Codex account's identity lives, and both the quota sampler and the reset-credit primitives need it. A second copy drifts first on the HEADER: a request without `ChatGPT-Account-Id` answers for the WRONG account when the login holds several workspaces, and an empty one is rejected rather than falling back - so it is omitted, never sent blank. The two failure kinds stay apart because their remedies differ (`missing_auth` = never logged in, `unauthenticated` = present but no usable token). The JWT email is a LABEL and its signature is deliberately unverified.
- **Codex rate-limit reset credits:** spend rules in `src/shared/codexResetCredits.ts` (`describeCreditSpend`, `shouldAutoSpendCredit`, `nextCreditToSpend`, `describeCreditExpiry`), the two primitives in `src/main/agents/codex-reset-credits.ts` (`fetchCodexResetCredits` READs, `consumeCodexResetCredit` WRITEs), renderer state in `src/renderer/stores/codexResetCreditsStore.ts`, automation in `src/renderer/services/codexAutoReset.ts`. **The count is free** - `rate_limit_reset_credits` already rides the `/wham/usage` payload, so `CodexUsageSnapshot.resetCredits` costs no extra request and only a surface rendering the LIST pays for the detail read. **THE trap: `available` and `applicable` are different questions.** `available` is inventory; `applicable` is how many would take effect RIGHT NOW, and the API returns 0 there whenever nothing is consumed enough to benefit (a real account read `2` and `0`). A credit spent then is simply gone, so every surface goes through `describeCreditSpend()` rather than pairing `available` with an unconditional button, and an ABSENT `applicable` stays `undefined` rather than becoming 0 - unknown and zero drive different verdicts. **Three wire-format traps in `consume`:** `redeem_request_id` is REQUIRED and IS the idempotency key, so it belongs to one logical SPEND and must never be minted per HTTP attempt inside a retry loop; failure arrives as **HTTP 200** (`{"code":"no_credit","windows_reset":0}`), so `response.ok` alone reports every refusal as success; and `windows_reset > 0` is the only proof anything happened. The AUTOMATIC path is strictly narrower than the button - `shouldAutoSpendCredit` refuses `wasteful` AND `unknown`, plus SSH-backed agents (those credits belong to THIS machine's account) - and fires at most once per outage, keyed at MODULE scope because the listener is re-registered whenever its effect re-runs. Do NOT call `consumeCodexResetCredit` from a refresh, sweep, or retry default: every path to it is an explicit click or an explicitly enabled per-agent toggle.
- **Naming the account a login acts on:** `AccountPill`, `ghAccountLabel()` in `src/renderer/components/ui/AccountPill.tsx`; `parseGhActiveAccount()` in `src/main/utils/ghErrors.ts`
- **Keeping a plan account on the Usage Dashboard after its agents leave:** `rememberQuotaAccounts()`, `pruneMissingQuotaAccounts()` in `src/main/stores/quotaAccountsStore.ts`; `partitionSnapshotsByAge()`, `SNAPSHOT_RETENTION_MS` in `src/main/stores/usageSnapshotRetention.ts`
- **SSH remote lookup:** `getSshRemoteById()` in `src/main/stores/getters.ts`
- **Deferred main-process store persistence:** `deferStoreWrites()`, `flushPendingSessionWrites()` in `src/main/stores/deferred-writes.ts` / `src/main/stores/instances.ts`
- **Recovering from a store file that is not JSON:** `createStoreDeserializer()`, `corruptStorePath()` in `src/main/stores/corrupt-store-recovery.ts`. conf rethrows a `SyntaxError` from the Store constructor, so one torn file bricks startup forever; the file is quarantined to a stamped `.corrupt-` sidecar and the store falls back to defaults. Never `clearInvalidConfig`.
- **Writing a Claude session's origin record:** `setClaudeSessionOrigin()`, `mergeClaudeSessionOrigin()` in `src/main/storage/claude-session-origins.ts`
- **Toast notifications:** `notifyToast({ color, title, message, dismissible? })` in `src/renderer/stores/notificationStore.ts`. Use for async results, errors, and persistent/dismissable messages. Same five-color design language as Center Flash: `green | yellow | orange | red | theme` (default `theme`). Set `dismissible: true` (or pass `--dismissible` from `maestro-cli notify toast`) when the user MUST acknowledge - disables auto-dismiss, requires click to close, and emphasizes the X button. Cannot combine `dismissible` with `duration`/`--timeout`. External CLI cap: 60 seconds (use `--dismissible` for sticky). **Click actions** (data-driven, survive the IPC bridge): pass `clickAction` for what should happen when the toast body is clicked, or use the legacy `sessionId`/`tabId` fields for plain agent jump. The shape and its validator (`parseToastClickAction`) are canonical in `src/shared/toastClickAction.ts` and aliased by every producer (web-server types, preload, `global.d.ts`, `cue-notify-bridge`); the renderer dispatches every kind through `dispatchToastClickAction()` in `src/renderer/services/toastClickActions.ts`. Kinds: `jump-session` (AI tab), `open-file` (File Preview tab), `open-terminal` (focus an existing terminal tab by id or name; omit `tabRef` for the agent's active one), `open-browser` (focus an in-app browser tab by `tabId`, or open `url` in a new one), `open-url` (system browser). Adding a kind is a change in exactly those two files. From the CLI: `--agent` (+ optional `--tab`), `--open-file <path>`, `--open-terminal [tab]`, `--open-browser <url>`, `--open-browser-tab <id>` (all requiring `--agent`), `--open-url <url>` - mutually exclusive with each other. `--action-url` / `--action-label` render an inline link button beneath the message and are independent of the body click. **Source-agent label:** pass `sourceAgent` (CLI `--source-agent <label>`) to stamp which agent/pipeline fired the toast in the header strip. It's store-independent, so it shows even when the agent isn't loaded in the Left Bar (the name resolved from `--agent`/`sessionId` only renders when that agent is in the desktop store) - use it for cron/watchdog toasts. The explicit label wins over the resolved name for display; pair it with `--agent` to also get click-to-jump. Do NOT pass renderer-only callbacks across the bridge - use `clickAction` instead.
- **Whether in-app toasts are on:** `areToastsEnabled()`, `TOAST_DURATION_OFF`, `resolveEnabledToastDuration()` in `src/shared/toastDuration.ts`; flip them with `setToastsEnabled()` (settings store) or `toggleInAppToasts()` in `src/renderer/services/inAppToasts.ts`. Do NOT compare `toastDuration` / `defaultDuration` against a literal `-1`.
- **What a toast click does:** `ToastClickAction` + `parseToastClickAction()` in `src/shared/toastClickAction.ts`, dispatched by `dispatchToastClickAction()` in `src/renderer/services/toastClickActions.ts`. The shape is data-only so it survives the IPC bridge, and every producer (web-server types, preload, `global.d.ts`, `cue-notify-bridge`) aliases the canonical type rather than restating it. Adding a click kind is a change in exactly those two files - a producer that declares its own union compiles fine and then hands the renderer a kind nothing dispatches, so the toast silently does nothing when clicked.
- **Toast history / unread state:** `history`, `markNotificationRead()`, `selectUnreadNotificationCount()` in `src/renderer/stores/notificationStore.ts`; `<NotificationCenter>`, `<NotificationCenterHost>` in `src/renderer/components/NotificationCenter.tsx`
- **Center flash (rapid acks):** `notifyCenterFlash({ message, color, detail?, duration? })` in `src/renderer/stores/centerFlashStore.ts`; clipboard helper `flashCopiedToClipboard()` in `src/renderer/utils/flashCopiedToClipboard.ts`. Use for momentary "I did the thing" confirmations of user-initiated actions. Five-color design language: `green | yellow | orange | red | theme` - default `theme` matches the active Maestro theme. External integrations can fire flashes via `maestro-cli notify flash <message> --color <color>`. Do NOT roll your own center-screen overlay, useState+setTimeout flash, add a sixth color, or use a Toast for clipboard acks. Single visible flash at a time, themed frosted-glass card mounted once in `App.tsx`. Full decision rules, color palette, and design language: [UI-PATTERNS.md → Center Flash System](docs/agent-guides/UI-PATTERNS.md#center-flash-system-rapid-temporary-notifications).
- **Opening a modal / dashboard by name:** `UI_SURFACES` + `resolveUiSurface()` / `resolveUiSurfaceTab()` / `describeSurfaceAccess()` in `src/shared/uiSurfaces.ts`, dispatched by `openUiSurface()` in `src/renderer/utils/openUiSurface.ts`. One registry serves `maestro-cli open <surface> [--tab]`, the main-process `open_modal` validator, and the renderer dispatcher, and it carries the discovery hints (hotkey id, command-palette text, click target) so an agent can teach the manual path while opening the pane. Adding a modal means adding one entry, not touching four files - and if it has tabs, list them there so `--tab` can deep-link.
- **Whether a modal takes the window over:** `DESTINATION_MODALS` + `DESTINATION_SHORTCUT_IDS` + `registerExternalDestination()` in `src/renderer/stores/modalStore.ts`. Full-window views you GO to (Settings, Usage Dashboard, Director's Notes, Symphony, Cue, Marketplace, Process Monitor, System Logs, Agent Sessions, Memory, Document Graph) close each other on open, so what you see is decided by what you just asked for rather than by each surface's fixed rank in `MODAL_PRIORITIES`. Before that, the Usage Dashboard opened BEHIND Director's Notes and a main-panel destination opened under an overlay changed nothing on screen - both read as a dead keystroke. Dialogs that answer a question about the surface beneath them still layer normally, so do NOT register one as a destination.
- **Toggling the unread filters:** `toggleAllUnreadFilters()` / `toggleTabUnreadFilter()` in `src/renderer/services/unreadFilters.ts`, with `areUnreadFiltersActive()` beside them. The agent filter and the tab filter stay independent, but one press drives both, and a half-filtered view COMPLETES rather than clears. The tab side routes through `toggleTabUnreadFilter` rather than `setShowUnreadOnly` so the pre-filter tab is still saved on the way in and restored on the way out - that save/restore dance used to live inline in `useSessionLifecycle`, where the three entry points could disagree about it.
- **Scheduled Tasks (clock-driven Cue subs):** types and pure helpers in `src/shared/cue/scheduled-tasks.ts`, filesystem work in `src/main/cue/cue-scheduled-tasks.ts`. `maestro-cli cue schedule` and the Cue modal's Scheduled Tasks tab (via `cue:*ScheduledTask*` IPC) both go through that one module, so they cannot write different YAML for the same task. Do NOT hand-roll another cue.yaml writer.
- **Editing one Cue subscription in place:** `patchSubscriptionInYaml()`, `setSubscriptionTurnSettings()`, `findSubscriptionsByName()` in `src/main/cue/cue-subscription-edit.ts`; `applyCueTurnSettingsPatch()` in `src/shared/cue/turn-settings.ts`
- **Whether two subscriptions are the same visual trigger:** `triggerGroupKey(sub)` in `src/shared/cue/trigger-group-key.ts`. The pipeline serializer writes fan-out to mixed or command targets as SEVERAL parallel subscriptions that each re-carry the whole trigger config, so "one trigger" is a derived identity, not a stored id. Three surfaces need that identity and must agree: `yamlToPipeline` collapses matching subs onto one trigger node, `cue-engine` fires every sibling when the user clicks Play (so a manual run matches a scheduled tick), and `SessionsTable` picks one representative per group for Run Now. They previously held three hand-mirrored copies, and the two runtime copies had already drifted from the editor's - they omitted `retrigger_on_comments` / `max_notifications`, so Play fired subs a real tick would have treated as separate triggers. Adding a new event-config field means adding it HERE, once: leave it out and two subs that differ only in that field silently collapse into one. Do NOT re-inline a JSON.stringify of the trigger fields at a new call site.
- **How big a pipeline is on the canvas:** `pipelineCardBounds(nodes, opts?)`, plus `nodeFootprintWidth()`, `estimateNodeWidth()` and `NODE_CHROME_OVERHANG` in `src/renderer/components/CuePipelineEditor/utils/nodeFootprint.ts`. Three surfaces need a pipeline's on-screen rect and must agree: the renderer that sizes the translucent group card (`convertToReactFlowNodes`), the drag handler that stops two dropped pipelines landing on each other (`resolveNonOverlappingPipelineOffset`), and the masonry packer that deals cards into columns (`arrangePipelineGroups`). They used to disagree - only the packer sized a node from its text, while the other two assumed a flat `NODE_BG_WIDTH` - so a node with a long name rendered outside the card that was supposed to contain it. A node is bigger than the box ReactFlow positions in two ways: its width is text-driven (`width: max-content`), which `estimateNodeWidth` derives from the same text the node components render and errs WIDE on purpose; and its chrome hangs OUTSIDE that box, since handles are centred on the borders and badges are pinned past a corner, which `NODE_CHROME_OVERHANG` allows for. Do NOT size a card from `NODE_BG_WIDTH` (it is a FLOOR, not a maximum) and do NOT fold the overhang into `PIPELINE_GROUP_PADDING`.
- **Which pipelines belong to an agent:** `pipelinesForSession(sessionId, pipelines, graphSessions)`, plus `pipelineInvolvesSession()` and `pipelineNamesDeclaredBySession()` in `src/renderer/components/CuePipelineEditor/utils/pipelineMembership.ts`. "Belongs" has two independent senses and the Cue dashboard needs both: PARTICIPATION (an agent node bound to the session, or a command node whose `owningSessionId` is the session) and DECLARATION (the pipeline is defined in that agent's own `cue.yaml`, even when no node points back at it - a fan-out that only dispatches to other agents, or an `action: command` pipeline that is just a trigger plus a shell step). The predecessor, `getPipelineColorForAgent()`, answered a COLOR question and checked agent nodes only, which broke twice over: a command-only pipeline belonged to nobody, so the Sessions table printed a dash and "View in Graph" dropped the user into every pipeline on the machine; and because two pipelines can legitimately share a color, pairing a color list back to names dropped one and mislabeled the other. Membership is resolved by pipeline identity, never by color - `PipelineDot` is keyed on `p.id` for the same reason. Declaration matches on `getPipelineKey(sub)` because names are the grouping key `subscriptionsToPipelines` builds with, so they equal `CuePipeline.name` exactly. Do NOT reintroduce a color-based lookup, and do NOT filter agent nodes inline at a new call site.
- **Where a pipeline is drawn (its home agent):** `resolvePipelineHomes()` in `src/renderer/components/CuePipelineEditor/utils/pipelineMembership.ts`; cluster box geometry `agentClusterBounds()` in `nodeFootprint.ts`
- **What a group chat does with a message it cannot send yet:** the pure rules in `src/shared/groupChatQueueModel.ts` (`mustQueue`, `canDrain`, `enqueueMessage`, `completeItem`, `failItem`, `failHead`, `requeueFailedAtFront`, `resumeQueue`, `removeItem`, `reorderItem`, `parseQueueState`), driven by `src/main/group-chat/group-chat-queue.ts`. **The queue is owned by MAIN, not by a client.** It used to be a zustand array in the renderer, so every client had its own: a message queued on a phone was invisible to the desktop, died with the browser tab holding it, and was delivered only if that one client happened to observe the moderator going idle. Main is also the only party that can decide whether a message is sent or queued - a client decides from a copy that is already stale, and a stale copy sends directly while items are waiting, putting the NEWEST message in front of older ones, which is the one thing a queue exists to prevent. The invariant the whole module holds: a message the user typed is delivered, or it is still queued with the chat paused and the user told why; there is no third outcome. So a failed send KEEPS the item, marks it, pauses, and posts a system message, and never retries on its own - a cause that does not clear (the Encore Feature off, a missing binary) would otherwise re-fire on every idle forever. Three traps the model encodes so no caller re-derives them: completion and failure are recorded BY ID rather than by position (the send is awaited and the queue is editable while it is in flight, so "drop whatever is first now" discards a message the moderator never received); `enqueueMessage` RESUMES rather than merely un-pausing, because `canDrain` refuses a failed head and a queue that reports itself running while sending nothing is a worse stall than a visible pause; and `parseQueueState` THROWS on a malformed file rather than degrading to empty, so the caller can quarantine the bytes instead of letting the next save erase them. The renderer holds only a MIRROR - `groupChatQueues` in `groupChatStore`, keyed by chat id and written from nothing but the `groupChat:queueState` broadcast and the `getQueue` read a chat performs when it opens, since a client can have been asleep, reloaded, or never seen the chat before. It has NO drain: a renderer-side drain only runs while that client is awake and watching, so a phone that slept left its messages queued forever and two clients that both saw idle each sent the same item. `GroupChatInput` adapts the mirrored items to the `QueuedItem` shape `QueuedItemsList` already speaks, so main never learns a renderer type, and it renders the pause: a paused queue sends nothing, so it has to say so and offer Resume rather than leaving messages sitting there with no explanation and no control. Do NOT add a second queue in a renderer store, and do NOT decide send-vs-queue on the client.
- **How much work happened in a group chat:** `computeGroupChatActivity(entries)` in `src/shared/groupChatActivity.ts`, fed by the per-turn measurements `src/main/group-chat/group-chat-turn-metrics.ts` stamps onto each history entry. **Never present the span between a chat's first and last message as effort** - a group chat is a ROOM, not a task, so a chat used twice a week reports hundreds of hours without anyone doing anything (a real one read `481h 34m` for about five hours of work). Working time is the UNION of the busy intervals, because participants run in PARALLEL: three agents working the same ten minutes is ten minutes of chat time, not thirty, and summing per-agent durations produces a number larger than the chat's own age. Entries written before per-turn timing existed carry no `elapsedTimeMs` and degenerate to points, so `GROUP_CHAT_ACTIVITY_STITCH_MS` treats turns closer than five minutes as one block - without it an older chat reports `0m`, which is a worse lie than the span it replaced. Turn duration is measured with `beginSleepAwareSpan()`, never a bare `Date.now()` delta. `turnsWithTokens` / `turnsWithCost` are coverage counts: render a dash rather than a zero when they are 0, since a chat whose turns reported no usage is UNKNOWN, not free. Both the Group Chat Info overlay and the HTML export call the one function, so they cannot disagree about how much work a chat did.
- **How big a tab's conversation is and how long it ran:** `computeTabConversationStats()`, `formatConversationDuration()` in `src/shared/tabConversationStats.ts`
- **Whether an agent is drawn in the Left Bar:** `sessionMatchesFilter()` and `passesUnreadFilter()` in `src/renderer/utils/sidebarMembership.ts`. "Visible" means drawn ANYWHERE in the scrollable list; scroll position is irrelevant. Both the render path (`useSessionCategories`) and `Cmd+[` / `Cmd+]` (`useCycleSession`) call these, because a cycle that disagrees with the list about membership moves the cursor onto an agent that is not on screen, which reads as the shortcut jumping at random. Adding a match rule here fixes both at once. The "does this agent need attention?" half of that answer lives in `sessionNeedsAttention()` / `sessionOrChildrenNeedAttention()` in `src/renderer/utils/sessionAttention.ts`, because the bell badge, the collapsed rail (`SkinnySidebar`), and the jump-badge projection (`useSortedSessions`) ask it WITHOUT a filter context - so a new attention rule (unread, busy, **error**, Auto Run batch, stuck outage) goes there and every surface picks it up at once. Errored agents were invisible in the unread filter for exactly this reason: four rival copies of the predicate, and only one of them knew about `state === 'error'`. The inputs are shared state (`uiStore.sessionFilter`, `uiStore.showArchivedGroupChats`) rather than local `useState` for the same reason - see [STATE-PATTERNS.md → uiStore](docs/agent-guides/STATE-PATTERNS.md#uistore). Note that `orderGroupChatsForDisplay()` drops archived chats ITSELF, so a caller that has already decided archived chats are on screen must pass `{ includeArchived: true }` rather than pre-filtering, or they are removed again on the way through.
- **Whether an agent is answering a consult right now:** `useSessionIsBeingConsulted(sessionId)` for one row, `useConsultedSessionIds()` / `selectConsultedSessionIds(requests)` for a surface that draws many rows in one pass, all in `src/renderer/stores/crossAgentInFlightStore.ts`. A cross-agent `@mention` runs under a synthetic `cross-agent-<requestId>` process id and writes into a HIDDEN tab, so by design it never touches the consulted agent's `state`, unread flags, or attention badge - background work must not disturb the conversation the user has open with that agent. The cost was that a consulted agent sat there drawing a green "Ready" dot for the whole time it was working, and a Claude agent the user had never opened drew the dim "No active Claude session" hollow dot, because a fresh consult tab has no provider session until the answer lands. This store is the one signal that leaks back out, and it is deliberately narrow: enough for a busy dot, never enough to mark the agent as needing attention - do NOT add a consult check to `sessionNeedsAttention()`, which is what lights badges the user has no chip to clear. Three surfaces read it and must agree: the Left Bar row dot (`getEnhancedStatusColor`, which also suppresses `isDisconnected`), the collapsed rail (`SkinnySidebar`), and the worktree pill strip (`CollapsedSessionPill`). Ranking is the part that is easy to get wrong: ABOVE the unbound-Claude hollow dot (an unopened Claude agent is the COMMON consult target, so the hollow dot would mask exactly the case this exists to show) and BELOW the agent's own `state === 'busy'` and any stuck outage, which draw the same dot but carry a more specific label. Do NOT hand-roll another loop over `requests` at a new call site: the per-row hook returns a boolean so a row re-renders only when the answer flips, and the set form is memoized on the `requests` identity, which changes only when a consult starts or finishes.
- **Taking the user to an agent:** `jumpToAgent(sessionId, { tabId? })`, `revealAgentInSidebar(session)`, `openAgentSettings(session)` in `src/renderer/services/agentNavigation.ts`
- **Dismissing a drawer after the user picks something in it:** `closeLeftSidebarForNavigation()` / `closeRightPanelForNavigation()` in `src/renderer/stores/uiStore.ts`, over `isNarrowViewportNow()` in `src/renderer/hooks/ui/useViewportBreakpoint.ts`. On a narrow viewport the Left Bar is an OVERLAY across the main panel, so activating anything listed in it - an agent row, a group chat, a starred session, any `jumpToAgent()` - is a request to LOOK at that thing and the drawer has to get out of the way; on a wide viewport it is a permanent column and closing it would be a surprise, which is why the width check lives inside the action rather than at each call site. **Call it AT THE TAP, never from an effect keyed on what became active.** That is the whole reason it exists: the pre-existing net in `App.tsx` watches the `activeSessionId` TRANSITION, and three of the four things a user can tap move no id at all - a group chat never touches `activeSessionId`, a starred row routinely names the agent that is already active, and re-tapping the current agent behind an open chat changes nothing - so the effect read each of those as nothing having happened and left the user staring at the drawer they just tapped through. `isNarrowViewportNow()` is the non-hook twin of `useViewportBreakpoint().isNarrow` and shares its `classify()`, so a store action reacting to a tap cannot disagree with the layout about where the drawers stop being columns. The transition-keyed effects in `App.tsx` stay as the net for anything that changes the active agent WITHOUT a tap. The RIGHT drawer has the same disease and the same cure: `handleOpenFileTab` calls `closeRightPanelForNavigation()` at the open, because re-previewing the file that is already the active tab moves no id, and media never becomes a tab at all - so the `activeTabKey` effect can never see either one. Do NOT hand-roll another `isNarrowViewport && setLeftSidebarOpen(false)` at a new navigation site.
- **Session lookup:** `selectActiveSession()`, `selectSessionById()` in `src/renderer/stores/sessionStore.ts`; `useActiveSession()` hook in `src/renderer/hooks/session/useActiveSession.ts`
- **Which agent this client is looking at:** `persistActiveSessionId()` / `readPersistedActiveSessionId()` in `src/renderer/utils/activeSessionPersistence.ts`. Do NOT call `window.maestro.sessions.getActiveSessionId()` directly to restore focus: the desktop and every web-desktop browser tab share that one pointer, and a browser reloads on every refocus, so reading it landed the user on whatever the DESKTOP had focused rather than the agent they were working in. Web-desktop answers it from a ladder - `sessionStorage` (this browser TAB's own choice), then `localStorage` (the last choice made in this browser), then the shared value - because two tabs share an origin, so a localStorage-only answer just moves the bleed one level down. Writing is unchanged on both - the shared pointer is what plugin `session.activated` and the CLI's current-agent answer read, so a client that went quiet there would trade one bug for another.
- **Telling the other clients an agent appeared or went away:** `sessions:lifecycleSync`, emitted by `sessions:setMany` / `sessions:setAll` in `src/main/ipc/handlers/persistence.ts` and applied by `useSessionLifecycleSync` (`src/renderer/hooks/session/useSessionLifecycleSync.ts`). Every client - each desktop window, each browser tab - holds its OWN session tree and flushes it into one store, so without the push a close never propagated and the peer's stale copy wrote the agent straight back. Four things keep it honest: main tombstones a closed id so a flush already in flight cannot resurrect it (the newest 1000, bounded by COUNT rather than age - a suspended browser tab can be away for hours, and ids are never reused, so an old tombstone has nothing to block but a stale write; a client that missed a close and outlived its tombstone reloads on reconnect anyway, since the web bridge has no replay), only ADDITIONS travel from `setAll` (a bootstrap snapshot predates anything a peer created, so an absent id there is ignorance, not a close), the hook waits for the startup load before applying (the load ends in one `setSessions` that would overwrite the delta), and deltas apply strictly in ARRIVAL ORDER through one promise chain (an add still restoring when the close for the same agent arrives makes the close look like it names an agent this client never had). It is lifecycle only - tab contents, read-state and queued messages remain last-writer-wins.
- **Session mutation:** `updateSessionWith(sessionId, updater)` for a whole agent, plus one single-tab updater per tab type in `src/renderer/stores/sessionStore.ts` - `updateAiTab(sessionId, tabId, updater)`, `updateFileTab(sessionId, tabId, updater)`, `updateBrowserTab(sessionId, tabId, updater)`. Do NOT hand-roll `setSessions(prev => prev.map(...))` for a single-agent or single-tab patch; reach for the matching helper instead.
- **Moving an agent to another directory:** `withWorkingDirectory(session, dir)`, `workingDirectoryChangeBlocker(session)`, `isSameDirectory(a, b)`, `rebasePathOntoRoot()` in `src/renderer/utils/agentWorkingDirectory.ts`. An agent's directory is FIVE fields, not one: `cwd`, `fullPath`, `shellCwd`, `projectRoot`, and an Auto Run folder living under the old root all move together, so moving `cwd` alone leaves the file tree, the shell, provider session storage, and the playbook folder pointed at the previous project. `workingDirectoryChangeBlocker` is the single reason a move is refused (a live agent process - the PTY's cwd is fixed at spawn time - or an unvalidated SSH path); ask it rather than re-deriving the guard, and remember a trailing slash is NOT a move (`isSameDirectory` normalizes), so a no-op edit must not tear down the tree. Every surface that relocates an agent rides it: the Edit Agent dialog, the web bridge's `update_session_cwd`, and `maestro-cli update-agent --cwd`.
- **Whether a PR is being opened right now:** `usePRCreationActive(worktreePath)` / `startPRCreation(target)` in `src/renderer/stores/prCreationStore.ts`, settled by `usePRCreationNotifier`. `gh pr create` can take a minute, so the request lives in the store rather than in `CreatePRModal`, which the user is free to close while it works. A second `startPRCreation` on the same repo re-attaches instead of opening a duplicate PR, `usePRCreationActive` is what the menus badge with, and the notifier reports the settlement exactly once - toast plus history entry - from a host that stays mounted. Do NOT `await window.maestro.git.createPR` inside a component: a close would throw away the PR's url and its error with it. Same shape as `gitCommandRunStore` for pull/push.
- **Ending a turn with no process exit:** `settleTabThinkingState(session, tabId)` in `src/renderer/utils/tabHelpers` - the inverse of `markTabRunningQueuedItem`. A tab's pulsing dot and its row on the Thinking pill are driven by `tab.state === 'busy'` plus the session's `state`/`busySource`/`thinkingStartTime`, and normally ONLY the process-exit listener clears them. Any path that ends a turn without a process exiting has to settle that state itself, or the tab pulses forever and the pill keeps counting elapsed time for work nobody is doing - `cancelRetry` (`src/renderer/stores/retryStore.ts`) is the live case, where a `scheduled` auto-retry's resend never spawned. Do NOT hand-roll the patch: it also recomputes the SESSION from whatever is still running (another busy tab, or an entry in `orphanedThinkingTabs` - a tab the user closed mid-turn, kept solely so the pill keeps counting it, which settling retires outright), and it deliberately leaves `state === 'error'` alone, since that is the blocking modal's state and settling a tab must not dismiss an error the user still has to act on. Only settle a tab with nothing behind it: an `in-flight` resend IS a live process, and its busy state belongs to the exit listener.
- **Leaving inline wizard mode:** `flattenWizardIntoTab(tab, { summary? })` in `src/renderer/utils/tabHelpers/index.ts`. The inline wizard (`/wizard`) keeps its conversation in `tab.wizardState.conversationHistory` and renders it through `WizardConversationView`, a store entirely separate from `tab.logs` / `TerminalOutput`. So dropping `wizardState` drops the whole wizard conversation, and the provider session handle with it, leaving a tab that looks empty - which is what shipped for every exit except a successful completion. Do NOT set `wizardState: undefined` by hand anywhere; call this instead. It appends the transcript to `tab.logs`, promotes `wizardState.agentSessionId` onto `tab.agentSessionId` so the plain tab can keep talking to the same provider context, appends an optional closing `summary` entry, and only then clears the wizard. It is idempotent (entries are matched by their `wizard-` prefixed ids), which matters because the wizard sync effect in `useWizardHandlers.ts` can race a caller that already flattened. Deliberately NOT wired to closing the wizard TAB: that already warns the user, and the tab itself is going away.
- **Which agents own an Opt+Cmd+# slot:** `buildSessionJumpSlotMap(visibleSessions)`, `sessionJumpShortcut(digit)` in `src/renderer/utils/sessionJumpSlots.ts`

Signals

Stars
3,490
Forks
374
Open issues
54
Last commit (this folder)
Oct 24, 2026 (5 days ago)
Last release
Oct 21, 2026 (8 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
AGPL-3.0
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from RunMaestro

Cite this page

CLAUDE.md · RunMaestro/Maestro. Agent Mart. https://agentmart.dev/templates/runmaestro-maestro-claude-md (last updated 2026-10-24)