Skip to content

TTSR: rule-driven stream interruption (Time-Traveling Stream Rules) - #1273

Open
chr1syy wants to merge 52 commits into
RunMaestro:rcfrom
chr1syy:feat/ttsr
Open

TTSR: rule-driven stream interruption (Time-Traveling Stream Rules)#1273
chr1syy wants to merge 52 commits into
RunMaestro:rcfrom
chr1syy:feat/ttsr

Conversation

@chr1syy

@chr1syy chr1syy commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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 existing FirstPartyPluginBridge lifecycle: enable mints grants through the sealed ledger, revoke fails closed.

Highlights

  • Detection: stream tap on parsed events; regex conditions with scopes (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.
  • Interruption: interrupt driver aborts the offending turn (keep interrupts, discard hard-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.
  • Deferred reminders: non-interrupting matches ride the conversation's next prompt as a <system-reminder>; draining is transactional (queue cleared only after the spawn actually happened).
  • Withdrawn aborts: refund the budget charge, re-arm the rules' cooldowns, re-file the guidance as reminders.
  • Authoring: Rules tab in the Right Bar (project-scoped, live re-list via ttsr:rulesChanged), agent-driven authoring flow via a core prompt, two-step delete, per-project + global disable.
  • Hardening: zero-cost disabled path (in-memory gate snapshot, no disk I/O on the stdout hot path), 32KB scan ceiling + nested-quantifier gate against hostile-repo regexes, bounded caches (glob matchers, conversation state TTL + cap), forced-parallel spawn-id normalization, ownership-gated respawn (desktop windows only; web-desktop clients never spawn), self-trip suppression for the agent writing rule files (including via shell redirects).

Validation

  • TTSR-scoped suites: 455 tests green (src/__tests__/main/ttsr/, TTSR IPC handlers, reminder application, useTtsr, Rules panel, respawn config).
  • npm run lint (tsc all configs + ESLint) clean; prettier clean.
  • Acceptance matrix in plans/ttsr-acceptance-matrix.md measured per agent.

Notes for review

  • encoreFeatures.ttsr is the lifecycle flag keying the first-party plugin definition, same as the other marketplace-surfaced features.
  • The regex gate + scan ceiling raise the bar but are not a ReDoS analyzer; rule files remain .maestro/cue.yaml-class trusted content and the doc comments say so explicitly.
  • No supervised background service: the tap short-circuits when disabled, so there is nothing to stop.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Time-Traveling Stream Rules (TTSR) to watch agent output, trigger corrective retries, and persist state for uninterrupted behavior.
    • Added a project-scoped Rules right panel to list, author, validate, enable/disable, and delete rules with live refresh.
    • Added TTSR settings (global enable/disable, disabled rules, interrupt context mode) and interrupt notifications with clearer outcome details.
  • Bug Fixes
    • Prevented double-counting of Claude Code partial streamed text.
  • Documentation
    • Added TTSR implementation plan, Gate A acceptance matrix, and rule-authoring guidance.
  • Tests
    • Added extensive TTSR end-to-end and acceptance coverage.

chr1syy and others added 19 commits July 21, 2026 08:41
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>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

TTSR 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.

Changes

TTSR implementation

Layer / File(s) Summary
Rule contracts and configuration
src/shared/ttsr-types.ts, src/main/ttsr/config/*, src/shared/maestro-paths.ts, plans/*, src/prompts/ttsr-rule-authoring.md
Defines rule schemas, settings, capability matrices, normalization, filesystem storage, validation, watching, authoring guidance, implementation phases, and acceptance criteria.
Stream monitoring and interruption
src/main/ttsr/*, src/main/process-manager/*
Adds parsed-event observation, regex/tool/AST matching, bounded state tracking, persistence, interrupt signaling, corrective payloads, notifications, and runtime lifecycle orchestration.
IPC and spawn integration
src/main/ipc/handlers/*, src/main/preload/*, src/main/index.ts
Exposes rule/settings APIs, injects deferred reminders transactionally into successful spawns, correlates corrective turns, and installs the main-process runtime.
Renderer controls and corrective respawn
src/renderer/components/*, src/renderer/hooks/*, src/renderer/services/*, src/renderer/stores/*
Adds TTSR settings, Rules navigation and management UI, event subscriptions, abort suppression, corrective agent respawns, and toast outcome rendering.
Verification
src/__tests__/*
Adds unit, integration, acceptance-matrix, persistence, IPC, runtime, and renderer coverage plus shared TTSR test mocks and reset wiring.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: reachrazamair, jsydorowicz21

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing TTSR for rule-driven stream interruption.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds rule-driven interruption and corrective respawning for live agent streams. The main changes are:

  • Project-scoped regex, glob, and AST rule loading.
  • Stream matching with interrupt budgets and repeat policies.
  • Corrective respawns and transactional deferred reminders.
  • Persisted conversation state and rule-file watching.
  • Settings, IPC bridges, and a Right Bar rule-management panel.

Confidence Score: 4/5

Regex matching and tool snapshot filtering need fixes before merging.

  • Oversized tool payloads can bypass rules after the first 32 KB.
  • Accepted regex patterns can block Electron's main process.
  • Nested project and fixture rule paths are incorrectly exempted from matching.
  • Interrupt and reminder lifecycle handling otherwise includes strong cleanup and recovery paths.

src/main/ttsr/ttsr-matcher.ts, src/main/ttsr/config/ttsr-config-normalizer.ts, src/main/ttsr/ttsr-tool-extract.ts

Security Review

Repository-controlled regexes run synchronously in Electron's main process. The current safety check permits overlapping-alternation patterns that can cause catastrophic backtracking and freeze the application.

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "ttsr: review fixes - fresh-project watch..." | Re-trigger Greptile

Comment thread src/main/ttsr/ttsr-matcher.ts
Comment thread src/main/ttsr/config/ttsr-config-normalizer.ts Outdated
Comment thread src/main/ttsr/ttsr-tool-extract.ts
…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/renderer/utils/ttsrRespawn.ts (1)

88-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expose a forcedParallel flag from parseSessionId and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1081299 and 7165b3b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (87)
  • package.json
  • plans/ttsr-acceptance-matrix.md
  • plans/ttsr-implementation-plan.md
  • src/__tests__/helpers/resetStores.ts
  • src/__tests__/main/ipc/handlers/process/apply-ttsr-reminders.test.ts
  • src/__tests__/main/ipc/handlers/ttsr.test.ts
  • src/__tests__/main/ttsr/ttsr-acceptance-matrix.test.ts
  • src/__tests__/main/ttsr/ttsr-ast.test.ts
  • src/__tests__/main/ttsr/ttsr-config-loader.test.ts
  • src/__tests__/main/ttsr/ttsr-interrupt-driver.test.ts
  • src/__tests__/main/ttsr/ttsr-manager.test.ts
  • src/__tests__/main/ttsr/ttsr-matcher.test.ts
  • src/__tests__/main/ttsr/ttsr-notify.test.ts
  • src/__tests__/main/ttsr/ttsr-runtime.test.ts
  • src/__tests__/main/ttsr/ttsr-state-persistence.test.ts
  • src/__tests__/main/ttsr/ttsr-state-store.test.ts
  • src/__tests__/main/ttsr/ttsr-tool-extract.test.ts
  • src/__tests__/renderer/components/RightPanel.test.tsx
  • src/__tests__/renderer/components/Settings/Extensions/extensionModel.test.ts
  • src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx
  • src/__tests__/renderer/components/TtsrRulesPanel.test.tsx
  • src/__tests__/renderer/hooks/useTtsr.test.ts
  • src/__tests__/renderer/services/ttsr.test.ts
  • src/__tests__/renderer/utils/ttsrRespawn.test.ts
  • src/__tests__/setup.ts
  • src/__tests__/shared/pianola/pianola-first-party-plugin.test.ts
  • src/main/coworking/coworking-session-id.ts
  • src/main/index.ts
  • src/main/ipc/handlers/index.ts
  • src/main/ipc/handlers/process.ts
  • src/main/ipc/handlers/process/apply-ttsr-reminders.ts
  • src/main/ipc/handlers/process/handle-spawn.ts
  • src/main/ipc/handlers/process/spawn-types.ts
  • src/main/ipc/handlers/ttsr.ts
  • src/main/preload/index.ts
  • src/main/preload/ttsr.ts
  • src/main/process-manager/ProcessManager.ts
  • src/main/process-manager/handlers/StdoutHandler.ts
  • src/main/process-manager/spawners/ChildProcessSpawner.ts
  • src/main/process-manager/spawners/OpencodeServerSpawner.ts
  • src/main/process-manager/types.ts
  • src/main/stores/defaults.ts
  • src/main/ttsr/config/ttsr-config-loader.ts
  • src/main/ttsr/config/ttsr-config-normalizer.ts
  • src/main/ttsr/config/ttsr-config-repository.ts
  • src/main/ttsr/index.ts
  • src/main/ttsr/ttsr-ast.ts
  • src/main/ttsr/ttsr-injection.ts
  • src/main/ttsr/ttsr-interrupt-driver.ts
  • src/main/ttsr/ttsr-manager.ts
  • src/main/ttsr/ttsr-matcher.ts
  • src/main/ttsr/ttsr-notify.ts
  • src/main/ttsr/ttsr-runtime.ts
  • src/main/ttsr/ttsr-spawn-registry.ts
  • src/main/ttsr/ttsr-state-persistence.ts
  • src/main/ttsr/ttsr-state-store.ts
  • src/main/ttsr/ttsr-tool-extract.ts
  • src/prompts/ttsr-rule-authoring.md
  • src/renderer/App.tsx
  • src/renderer/components/RightPanel.tsx
  • src/renderer/components/Settings/Extensions/extensionModel.ts
  • src/renderer/components/Settings/searchableSettings.ts
  • src/renderer/components/Settings/tabs/EncoreTab/EncoreTab.tsx
  • src/renderer/components/Settings/tabs/EncoreTab/components/TtsrSettingsSection.tsx
  • src/renderer/components/Settings/tabs/EncoreTab/components/index.ts
  • src/renderer/components/Settings/tabs/EncoreTab/hooks/index.ts
  • src/renderer/components/Settings/tabs/EncoreTab/hooks/useTtsrSettingsState.ts
  • src/renderer/components/Settings/tabs/EncoreTab/types.ts
  • src/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsx
  • src/renderer/components/TtsrRulesPanel/index.ts
  • src/renderer/global.d.ts
  • src/renderer/hooks/agent/internal/useAgentExitListener.ts
  • src/renderer/hooks/batch/useAutoRunHandlers.ts
  • src/renderer/hooks/props/useRightPanelProps.ts
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/hooks/useTtsr.ts
  • src/renderer/services/ttsr.ts
  • src/renderer/stores/batchStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/stores/ttsrStore.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/ttsrRespawn.ts
  • src/shared/maestro-paths.ts
  • src/shared/plugins/first-party.ts
  • src/shared/promptDefinitions.ts
  • src/shared/settingsMetadata.ts
  • src/shared/ttsr-types.ts

Comment thread src/__tests__/main/ttsr/ttsr-state-store.test.ts Outdated
Comment thread src/main/ttsr/ttsr-interrupt-driver.ts
Comment thread src/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsx
Comment thread src/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsx
Comment thread src/renderer/components/TtsrRulesPanel/TtsrRulesPanel.tsx
chr1syy and others added 6 commits July 22, 2026 12:37
…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>
chr1syy and others added 13 commits July 25, 2026 15:25
…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts (1)

2568-2591: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Closing 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 a content: [{type:'text', ...}] block, never a thinking block. Per the parser's transformMessage logic, isReasoning is derived from thinkingText.length > 0, so the closing assistantMessage('Answer.') in this test always has isReasoning: undefined regardless of textAlreadyStreamed. Since StdoutHandler only emits thinking-chunk for claude-code when event.isReasoning is true, the assertion expect(thinkingSpy).toHaveBeenCalledTimes(2) after Line 2588 would pass even if the textAlreadyStreamed skip were removed entirely - it isn't exercising the double-count guard on the thinking-chunk path, only on streamedText.

Consider extending assistantMessage (or adding a variant) to include a thinking content block mirroring the deltas, so the flagged event actually carries isReasoning: true and the test genuinely proves the guard suppresses the extra thinking-chunk emission.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between af02a8f and 1a34745.

📒 Files selected for processing (13)
  • src/__tests__/main/parsers/claude-output-parser.test.ts
  • src/__tests__/main/process-manager/handlers/StdoutHandler.test.ts
  • src/__tests__/main/ttsr/ttsr-manager.test.ts
  • src/__tests__/renderer/hooks/useTtsr.test.ts
  • src/main/agents/definitions.ts
  • src/main/parsers/agent-output-parser.ts
  • src/main/parsers/claude-output-parser.ts
  • src/main/process-manager/handlers/StdoutHandler.ts
  • src/main/ttsr/ttsr-manager.ts
  • src/renderer/components/TerminalOutput/components/LogItem.tsx
  • src/renderer/hooks/useTtsr.ts
  • src/renderer/types/index.ts
  • src/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

Comment thread src/renderer/components/TerminalOutput/components/LogItem.tsx Outdated
Comment thread src/renderer/components/TerminalOutput/components/LogItem.tsx
chr1syy and others added 13 commits July 26, 2026 21:49
# 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant