TTSR: rule-driven stream interruption (Time-Traveling Stream Rules) - #1273
TTSR: rule-driven stream interruption (Time-Traveling Stream Rules)#1273chr1syy wants to merge 52 commits into
Conversation
Adds the static foundation for Time-Traveling Stream Rules: - TTSR_CONFIG_PATH / TTSR_RULES_DIR / ttsrRuleFilePath() in the shared maestro-paths module (no scattered path strings). - src/shared/ttsr-types.ts: serializable rule schema, the four validated enums, project settings, and the Gate A per-agent capability matrix (verified per parser, not inferred from agents/capabilities.ts). - src/main/ttsr/config/: repository (single fs owner of .maestro/rules and .maestro/ttsr.yaml, chokidar watch), normalizer (js-yaml frontmatter, regex compile-check, enum validation, Gate A agent defaulting), and the loader facade with name-collision first-wins. - 13 unit tests covering normalization, invalid-regex drop, shadowing, enum fallback, disabledRules, missing/unparseable/invalid configs. Also vendors the authoritative plan at plans/ttsr-implementation-plan.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gin, settings section Completes Phase 1 of plans/ttsr-implementation-plan.md: - 4-file settings pipeline for the `ttsr` group (ttsrEnabled, ttsrDisabledRules, ttsrContextMode, ttsrBuiltinRules) across settingsMetadata.ts, main/stores/defaults.ts, settingsStore.ts (state + setters + validated hydrate) and useSettings. - `ttsr` Encore flag on EncoreFeatureFlags + TTSR_FIRST_PARTY_PLUGIN (com.maestro.ttsr) registered in FIRST_PARTY_PLUGINS, so the FirstPartyEncoreFlag compile-assert covers it. Marked beta in the marketplace tile list. - TtsrSettingsSection (chromeless extension-detail body) wired into EncoreTab's settingsBodies with the data-setting-id="encore-ttsr" wrapper, plus the matching searchableSettings registry entry. - isTtsrContextMode() guard in shared/ttsr-types.ts, used by the settings-store hydrate path. npm run lint (all 3 tsconfigs), eslint, prettier and the touched suites (Settings 488, plugins/cli/ttsr/settingsStore 542) are green.
Adds the detection half of Phase 2 as a standalone, injectable subsystem: - ttsr-state-store.ts: main-authoritative repeat/injection state (Gate B), keyed by (maestroSessionId, providerSessionId, ruleName), with a pending bucket folded in once the session-id event lands, plus snapshot/hydrate for the Phase 3 persistence seam. - ttsr-matcher.ts: pure predicates - agents gate, scope narrowing, picomatch glob path gate (relativizes absolute paths, normalizes separators), regex evaluation, and the interruptMode x source classification table. - ttsr-tool-extract.ts: edit/write snapshots from both parser event shapes (toolUseBlocks and toolName + toolState.input), including codex patch additions recovery per Gate A's partial AST support. - ttsr-manager.ts: the per-session matcher - prose/thinking buffers with boundary overlap, tool-content matching, repeat policy, interrupt vs deferred buckets, and the ttsr:matched payload. Dependencies are injected so the process manager never imports TTSR directly. 67 tests green across the TTSR suite; all three tsconfigs and eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the Phase 2 detection core into the live agent stream. - ParsedEventObserver seam in StdoutHandler.handleParsedEvent, right after parseJsonObject: the only place that sees partial prose (dropped by the public thinking-chunk/data events for several agents), thinking, and tool calls with full input payloads for every structured agent. Injected via ProcessManager.setParsedEventObserver so process-manager keeps no TTSR import. A throwing observer is contained and reported, never taken out on the agent's output stream. - TtsrSpawnRegistry: main-authoritative record of each in-flight turn (agent, project root, original prompt, provider session id), fed from the public spawn / session-id / exit events. ManagedProcess holds no prompt, so this is Phase 3's only source for originalGoal. - TtsrRuntime: composes registry + rule cache + TtsrManager, reads the ttsrEnabled AND encoreFeatures.ttsr gate live, and reports ttsr:matched. A broken rule directory degrades to "no rules", never to a broken stream. - installTtsrRuntime() called once from main; the OpenCode SDK spawn path now emits the spawn event too, so its turns are observed (and agent-run capture no longer misses them). 18 new tests; three tsconfigs, eslint, and the process-manager / agent-run / opencode-server / process-listeners suites green.
Completes Phase 2 by wiring the AST half of the matcher: - `ttsr-ast.ts`: lazy-loaded `@ast-grep/napi` matcher (same lazy-require degradation pattern as `@napi-rs/keyring`), plus the extension -> grammar map. Only the five grammars bundled with the core napi package are supported (ts/tsx/js/jsx/css/html); other file types skip AST rather than guess, since Maestro does not ship dynamic grammars. - `TtsrManager.observeAst`: async structural pass over edit/write snapshots, sharing the agents gate, glob path gate, repeat policy and interrupt/defer buckets with the regex path. Identical consecutive snapshots are skipped. - `TtsrManager.needsAstCheck`: sync gate so the stream tap allocates no promise for AST-free turns. - `TtsrRuntime`: drives the AST pass off the synchronous path and exposes `flushAst()` so Phase 3 (and tests) can settle it deterministically. - packaging: `@ast-grep/napi` added to `asarUnpack` next to the other native modules (`npmRebuild: false` holds - napi-rs prebuilds are ABI-stable). Verified: 399 tests green (ttsr + process-manager), tsc clean on all three configs, eslint clean.
…d payload Turns a detected interrupting match into a real abort: signal the in-flight process (interrupt for contextMode keep, kill for discard), wait for its exit so stdout is drained, then emit the ttsr:triggered payload the renderer needs to respawn the corrective turn. - ttsr-injection.ts: <system-interrupt>/<system-reminder> rendering, per-rule dedupe, attribute escaping, and the degraded fresh-mode goal restatement. - ttsr-interrupt-driver.ts: abort lifecycle, ttsrAbortPending flag, late-match folding, exit timeout, and the Gate A resume-vs-degraded split. - TtsrRuntime drains the manager's interrupt bucket after every sync observation and after each AST pass settles; noteExit runs before the spawn registry entry is dropped so the payload keeps its meta. - TtsrTriggeredPayload added to shared/ttsr-types. 25 new tests; 417 TTSR + process-manager tests green, tsc clean on all three configs.
…ive respawn Closes the interrupt loop: main aborts the offending turn, the renderer continues the conversation with the <system-interrupt> prompt. - ttsr:abortPending push (new, emitted before the signal) so the exit listener treats the abort as a TTSR interruption instead of a failed turn - it would otherwise idle the tab and dequeue onto the same process id before the corrective spawn lands. - src/main/preload/ttsr.ts push-event bridge (onAbortPending/onTriggered/ onMatched), registered in preload/index.ts and typed in global.d.ts. - ttsrStore: renderer display cache + the ttsrAbortPending flag (Gate B - main stays authoritative). - ttsrRespawn: resolveTtsrTarget via the shared session-id parser and buildTtsrRespawnConfig, which reuses the normal spawn rules (permission mode, YOLO filtering, per-session overrides, SSH, Windows stdin) and takes providerSessionId from the payload, not the tab cache. - useTtsr, mounted in App.tsx behind the ttsr Encore flag, runs the corrective turn and marks the tab busy with a transcript notice. 211 tests green (TTSR suites + agent listeners), tsc clean on all three configs.
Non-interrupting matches queue a <system-reminder> main-side; the spawn path now drains that queue and prepends it to the conversation's next prompt (Maestro has no tool-result hook to fold guidance in-band). - TtsrRuntime.takeDeferredReminders() renders + clears the queue, gated on the global TTSR switch. - applyTtsrReminders() applies it in handle-spawn via an injected drain, so the process handlers keep no TTSR import. - Bound the manager's retained state: reminders are capped per conversation and a turn that ends with an empty queue drops its session state (Auto Run mints a fresh session id per task).
`once` and `after-gap` only mean anything if they outlive the process, so the main-authoritative state store now round-trips through disk: - TtsrStateStore stamps `updatedAt` per conversation and fires an `onChange` callback on every mutation (hydrate deliberately does not). - New ttsr-state-persistence.ts owns the `ttsr-state` electron-store namespace behind a swappable backend: debounced writes, TTL (30d) plus a 500-conversation cap prune on read and write, and disk errors logged and swallowed so a failing write never breaks the output stream. - TtsrRuntime builds the state store, hydrates it before the first observation, and exposes flushState()/dispose(); installTtsrRuntime wires the real backend (pass `persistence: null` for in-memory). - main/index.ts flushes on `will-quit` so a rule that fired seconds before shutdown is still remembered next launch. 137 TTSR tests green, tsc clean across lint/main configs.
An interrupted turn was silent unless the user happened to be watching that tab. The interrupt path now raises a sticky orange toast on the existing remote:notifyToast channel (no new notification primitive), unwrapping the composite process id to the bare agent id so the renderer can resolve the agent and jump to the interrupted tab. ttsr:triggered is sent first: the corrective respawn should already be in flight before the user is told about it. Toast failures are logged, never thrown - the reinject matters more. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runs each in-scope agent's real provider stdout through its real parser, the runtime tap, the matcher and the interrupt driver, then records the prose / ast / resume axes from what the pipeline actually did and compares that with the plan's scope table and TTSR_AGENT_CAPABILITIES. - claude-code, codex, opencode, factory-droid: pass (clean resume) - copilot-cli, grok: degraded fresh reinject, as Gate A specifies - terminal: excluded, the spawn registry never registers it - feature-off is a total no-op per agent, rules are not even read Also corrects the StdoutHandler tap comment: the observer is always installed and gates itself live, rather than being installed conditionally.
Eight findings from reviewing the goal-driven Auto Run output. Blocking: - Only AI-tab spawns are registered. TTSR aborts in main but can only respawn into an AI tab, so registering Auto Run tasks, synopsis, tab naming or group-chat participants let it kill an unattended turn it could never restart. Adds parseAiTabSpawnId() alongside the existing coworking regex; tabId now comes from the spawn id (no caller sets config.tabId). - ttsrContextMode had no consumer. TtsrProjectSettings.contextMode is now optional so "unset" differs from "keep", resolving project yaml -> global setting -> keep. - The corrective respawn dropped the Auto Run read-only gate, so a rule firing during a non-worktree run handed the new turn write access the aborted one lacked. Mirrors useInputProcessing, forced-parallel exempt. Correctness: - Carry the original goal across a corrective respawn, so a second interrupt on a degraded agent restates the user's request rather than the previous injection. - Bound interrupts per conversation (MAX_TTSR_INTERRUPTS, persisted). Past it, matches defer to reminders instead of killing turns forever. - Drop the 5s rule cache TTL: loading is synchronous disk IO and ran inside StdoutHandler. Rules load at spawn time and invalidate via the watcher that was already written but never wired. - A throwing interrupt()/kill() left abortPending set forever, wedging the tab busy. The driver now withdraws over a new ttsr:abortCleared channel, which is what clearAbortPending existed for. Also removes the unused ttsrBuiltinRules setting and endTurn's dead finalText param, and fixes a pre-existing failure in the first-party plugin registry test (never updated when com.maestro.ttsr landed). The acceptance matrix report now states that its evidence is an in-process run with a stubbed ProcessManager, not a live per-agent E2E. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Stop the agent doing X" was mostly not expressible. The scope vocabulary covered prose and file content only, and extractEditSnapshots classified just the file-mutating tools, so shell calls were invisible to TTSR. A rule like "never force-push" could only match the agent talking about it, which fires on "I won't force-push" and misses the actual command. Adds a tool:bash scope matching the command a shell tool is about to run, sourced from Bash (claude-code), shell (codex), bash (opencode) and the run_* spellings, with argv arrays joined into one matchable line. Gate A gains a shellEvents axis; factory-droid and grok report no tool calls, so the loader keeps command rules off them and says why. globs now gate only the file-bearing scopes. A command has no path, so a bash rule with globs previously matched nothing at all; it is now ignored with a load-time warning rather than silently never firing. The acceptance matrix measures the new axis per agent from real parser output, so a parser that stops surfacing commands fails the suite instead of leaving those rules quietly inert. Renames the extractor to extractToolSnapshots/TtsrToolSnapshot now that it covers more than edits. Interrupting stays corrective, not preventive: the command is matched as the tool call streams, so a fast one may already have run. Documented in the acceptance record rather than implied away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rules were already project-scoped on disk but had no UI, so the only way
to add one was to hand-write a file the user had no way to discover.
Adds a Rules tab to the Right Bar, which is implicitly scoped to the
agent being viewed: its cwd is the project root every call names. That
is why this is not in Settings, which is global and cannot express "in
this repo". The tab lists the project's rules, surfaces load warnings
(the only signal a rule that can never fire otherwise gives), and
exposes the per-project `enabled` and `contextMode` from
.maestro/ttsr.yaml. Leaving contextMode unset hands the choice back to
the global setting rather than pinning it.
Authoring is delegated to the agent instead of a form. Rule files are
markdown and the agent already writes files, so a new prompt teaches it
the schema and the tab sends that brief plus the user's request as a
normal turn; the agent writes the file, the watcher notices, the list
refreshes. Nothing to outgrow, and the user can iterate conversationally
("narrower", "it fires too often"). Settings writes merge rather than
overwrite so a hand-written config survives a toggle.
Also fixes a bug this path would have hit constantly: a rule file
necessarily contains the text its own rule looks for, so writing
.maestro/rules/no-console-log.md tripped a tool:write console.log rule.
Verified before fixing, now covered. TTSR no longer matches its own
config at the one extraction choke point.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s, bash self-trip - Interrupt budget is charged once per real abort: matches that drain while an abort is already pending are folded, not gated and charged again. - A withdrawn abort refunds the charge, re-arms the matched rules and re-queues their guidance as deferred reminders; a corrective turn that never spawns is recovered on the next spawn of that session. - Conversation state keys on the parsed tab identity, so a forced-parallel spawn id no longer mints a fresh conversation per turn. - Shell commands that write TTSR's own config no longer trip tool:bash rules. - Deferred reminders drain transactionally: the queue is cleared only after the spawn succeeds, so a throwing spawn no longer destroys queued guidance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s panel Renderer robustness: - useTtsr gates the corrective respawn behind useOwnedSessionGate. TTSR pushes are broadcast to every window and web-desktop client, so two renderers were both respawning; the second spawn killed the first mid-flight and could double the <system-interrupt> in the transcript. Non-owning windows still record display state. - A failed respawn now releases the whole session, not just the tab. The aborted turn's exit is suppressed by the abort-pending flag, so nothing else could clear session.state / busySource / thinkingStartTime - the agent span forever with queue dispatch blocked. releaseAfterFailedRespawn() idles both, clears the abort mark, and toasts red with the rules and the spawn error. - Abort-pending marks can no longer suppress exits forever: entries carry a timestamp, isTtsrAbortPending() drops anything older than 30s, and useTtsr's cleanup wipes standing marks on unmount / when the Encore flag flips off. Rules panel: - New ttsr:rulesChanged push (runtime -> safeSend -> preload -> service -> panel, debounced 300ms) so the agent-driven authoring loop ends on a fresh list instead of a stale one. - listRules returns every rule with a `disabled` flag instead of hiding disabled ones; the panel dims them and adds an always-visible on/off toggle that writes the project ttsr.yaml or clears the global ttsrDisabledRules setting, whichever holds the rule. The matcher still only ever sees enabled rules - loadTtsrConfigDetailed.rules is unchanged and the new disabledRules list is for the management surface alone. - Delete uses the codebase's existing two-step arm pattern (4s auto-disarm) instead of firing on one click in a hover cluster. App: - handleSendPromptToAgent toasts yellow when no agent input is available instead of silently dropping the click. No existing test renders App.tsx or covers this path, so the added coverage is panel-side only: the Rules panel does not offer the hand-off when no handler is wired. Suites run: useTtsr, TtsrRulesPanel, services/ttsr, ttsrRespawn, ipc/handlers/ttsr (74 green), plus ttsr-runtime (38) and ttsr-config-loader (18) to confirm matcher-side filtering is unchanged.
Coverage gaps closed:
- Two concurrent aborts through the driver's `pending` map: out-of-order
exits, per-session payloads, late matches folding into their own session
only, and a session that merely streams while another aborts. Same two
shapes again at runtime level.
- The provider session id landing DURING the wait-for-exit window, which
upgrades the pending abort from `fresh` to `resume`; plus the inverse
guard for an id arriving after the payload was built.
- Config-watcher churn (a burst of rule writes coalescing into one reload of
a consistent final rule set), a rule-cache invalidation between two
observes of one turn, and `watchConfig` throwing.
Production fix found by those tests: `watchTtsrConfigFiles` handed chokidar
`.maestro/ttsr.yaml` as a watch target, and chokidar 3 watches NOTHING when a
listed path inside a dot-directory does not exist yet - one missing path
poisons its siblings. Projects with rules but no `ttsr.yaml` (a valid, and the
common, setup) never saw a rule edit at all. The watch is now anchored on the
`.maestro` directory with an `ignored` predicate of identical scope.
Regex safety for repo-supplied rules:
- The normalizer refuses patterns with a quantified group over an unbounded
body ((a+)+, (x*)+, (\d+){2,}) with a load warning, like an invalid regex.
- `findRegexMatch` caps its input at TTSR_MAX_SCAN_CHARS (32KB). Prose was
already bounded by the manager's buffer; tool payloads were not.
- Trust model written down in `ttsr-rule-authoring.md` and as fidelity gap 6:
rule bodies are repo-controlled prompt injection, same class as cue.yaml;
regexes are case-sensitive with no flags surface.
Dead surface and caps:
- Removed `TtsrRuntime.isAbortPending` (pure wrapper, no production caller)
and `TtsrInterruptDriver.noteExit`'s ignored boolean return.
- KEPT `flushAst` / `flushInterrupts`: test-only, but the only signal a test
can await for the runtime's two async paths. Their docs now say so.
- `globMatcherCache` bounded at 100 with oldest-first eviction; the state
store prunes its in-memory conversations on the same 30-day/500 policy as
disk (the two constants now live in the store, which persistence imports).
Corrective-turn recognition no longer reads the prompt: an optional
`ttsrCorrelationId` rides the triggered payload, the renderer's respawn
config, and the spawn event into the registry, with the old `endsWith` check
kept as a fallback for a spawn that carries no id.
Merge prep: upstream/rc is at HOST_API_VERSION 1.14.0 and this branch never
touched host-api.ts or the SDK (only `first-party.ts`, additively), so there
is no version to re-bump; likewise the settings-placed `PluginPanelSlot`
already lives in DisplayTab at the merge base. Both recorded in the
acceptance matrix rather than acted on.
- Create .maestro before arming the rule watcher: chokidar cannot watch a missing anchor and the runtime never re-arms, so a fresh repo's first agent-authored rule was invisible until app restart. - Refuse the corrective respawn in web-desktop clients outright: their ownership predicate is a permit-all, so a connected browser tab raced the desktop window into a duplicate spawn. - Release only the interrupted tab on a failed respawn; the session stays busy while a sibling tab is still mid-turn (same rule as the exit listener). - Guard TtsrRulesPanel.refresh against out-of-order list responses so an agent switch cannot surface (or write back) the previous project's rules. - Document why registry.clear() leaves pendingCorrective alone (the hung-process exit lands before the respawn), stop overclaiming what the nested-quantifier gate guarantees, and validate the initial ttsrDisabledRules snapshot like its change listener does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughTTSR adds configurable stream rules with regex and AST matching, deferred reminders, interrupt-and-reinject handling, persistence, IPC and preload APIs, renderer settings and Rules UI, agent capability gating, and extensive runtime, acceptance, and UI tests. ChangesTTSR implementation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds rule-driven interruption and corrective respawning for live agent streams. The main changes are:
Confidence Score: 4/5Regex matching and tool snapshot filtering need fixes before merging.
src/main/ttsr/ttsr-matcher.ts, src/main/ttsr/config/ttsr-config-normalizer.ts, src/main/ttsr/ttsr-tool-extract.ts
|
| Filename | Overview |
|---|---|
| src/main/ttsr/ttsr-runtime.ts | Coordinates rule caching, stream observation, asynchronous AST matching, interrupt budgets, lifecycle events, and reminder draining. |
| src/main/ttsr/ttsr-interrupt-driver.ts | Adds abort signaling, late-match folding, exit waiting, corrective payload creation, and withdrawal cleanup. |
| src/main/ttsr/ttsr-matcher.ts | Adds agent, scope, glob, and regex matching, but ignores tool content after the first 32 KB. |
| src/main/ttsr/config/ttsr-config-normalizer.ts | Adds rule normalization and regex validation, but permits exponential overlapping-alternation patterns. |
| src/main/ttsr/ttsr-tool-extract.ts | Normalizes tool payloads and suppresses self-authored configuration writes, with an overly broad path check. |
| src/main/ttsr/config/ttsr-config-repository.ts | Centralizes contained rule CRUD and project rule-file watching. |
| src/renderer/hooks/useTtsr.ts | Adds renderer ownership checks, corrective respawning, and failed-respawn cleanup. |
| src/shared/ttsr-types.ts | Defines the rule, capability, lifecycle payload, and persisted-state contracts used across processes. |
Sequence Diagram
sequenceDiagram
participant Agent
participant Stdout as StdoutHandler
participant Runtime as TTSR Runtime
participant Matcher
participant Driver as Interrupt Driver
participant Renderer
participant Spawn as Spawn Path
Agent->>Stdout: Parsed stream event
Stdout->>Runtime: observe(sessionId, event)
Runtime->>Matcher: Evaluate rules
alt Interrupting match
Matcher-->>Runtime: Pending interrupt
Runtime->>Driver: Trigger abort
Driver->>Agent: Interrupt or kill
Agent-->>Driver: Exit
Driver->>Renderer: Corrective payload
Renderer->>Spawn: Respawn request
Spawn->>Agent: system-interrupt prompt
else Deferred match
Matcher-->>Runtime: Queue reminder
Spawn->>Runtime: Peek reminders on next prompt
Runtime-->>Spawn: system-reminder block
Spawn->>Agent: Reminder and user prompt
Spawn->>Runtime: Commit reminder drain
end
Reviews (1): Last reviewed commit: "ttsr: review fixes - fresh-project watch..." | Re-trigger Greptile
…ths, wider regex gate - Normalize separators in the rule watcher's ignored predicate: anymatch posix-normalizes every candidate path before calling a function matcher, so comparing against path.join-built strings ignored everything on Windows and the watcher never fired (all three Windows CI failures). - Anchor the TTSR config carve-out to the project root: a nested .maestro/rules/ (fixture, vendored repo) is ordinary content again, and an absolute path only counts as TTSR config inside this project's own .maestro. Threads ctx.cwd through extractToolSnapshots. - Extend the backtracking gate to refuse overlapping alternation under a quantifier ((a|aa)+x, (\d|\w)+!) via first-character overlap, alongside the nested-quantifier shape. - Scan oversized tool payloads in bounded 32KB windows with 1KB overlap instead of truncating: a rule hit at the end of a large file now fires, while no single regex evaluation ever sees more than the ceiling. - Run prettier over plans/ttsr-implementation-plan.md (lint-and-format leg). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/renderer/utils/ttsrRespawn.ts (1)
88-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose a
forcedParallelflag fromparseSessionIdand use it here. The current/-fp-\d+$/check duplicates the parser’s suffix handling and can drift; reuse a parser field instead of rechecking the suffix locally.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/utils/ttsrRespawn.ts` around lines 88 - 92, Update parseSessionId to return a forcedParallel flag derived from its existing suffix parsing, then use that field in the isForcedParallel assignment within the read-only decision flow. Remove the local /-fp-\d+$/ test so suffix detection has a single source of truth.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/__tests__/main/ttsr/ttsr-state-store.test.ts`:
- Around line 145-146: Remove the dead assertion for the nonexistent `prov-x`
key in the pruning test. Keep the existing `getMessageCount(...)` coverage for
newest records and the valid oldest-record assertion, unless replacing it with
an assertion targeting an actually evicted key is necessary.
In `@src/main/ttsr/ttsr-interrupt-driver.ts`:
- Around line 252-275: Update canResume in buildPayload to safely handle agent
IDs absent from TTSR_AGENT_CAPABILITIES by using an optional-chaining guard when
reading the resume capability. Preserve the existing clean-capability and
providerSessionId checks, defaulting missing entries to mode: 'fresh' without
throwing.
In `@src/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsx`:
- Around line 93-107: Reset project-specific panel state whenever projectRoot
changes: clear data, disarm armedDelete, and cancel the existing disarm timer
before starting or allowing the new refresh. Update the relevant useEffect/state
logic around refresh and the delete-arm timer so stale rules are not displayed
and an armed deletion cannot carry across projects.
- Around line 174-182: Update authorRule to accept an optional clearDraft flag
defaulting to false, and only call setRequest('') when that flag is true. Pass
true from the compose submission and onKeyDown paths that invoke
authorRule(request), while leaving the per-rule edit button invocation at its
default so it preserves the unrelated draft.
- Around line 391-433: Update the controls wrapper in TtsrRulesPanel around the
onOpenFile, onSendToAgent, and removeRule buttons to reveal the action cluster
when any contained button receives keyboard focus, while preserving the existing
hover behavior. Use a focus-visible descendant variant or equivalent styling so
keyboard users can see the controls without changing their actions.
---
Nitpick comments:
In `@src/renderer/utils/ttsrRespawn.ts`:
- Around line 88-92: Update parseSessionId to return a forcedParallel flag
derived from its existing suffix parsing, then use that field in the
isForcedParallel assignment within the read-only decision flow. Remove the local
/-fp-\d+$/ test so suffix detection has a single source of truth.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: caad8d63-7649-402b-885b-79d0947ea196
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (87)
package.jsonplans/ttsr-acceptance-matrix.mdplans/ttsr-implementation-plan.mdsrc/__tests__/helpers/resetStores.tssrc/__tests__/main/ipc/handlers/process/apply-ttsr-reminders.test.tssrc/__tests__/main/ipc/handlers/ttsr.test.tssrc/__tests__/main/ttsr/ttsr-acceptance-matrix.test.tssrc/__tests__/main/ttsr/ttsr-ast.test.tssrc/__tests__/main/ttsr/ttsr-config-loader.test.tssrc/__tests__/main/ttsr/ttsr-interrupt-driver.test.tssrc/__tests__/main/ttsr/ttsr-manager.test.tssrc/__tests__/main/ttsr/ttsr-matcher.test.tssrc/__tests__/main/ttsr/ttsr-notify.test.tssrc/__tests__/main/ttsr/ttsr-runtime.test.tssrc/__tests__/main/ttsr/ttsr-state-persistence.test.tssrc/__tests__/main/ttsr/ttsr-state-store.test.tssrc/__tests__/main/ttsr/ttsr-tool-extract.test.tssrc/__tests__/renderer/components/RightPanel.test.tsxsrc/__tests__/renderer/components/Settings/Extensions/extensionModel.test.tssrc/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsxsrc/__tests__/renderer/components/TtsrRulesPanel.test.tsxsrc/__tests__/renderer/hooks/useTtsr.test.tssrc/__tests__/renderer/services/ttsr.test.tssrc/__tests__/renderer/utils/ttsrRespawn.test.tssrc/__tests__/setup.tssrc/__tests__/shared/pianola/pianola-first-party-plugin.test.tssrc/main/coworking/coworking-session-id.tssrc/main/index.tssrc/main/ipc/handlers/index.tssrc/main/ipc/handlers/process.tssrc/main/ipc/handlers/process/apply-ttsr-reminders.tssrc/main/ipc/handlers/process/handle-spawn.tssrc/main/ipc/handlers/process/spawn-types.tssrc/main/ipc/handlers/ttsr.tssrc/main/preload/index.tssrc/main/preload/ttsr.tssrc/main/process-manager/ProcessManager.tssrc/main/process-manager/handlers/StdoutHandler.tssrc/main/process-manager/spawners/ChildProcessSpawner.tssrc/main/process-manager/spawners/OpencodeServerSpawner.tssrc/main/process-manager/types.tssrc/main/stores/defaults.tssrc/main/ttsr/config/ttsr-config-loader.tssrc/main/ttsr/config/ttsr-config-normalizer.tssrc/main/ttsr/config/ttsr-config-repository.tssrc/main/ttsr/index.tssrc/main/ttsr/ttsr-ast.tssrc/main/ttsr/ttsr-injection.tssrc/main/ttsr/ttsr-interrupt-driver.tssrc/main/ttsr/ttsr-manager.tssrc/main/ttsr/ttsr-matcher.tssrc/main/ttsr/ttsr-notify.tssrc/main/ttsr/ttsr-runtime.tssrc/main/ttsr/ttsr-spawn-registry.tssrc/main/ttsr/ttsr-state-persistence.tssrc/main/ttsr/ttsr-state-store.tssrc/main/ttsr/ttsr-tool-extract.tssrc/prompts/ttsr-rule-authoring.mdsrc/renderer/App.tsxsrc/renderer/components/RightPanel.tsxsrc/renderer/components/Settings/Extensions/extensionModel.tssrc/renderer/components/Settings/searchableSettings.tssrc/renderer/components/Settings/tabs/EncoreTab/EncoreTab.tsxsrc/renderer/components/Settings/tabs/EncoreTab/components/TtsrSettingsSection.tsxsrc/renderer/components/Settings/tabs/EncoreTab/components/index.tssrc/renderer/components/Settings/tabs/EncoreTab/hooks/index.tssrc/renderer/components/Settings/tabs/EncoreTab/hooks/useTtsrSettingsState.tssrc/renderer/components/Settings/tabs/EncoreTab/types.tssrc/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsxsrc/renderer/components/TtsrRulesPanel/index.tssrc/renderer/global.d.tssrc/renderer/hooks/agent/internal/useAgentExitListener.tssrc/renderer/hooks/batch/useAutoRunHandlers.tssrc/renderer/hooks/props/useRightPanelProps.tssrc/renderer/hooks/settings/useSettings.tssrc/renderer/hooks/useTtsr.tssrc/renderer/services/ttsr.tssrc/renderer/stores/batchStore.tssrc/renderer/stores/settingsStore.tssrc/renderer/stores/ttsrStore.tssrc/renderer/types/index.tssrc/renderer/utils/ttsrRespawn.tssrc/shared/maestro-paths.tssrc/shared/plugins/first-party.tssrc/shared/promptDefinitions.tssrc/shared/settingsMetadata.tssrc/shared/ttsr-types.ts
…tion, a11y reveal - Reset TtsrRulesPanel project-scoped state on projectRoot change: clear the listed rules and DISARM a pending delete, whose carried-over relative path could otherwise delete the same-named rule in the next project. - Only clear the compose draft on the compose-send path; the per-rule "edit this rule" hand-off sends a fixed instruction and now leaves an unrelated draft intact. - Reveal the per-rule hover controls on group-focus-within so keyboard navigation can see the buttons it is activating. - Optional-chain the Gate A capabilities lookup in buildPayload so an agent id with no entry fails safe to a fresh turn on the respawn path. - Replace a dead pruning assertion (prov-x key never existed) with an actually-evicted key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Web-desktop clients never spawn the TTSR corrective turn (the desktop primary window does), which left the web transcript stopping mid-sentence with no boundary marker. Add webInterruptionNotice() and a web branch in the onTriggered handler that appends a system-log notice to the target tab without flipping state to busy or spawning. The mirrored process:* events from the desktop-spawned turn still drive the visible streaming. Phase 1, task 1 of FIX-D2-01. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rose
buildTtsrToast now describes only the detection ("Rules X fired; the
turn was interrupted.") and attaches a structured ttsr marker
{ mode: 'resume' | 'fresh' } instead of baking the resume/restart
outcome sentence into the message. The plain message stays a sensible
fallback for clients that ignore the marker. The display layer resolves
the client-specific outcome line at render time (follow-up task), so a
single broadcast payload reads correctly on both the desktop renderer
and web-desktop clients.
- Add shared TtsrToastMarker type in src/shared/ttsr-types.ts
- TtsrToastParams gains required ttsr field; buildTtsrToast sets it
- notificationStore Toast type gains additive optional ttsr?: marker
…e line Toast.tsx now resolves the TTSR interrupt outcome sentence at display time from the structured ttsr marker: web-desktop clients read "Correction runs in the desktop app." while the desktop renderer keeps "Resuming with corrective guidance." (resume) / "Restarting the turn with corrective guidance." (fresh). Non-TTSR toasts fall back to the plain message unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r + detection-only prose Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-count guard Re-adds the optional textAlreadyStreamed?: boolean flag on ParsedEvent, orphaned when this branch was rebased onto feat/ttsr. Marks a complete assistant text event whose prose was already delivered token-by-token via stream_event deltas so downstream consumers (streamedText append, thinking-chunk emit, TTSR prose buffer) skip re-ingesting it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Handle type:"stream_event" objects (--include-partial-messages): - content_block_delta/text_delta -> partial text event - content_block_delta/thinking_delta -> partial text + isReasoning - all other SSE kinds and empty deltas -> null (no downstream spam) Track prose deltas via a sawStreamedProseDelta instance marker; stamp the closing assistant event textAlreadyStreamed: true so downstream consumers skip re-ingesting already-streamed prose. No-delta turns behave exactly as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skip both the thinking-chunk emit and the streamedText append for a claude-code assistant event stamped textAlreadyStreamed:true, since its prose was already delivered token-by-token via stream_event text_delta partials. Raw deltas carry no flag and still flow through, so exit-fallback text (result event on normal completion, streamedText on mid-turn abort) stays identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…adyStreamed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…id-turn abort Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
liveProse:true is now genuinely verified at token granularity via --include-partial-messages + stream_event text deltas, so document that claude-code prose-only turns are preventable (real mid-turn abort), not just corrective, and note the textAlreadyStreamed double-count guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an optional ttsr marker to LogEntry, appends a badged source:'user' injection entry in runTtsrCorrectiveTurn, and renders a TTSR badge in LogItem with the <system-interrupt> XML collapsed by default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a stream_event describe block to the claude-output-parser spec asserting the new TTSR mid-turn abort behavior: text_delta -> partial text, thinking_delta -> partial reasoning text, all other SSE kinds return null, the closing assistant event is stamped textAlreadyStreamed after prose deltas (marker resets per message), and delta-free turns behave exactly as before. 73 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ose guards Assert the double-count guards for claude-code token-level deltas: - StdoutHandler: streamed prose accumulates once, thinking-chunk fires only for deltas, and the flagged closing assistant event is skipped; no-delta turns behave as before. Driven by the real ClaudeOutputParser. - TtsrManager: a mid-message rule fires on the streamed delta, not the closing assistant event; delta prose is buffered exactly once; tool snapshots still evaluate on a flagged event. - useTtsr: the corrective turn now records both the gray system abort line and the badged source:'user' injection entry carrying the ttsr marker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts (1)
2568-2591: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winClosing assistant event in this test never carries
isReasoning, so the "no extra thinking-chunk" assertion doesn't test the guard it claims to.
assistantMessage()(Line 2537-2543) only builds acontent: [{type:'text', ...}]block, never athinkingblock. Per the parser'stransformMessagelogic,isReasoningis derived fromthinkingText.length > 0, so the closingassistantMessage('Answer.')in this test always hasisReasoning: undefinedregardless oftextAlreadyStreamed. SinceStdoutHandleronly emitsthinking-chunkfor claude-code whenevent.isReasoningis true, the assertionexpect(thinkingSpy).toHaveBeenCalledTimes(2)after Line 2588 would pass even if thetextAlreadyStreamedskip were removed entirely - it isn't exercising the double-count guard on the thinking-chunk path, only onstreamedText.Consider extending
assistantMessage(or adding a variant) to include athinkingcontent block mirroring the deltas, so the flagged event actually carriesisReasoning: trueand the test genuinely proves the guard suppresses the extrathinking-chunkemission.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts` around lines 2568 - 2591, Update the test helper assistantMessage (or add a dedicated variant) so the closing assistant event includes a thinking content block matching the earlier thinking deltas, causing transformMessage to set isReasoning: true. Keep the existing assertions and verify that the textAlreadyStreamed guard suppresses an additional thinking-chunk as well as duplicate streamedText.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/components/TerminalOutput/components/LogItem.tsx`:
- Around line 1040-1055: Update the TTSR badge rendering in LogItem to map each
log.ttsr.rules object to its name, falling back to path, before joining the
values. Use the mapped display names consistently in both the tooltip title and
visible badge text, preserving the existing singular/plural rule wording.
- Around line 591-610: Scope the TTSR details branch in the LogItem rendering
logic to user messages, matching the badge condition by requiring log.source ===
'user' alongside log.ttsr. Keep the existing fallback rendering unchanged for
non-user entries, even when their log.ttsr value is truthy.
---
Nitpick comments:
In `@src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts`:
- Around line 2568-2591: Update the test helper assistantMessage (or add a
dedicated variant) so the closing assistant event includes a thinking content
block matching the earlier thinking deltas, causing transformMessage to set
isReasoning: true. Keep the existing assertions and verify that the
textAlreadyStreamed guard suppresses an additional thinking-chunk as well as
duplicate streamedText.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6c10ab28-4eb6-474d-89fb-f5044543953e
📒 Files selected for processing (13)
src/__tests__/main/parsers/claude-output-parser.test.tssrc/__tests__/main/process-manager/handlers/StdoutHandler.test.tssrc/__tests__/main/ttsr/ttsr-manager.test.tssrc/__tests__/renderer/hooks/useTtsr.test.tssrc/main/agents/definitions.tssrc/main/parsers/agent-output-parser.tssrc/main/parsers/claude-output-parser.tssrc/main/process-manager/handlers/StdoutHandler.tssrc/main/ttsr/ttsr-manager.tssrc/renderer/components/TerminalOutput/components/LogItem.tsxsrc/renderer/hooks/useTtsr.tssrc/renderer/types/index.tssrc/shared/ttsr-types.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/process-manager/handlers/StdoutHandler.ts
- src/renderer/types/index.ts
- src/renderer/hooks/useTtsr.ts
- src/tests/renderer/hooks/useTtsr.test.ts
- src/main/ttsr/ttsr-manager.ts
- src/shared/ttsr-types.ts
# Conflicts: # src/main/parsers/claude-output-parser.ts # src/renderer/types/index.ts
A tool-source file path that could not be relativized into the project root (no cwd, or the file lives outside it) was handed to project-relative glob patterns as-is. picomatch's leading ** consumes any directory prefix, so a rule globbed to '**/*.ts' matched every .ts file on the filesystem and a /tmp write could interrupt a rule the user scoped to their project. matchesGlobs now only lets an explicitly absolute pattern match a candidate that is still absolute after toGlobCandidate. In-project absolutes and absolute rule globs are unaffected.
…tests Lands the task-1 repro as permanent coverage so the reported mid-turn tool-scope no-op cannot silently return, and locks in the task-2 glob fix. - ttsr-acceptance-matrix: new claude-code block driving a full --include-partial-messages turn (stream_event framing, input_json_delta frames, complete assistant tool_use with an absolute file_path) through the real parser into TtsrRuntime under a globbed tool:write rule. Asserts a mid-turn interrupt for an in-project .ts write and no fire for an out-of-project /tmp write or a glob-excluded .md write. The runTurn harness now records onMatched payloads so source and file path are assertable. - ttsr-matcher: matchesGlobs coverage for absolute paths with and without cwd, plus explicitly absolute rule globs and mixed glob lists. - ttsr-manager: regression proving a glob-gated non-match never reaches the deferred queue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…user logs Resolves the CI transform failure and PR RunMaestro#1273 review threads. - StdoutHandler.test.ts: drop the duplicate ClaudeOutputParser import (merge artifact that made the whole suite fail oxc transform), and align the double-count guard assertions with RunMaestro#1289: claude-code prose partials drive the live thinking preview (fire thinking-chunk) while the flagged textAlreadyStreamed assistant event is still skipped, so prose is counted exactly once. The guard being tested is unchanged. - LogItem.tsx: scope the TTSR interrupt-details disclosure to user-source entries, mirroring the footer badge. The ttsr marker is only ever set on the user injection log, so a non-user entry carrying it now falls through to normal rendering instead of a bare disclosure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A TTSR tool-scope match fires at the assistant-message boundary, which on claude-code is after the CLI already ran the tool. The CLI then synthesizes its own 'the user doesn't want to proceed with this tool use' rejection into a transcript Maestro cannot edit, so the corrective turn believed the write never landed and left the forbidden content on disk (finding AC1). Option A: renderTtsrInterrupt prepends a preamble when any match has a tool source, stating that the effects are already applied, that any earlier rejection claim is incorrect, and that inspecting the affected files is the first step. Inform only, no auto-revert (AC1 decision 1). Prose-only interrupts render byte-identically to before. Option B: renderBlocks aggregates the distinct filePath values of every match folded into a rule and renders them as affected-files=, appended after the existing attribute trio. Bash matches contribute no path. New ttsr-injection.test.ts pins both halves plus the prose regression guard. ttsr-interrupt-driver.ts has zero diff.
On message-granularity agents (claude-code today) a tool-only or always interrupt fires after the tool call has executed, so the rule corrects the result rather than preventing it. Rule authors reading only the interrupt-mode description would reasonably expect prevention. Adds that one sentence to the TTSR_INTERRUPT_MODES doc comment and to the interruptMode row of the user-editable rule-authoring prompt, and sharpens the prompt's existing corrective bullet to say why: the tool call only reaches Maestro at the assistant-message boundary, so the write is already on disk. Comment and prose only. No schema, YAML key, or runtime behaviour changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A non-interrupting match (`interruptMode: never`) emitted `ttsr:matched` and nothing else - no abort, no toast, no transcript line - and NOTHING in the renderer subscribed, so the rule fired in total silence and read as broken. - ttsrStore: `matches` map keyed by `matchKey(projectRoot, rulePath)` with per-rule count, last timestamp, last source, last interrupt flag and last file path. Bounded by the number of rules, so no eviction; counts are per-renderer and since app start, not the persisted injection counts. - useTtsr: subscribe to `bridge.onMatched`, resolve the target tab and key the counts by its project root. Every client records (desktop, extra windows, web-desktop) because the store is per-renderer display state, so there is no ownership gate here. Guarded for older preloads and unsubscribed with the other three channels. - resolveTtsrTarget now takes the session id structurally instead of a whole `ttsr:triggered` payload; it only ever read `sessionId`. The Rules panel line that renders this lands in the next task.
…art 2) A rule with interruptMode: never fires with no toast, no transcript entry, and no other trace, so a user cannot tell it from a rule that is broken. Task 3 started counting ttsr:matched pushes in the renderer store; this renders them. - TtsrRulesPanel rule rows gain a third dim line, "N match(es) · last [interrupted] <relative time>", only when that rule has an entry for matchKey(projectRoot, rule.path). No entry renders nothing rather than "0 matches" on every row. - The "N rules" header gains a "counts since app start" suffix (plus a tooltip on it and on each match line) so the number is not mistaken for the persisted ttsr-state.json injection count. - The onMatched docstring in ttsr-runtime.ts and the ttsrStore module docstring both said the channel had no consumer; they now name the Rules panel match line. 4 scoped tests cover the rendered line, the interrupted wording, the project-root keying, and the empty-map case.
The interrupt toast is raised optimistically: main broadcasts `ttsr:triggered` and toasts before any renderer has spawned anything. Nothing ever checked that promise, so a corrective turn that never started left every client - web-desktop ones especially, since they never spawn and never see the desktop renderer's local failure toast - believing the turn was being fixed. Each interrupt now arms a 10s watchdog (`TtsrCorrectiveAckTracker`) keyed by the process id. The renderer that spawns acks over the new `ttsr:correctiveResult` channel once `processService.spawn` returns, which cancels it; an explicit failure or silence past the timeout broadcasts a sticky red "the corrective turn did not start - open the desktop app" toast to every client. The watchdog is wired only where a toast can be raised at all, superseded on re-arm, ignored for unknown or late acks, and dropped on runtime dispose. The preload method and its type are optional, so an older preload or web-desktop shim degrades to the timeout instead of crashing.
Brings the TTSR branch up to date with upstream's decomposition of settingsMetadata.ts, searchableSettings.ts, and the main-process IPC bootstrap. Six conflicts across four files, all additive on our side (147 insertions, 0 deletions against merge base 75a6243), so no upstream behaviour was overwritten. Conflict classes resolved: - (a) keep both sides - src/main/ipc/handlers/process/handle-spawn.ts: our ttsrReminders.commit() and upstream's OMP late-prime re-emit are independent post-spawn statements. - (b) relocate our addition into the module upstream now owns: - encore-ttsr entry -> searchableSettingsEncore.ts - ttsr: false plus ttsrEnabled / ttsrDisabledRules / ttsrContextMode -> settingsMetadataFeatures.ts - registerTtsrHandlers import + call and peekTtsrReminders -> src/main/ipc/bootstrap/index.ts, threaded through a new getTtsrRuntime: () => TtsrRuntime | null dep in bootstrap/types.ts (lazy getter, matching getCueEngine, because ttsrRuntime is assigned after setupIpcHandlers runs) - TTSR gate snapshot kept in src/main/index.ts module scope - cadenza ipcMain.on handlers dropped in favour of upstream's cadenza-bridge/ipc.ts (upstream's own relocation of merge-base code) - (c) genuine semantic overlap: none. Validation: all three tsc configs clean; scoped suites only (TTSR main+renderer 428 pass, Settings/process/parsers/RightPanel 1035, IPC + pianola 1785). One failure from upstream's new settingsStyleGuide no-double-dimming rule was fixed in the code (EncoreTab/components/TtsrSettingsSection.tsx), not the test. Prettier and ESLint clean on all touched files.
Second merge, needed because rc advanced by one commit (the context-gauge latch-at-0% fix) while the first merge was in CI, which flipped PR RunMaestro#1273 back to CONFLICTING. One conflict, class (a) pure addition on both sides, in src/main/parsers/claude-output-parser.ts: our ClaudeStreamRawEvent interface and upstream's new ClaudeCallUsage interface plus OccupancySnapshot type were declared at the same point in the file. Both kept, neither side discarded. Validation: all three tsc configs clean; scoped run of claude-output-parser.test.ts and StdoutHandler.test.ts (172 pass, 0 fail). Prettier and ESLint clean on the touched file.
One conflict, in src/renderer/components/Toast.tsx: pure addition on both sides. Upstream added the Z_LAYERS import for the toast container's z-index, this branch added the ttsrOutcomeLine helper. Both kept, both verified in use. Scoped tests: 351 passing across Toast and the TTSR suites. Three tsc configs, prettier clean.
What
TTSR (Time-Traveling Stream Rules) watches an agent's live output stream against project-scoped rules (
.maestro/rules/*.md+.maestro/ttsr.yaml) and, when a rule trips, aborts the turn and respawns it with a corrective<system-interrupt>block - so bad output stops the moment it appears instead of after the turn completes.Surfaced as a first-party plugin (
com.maestro.ttsr, category automation, beta) with an honest permission disclosure, riding the existingFirstPartyPluginBridgelifecycle: enable mints grants through the sealed ledger, revoke fails closed.Highlights
text,thinking,tool:edit,tool:bash, ...), glob targeting, and ast-grep structural matching (astCondition). Per-agent capability matrix (Gate A) keeps rules off agents that cannot surface the needed content.keepinterrupts,discardhard-kills), waits for the exit, then hands the renderer a corrective respawn payload recognized back by correlation id. Repeat policy, per-conversation interrupt budget (past it, guidance defers to the next prompt), and persisted repeat/injection state across restarts.<system-reminder>; draining is transactional (queue cleared only after the spawn actually happened).ttsr:rulesChanged), agent-driven authoring flow via a core prompt, two-step delete, per-project + global disable.Validation
src/__tests__/main/ttsr/, TTSR IPC handlers, reminder application,useTtsr, Rules panel, respawn config).npm run lint(tsc all configs + ESLint) clean; prettier clean.plans/ttsr-acceptance-matrix.mdmeasured per agent.Notes for review
encoreFeatures.ttsris the lifecycle flag keying the first-party plugin definition, same as the other marketplace-surfaced features..maestro/cue.yaml-class trusted content and the doc comments say so explicitly.🤖 Generated with Claude Code
Summary by CodeRabbit