file the 2026-06-11 Fable review under epic T-359

Commit fable-ous.md (13-reviewer multi-agent assessment of the whole
tree) and the ticket tree it produced: epic T-359 with 26 children
covering the dragon bugs (PTY fd leak, undrained claude stderr,
unauthenticated MCP HTTP, path-confinement gaps, dispose-path leaks,
SGR crash, a11y semantics), the medium scorpions, a dead-code sweep,
the systemic-pattern work, and split plans for app.dart and the claude
meta sidebar. Root causes appended to existing T-274, T-283 context,
and the parser split plan to T-123. One review claim (ColumnHat
duplicated in app.dart) was refuted during verification and is
annotated on T-385/T-394.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:13:52 +02:00
co-authored by Claude Fable 5
parent 662325d5db
commit 3ed90fc318
3 changed files with 1035 additions and 0 deletions
+304
View File
@@ -3633,3 +3633,307 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2T11GCV1EV07DYD5BZENTM', 'status', 'ready', 'in_progress', NULL, '2026-06-11 14:18:34', '2026-06-11 14:18:34', '2026-06-11 14:18:34', NULL, 'a2a75bcb471d6eba8a215d56922c6337', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2T11GCV1EV07DYD5BZENTM', 'status', 'in_progress', 'in_progress', NULL, '2026-06-11 14:59:53', '2026-06-11 14:59:53', '2026-06-11 14:59:53', NULL, '96bef4f722871cb468f67b55d6447acc', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2T11GCV1EV07DYD5BZENTM', 'status', 'in_progress', 'done', NULL, '2026-06-11 17:09:12', '2026-06-11 17:09:12', '2026-06-11 17:09:12', NULL, '591cc2576194fd9400b42aa6c932f0a8', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBGHNEQTAEPGNJKN42C1E8', 'description', NULL, 'Thirteen parallel subsystem reviewers + adversarial verification over ~58k LOC produced ~14 high-severity and ~35 medium findings plus systemic patterns and a feature backlog. Source: fable-ous.md (committed alongside this epic). Children are filed individually so they can land independently; feature proposals went to governance/questions as Q-records, not tickets.
Acceptance: all dragon (high-severity) findings resolved or formally rejected with a D-record; scorpions shipped or moved to follow-up work with rationale; rat-extermination and systemic-pattern batches closed; god-file split plans written into their tickets.', NULL, '2026-06-11 21:54:44', '2026-06-11 21:54:44', '2026-06-11 21:54:44', NULL, 'f4f3f8de34ddcffcedfa3dda86cddd7f', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBJ5T7HAQ9CA8XQMX43A2C', 'description', NULL, 'lib/src/pty/native_pty.dart:444-450 — on child EOF, _reap() sets _dead = true but never closes _fd; a later close() short-circuits at `if (_dead) return;` (line ~460) so _nativeClose(_fd) (line ~477) never runs. Every terminal/Claude pane whose child exits on its own leaks an fd and a pty device for the life of the app. Two independent reviewers confirmed.
Fix: close the master fd in the natural-exit path (or let close() proceed to fd teardown when dead). Add a test asserting the fd is released after child EOF.', NULL, '2026-06-11 21:54:56', '2026-06-11 21:54:56', '2026-06-11 21:54:56', NULL, '446dac267b474cc3c72291e1d34ed8d1', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBKK2TZQK683J8FS0ZH5A4', 'description', NULL, 'lib/builtin/claude/src/stream_json_session.dart:43-78 — the claude child stderr is never drained: >=64KB of --verbose spew fills the pipe, the child blocks mid-turn, and the flagship pane wedges with zero diagnostics. Nothing watches exitCode or stdout onDone (line ~303), so a crashed/dead session just looks busy.
Fix: drain stderr into a bounded ring buffer (surface it on failure), watch exitCode/onDone, and emit a terminal SessionEnded state to the pane. Intersects T-283 (resume hang has no timeout/fallback). Tests: dead-child surfaces SessionEnded; stderr flood does not wedge the session.', NULL, '2026-06-11 21:55:07', '2026-06-11 21:55:07', '2026-06-11 21:55:07', NULL, '9a3d2aecfc09ce6c6afa183e80e718f4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'description', NULL, 'lib/src/ipc/mcp_server.dart:138-195, started unconditionally at boot (lib/main.dart:174-180). D-71 threat model (another user on the same host must not drive my IDE) is enforced with 0600 on the unix socket — then bypassed wholesale by an unauthenticated localhost HTTP port that, since D-86, serves every clide verb as a tool.
Fix: generate a token in the lock file (Claude Code /ide lock format has a slot for it) and require the auth header on every request. Tests: request without token is rejected; token round-trips via the lock file.', NULL, '2026-06-11 21:55:21', '2026-06-11 21:55:21', '2026-06-11 21:55:21', NULL, '3e4a3ab07c224bc9c498c4ac168fcb42', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'description', NULL, 'lib/src/editor/registry.dart:215-219 returns absolute paths verbatim — no .. normalization, no path_safety call — an unconfined read AND write primitive over IPC while files.read is carefully guarded.
Fix: route editor.open/editor.save through path_safety like files.*. Tests: traversal and absolute-escape attempts rejected for both verbs. Longer-term the confinement should move to the dispatch layer (see the systemic ticket filed with this epic).', NULL, '2026-06-11 21:55:33', '2026-06-11 21:55:33', '2026-06-11 21:55:33', NULL, '9814d1e2ac630b792799349b073e29cd', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'description', NULL, 'lib/src/search/replace_engine.dart:124-143 — the include/exclude glob filters the user typed are accepted but never applied; replace will happily rewrite files outside the filter.
Fix: apply the same glob filtering the search side uses before rewriting. Test: replace with an include glob touches only matching files; exclude glob is honored.', NULL, '2026-06-11 21:55:44', '2026-06-11 21:55:44', '2026-06-11 21:55:44', NULL, '6f7302d4cf04632c7c336131552dddc8', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBSG6356MZJ2DCCCSBMBGM', 'description', NULL, 'lib/src/files/listing.dart:46-54 — stat() follows links, so isSymlink is always false; walkFiles therefore descends symlinked directories the docs claim it skips (escape hatch out of the workspace, plus cycle risk).
Fix: use lstat (FileStat via Link check / FileSystemEntity.isLinkSync on the raw path) for symlink detection. Tests: symlinked dir is reported as symlink and not descended; symlink cycle does not hang the walk.', NULL, '2026-06-11 21:55:56', '2026-06-11 21:55:56', '2026-06-11 21:55:56', NULL, 'eb7a89483e85f7c069567c56ee80fdb9', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBV0465906BY3QFAY9F1YM', 'description', NULL, 'lib/builtin/terminal/src/terminal_pane.dart:131-137 calls ClideKernel.of(context) from dispose() — illegal ancestor lookup, swallowed by catch (_) — so pane.close is never sent and the backend PTY + daemon pane leak on every closed terminal pane. The same idiom leaks the settings listener in every disposed ClaudePane (lib/builtin/claude/src/claude_pane.dart:460-466).
Fix: cache the kernel ref in didChangeDependencies, delete the catch-alls. Combined with the PTY natural-exit fd leak this is a two-stage leak pipeline. Tests: closing a terminal pane sends pane.close; disposing a ClaudePane removes its settings listener.', NULL, '2026-06-11 21:56:09', '2026-06-11 21:56:09', '2026-06-11 21:56:09', NULL, '92693bb428217e84d75d240b5da07bec', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBWE2W1226T58CX37E50HC', 'description', NULL, 'lib/main.dart:335-344 — switching projects builds a new dispatcher with fresh PaneRegistry, FilesService, EditorRegistry, etc., but nothing calls the old set''s shutdown() methods (which exist and have zero callers). Old file watchers keep emitting into the new workspace''s bus.
Fix: dispose/shutdown the previous service set before (or while) standing up the new one. Test: after a workspace switch, the old FilesService watcher no longer delivers events.', NULL, '2026-06-11 21:56:19', '2026-06-11 21:56:19', '2026-06-11 21:56:19', NULL, '25954a8b7b5f77985dff44c8e78f5ee9', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W6VN98RQM6S22X28', 'description', 'The bottom status-bar context slot for the Claude pane (model · permission-mode · context, T-145/T-150) frequently renders empty and doesn''t update. The slot is fed by the active pane''s status widget via the focus service (PaneContextStatusItem -> ClidePane.statusWidget in claude_pane.dart), sourced from StreamJsonSession.statusStream (model/permissionMode from the ''system/init'' event; cost/contextWindow from ''result'' events in stream_json_session.dart).
Repro: open clide; the status line is often blank and stays blank until/unless a turn completes (or never populates).
Likely suspects to investigate:
- status only published while the pane is the focused contribution (active==true) if focus isn''t on the Claude pane, the slot clears.
- statusStream may not emit until the first ''result''/''init'' event; a resumed session (--resume) may not re-emit init, so model/mode never arrive.
- _statusWidget returns null when _status.isEmpty AND no skills, so an unstarted/!init session shows nothing.
- focus-slot wiring (FocusTracker) may not re-publish on pane (re)build / session rebind.
Acceptance: the Claude status line shows model · mode · context promptly after a session starts/resumes and stays current across turns and focus changes; add a test covering the resumed-session (no fresh init) case.', 'The bottom status-bar context slot for the Claude pane (model · permission-mode · context, T-145/T-150) frequently renders empty and doesn''t update. The slot is fed by the active pane''s status widget via the focus service (PaneContextStatusItem -> ClidePane.statusWidget in claude_pane.dart), sourced from StreamJsonSession.statusStream (model/permissionMode from the ''system/init'' event; cost/contextWindow from ''result'' events in stream_json_session.dart).
Repro: open clide; the status line is often blank and stays blank until/unless a turn completes (or never populates).
Likely suspects to investigate:
- status only published while the pane is the focused contribution (active==true) if focus isn''t on the Claude pane, the slot clears.
- statusStream may not emit until the first ''result''/''init'' event; a resumed session (--resume) may not re-emit init, so model/mode never arrive.
- _statusWidget returns null when _status.isEmpty AND no skills, so an unstarted/!init session shows nothing.
- focus-slot wiring (FocusTracker) may not re-publish on pane (re)build / session rebind.
Acceptance: the Claude status line shows model · mode · context promptly after a session starts/resumes and stays current across turns and focus changes; add a test covering the resumed-session (no fresh init) case.
Root cause found and verified by the 2026-06-11 Fable review (fable-ous.md, epic T-359): statusStream is a plain broadcast controller the system/init event fires while spawn() is still awaiting a 256KB transcript-tail read, before the pane ever subscribes (lib/builtin/claude/src/claude_pane.dart:322, lib/builtin/claude/src/session_orchestrator.dart:222-225). Fix: seed from session.status on bind, or make the stream replay-latest (see the ValueStream systemic ticket under T-359).', NULL, '2026-06-11 21:56:28', '2026-06-11 21:56:28', '2026-06-11 21:56:28', NULL, '8bddda6f828eeb567dfaed51eef82762', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBYTJ4E7ZBY6DWWNT1S16M', 'description', NULL, 'lib/builtin/claude/src/conversation_view.dart:268-277 — the _atBottom pin exists but is only consulted on viewport resize, not on new items. Anyone reading earlier output during a long streaming reply is dragged to the bottom continuously.
Fix: gate the new-item auto-scroll on _atBottom (one-line) and add the missing twin test: scrolled-up viewport stays put when items stream in; at-bottom viewport follows.', NULL, '2026-06-11 21:56:40', '2026-06-11 21:56:40', '2026-06-11 21:56:40', NULL, 'e519966a59d3a52cdc28d598967010ce', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC0HYRZ86CWW0DDQJ5CAQM', 'description', NULL, 'lib/src/terminal/src/core/escape/parser.dart:501-516 — SGR 38/48 extended-color parsing does unguarded params[i + 1] lookahead, so a truncated sequence like printf ''\e[38m'' throws RangeError inside Terminal.write. An emulator must never throw on hostile bytes. Secondary, same code path: colon-form SGR sub-parameters (e.g. 38:2:r:g:b, emitted by modern terminfo) are not split out and get mangled into bogus params.
Fix: bounds-check the lookahead (ignore incomplete 38/48 sequences), and parse colon-form sub-parameters per ECMA-48/ITU T.416 treat 38:2::r:g:b and 38;2;r;g;b equivalently. Note T-123 (parser split) touches the same file; coordinate but do not block on it.
Acceptance: feeding any truncated/garbled SGR byte sequence never throws (fuzz-style test over partial sequences); colon-form truecolor sets the same fg/bg as semicolon form; existing SGR tests stay green.', NULL, '2026-06-11 21:56:59', '2026-06-11 21:56:59', '2026-06-11 21:56:59', NULL, 'acbde4172213adbf84599ca1575e2bb6', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC2NM7AYKENZ0ZD49HAX1W', 'description', NULL, 'lib/widgets/src/clide_collapser_card.dart:92-101 — excludeSemantics: true on the card wipes every expanded child from the a11y tree: a screen-reader user can expand a run/tool card and hear nothing inside it. A11y is a Tier-0 contract in this repo, so this is a contract breach, not polish.
Fix: exclude semantics only while collapsed (or scope the exclusion to the chrome, not the body); keep the header announcing expanded/collapsed state.
Acceptance: semantics test asserting expanded-card children are present in the semantics tree and absent (or summarized) when collapsed; make test-a11y green.', NULL, '2026-06-11 21:57:12', '2026-06-11 21:57:12', '2026-06-11 21:57:12', NULL, '7ffe4c0c1c435020f5b0ef564088b997', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC46SEHH8NQY481VMGK66R', 'description', NULL, 'The three a11y gate tests hand-enumerate their subjects and have measurably drifted from lib/: the contrast gate checks fewer themes than lib/main.dart:443-454 actually loads (catppuccin is silently unvalidated — the same theme list also drifted between main.dart and the testmode harness), and the i18n gate checks 4 of 8 namespaces. The gates stay green while covering less — worst kind of drift.
Fix (pattern: hand-enumerated lists drift; export the truth): make lib/ export one canonical const each for bundled themes, a11y gate subjects, and i18n namespaces; the gates and the testmode harness iterate those exports instead of their own lists. Add a meta-assertion where feasible (e.g. namespace list derived from the assets dir at test time) so a new theme/namespace cannot ship unvalidated.
Acceptance: gates fail if a newly added theme/namespace is not covered; catppuccin contrast-checked; all 8 i18n namespaces checked; testmode harness consumes the same exported theme list.', NULL, '2026-06-11 21:57:26', '2026-06-11 21:57:26', '2026-06-11 21:57:26', NULL, 'c8af63caa7c66890bf983ca0745ba220', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC5ZE4EZEGXK8YY8J86CM0', 'description', NULL, 'lib/src/ipc/server.dart:151-180 — D-72 promises serial dispatch, but the async onData handler never pauses the subscription, so pipelined requests interleave; the shared StringBuffer framing can also drop or double lines when chunks split mid-frame, and per-chunk utf8 decode corrupts multi-byte characters split across chunks.
Fix in one move: client.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()) consumed with await for gives correct framing, persistent UTF-8 decoding, and true serialization at once.
Acceptance: test sending two pipelined requests in a single write (responses arrive in order, both handled); test a request split mid-UTF-8-rune across two socket writes; existing IPC tests stay green. Runs under dart test keep imports Flutter-free.', NULL, '2026-06-11 21:57:40', '2026-06-11 21:57:40', '2026-06-11 21:57:40', NULL, '4463b6a7be217a811ec7d0a34f90d40e', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC7KDFW07S8WTCC3MD71J0', 'description', NULL, 'The terminal''s only ingestion API is write(String) (lib/src/terminal/src/terminal.dart:218), so both consumers decode bytes per-chunk — a multi-byte rune split across PTY reads renders as U+FFFD garbage. FileTailFollower starts reading mid-file by construction, so it can begin mid-character too.
Fix: add Terminal.writeBytes(List<int>) backed by a persistent chunked Utf8Decoder (allowMalformed) per terminal instance; migrate the PTY consumer and FileTailFollower to it. Keep write(String) for tests/programmatic use.
Acceptance: test feeding a multi-byte rune split across two writeBytes calls renders one glyph; FileTailFollower starting mid-rune resyncs without emitting replacement chars mid-stream.', NULL, '2026-06-11 21:57:52', '2026-06-11 21:57:52', '2026-06-11 21:57:52', NULL, 'e3a6ef892b21e588590ce513c7e4e40d', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC90B72A270CAKA7AP1ZX8', 'description', NULL, 'lib/builtin/claude/src/session_orchestrator.dart:191-249 — spawn() does check-then-act on the sessions map across two awaits (transcript-tail read, process start). Two concurrent spawns for the same session id both pass the check; the loser''s live claude process is orphaned, never killed, never observed.
Fix: hold a Map<String, Future<ManagedSession>> first caller installs the future synchronously, later callers await the same future; remove the entry on failure.
Acceptance: test issuing two concurrent spawn() calls for one id yields the same ManagedSession instance and exactly one process spawn (count via injected spawner); failure path clears the in-flight entry so a retry can proceed.', NULL, '2026-06-11 21:58:03', '2026-06-11 21:58:03', '2026-06-11 21:58:03', NULL, '8dc37c08e1f62b6b8c3cdf80e32eb4be', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCAFKK334YNJXZJQG4J6AW', 'description', NULL, 'lib/builtin/claude/src/claude_pane.dart:266-281 — when (re)binding a session, widget.forkSourceId takes precedence forever, so /clear in a fork pane re-forks the original conversation instead of clearing, and /resume and /fork misbehave the same way. Related context: clide owns /clear, /resume, /compact interception (T-156); panes pin a session id.
Fix: treat forkSourceId as a one-shot spawn parameter consume it on first bind (clear it into pane state), so subsequent session-mutating commands operate on the pane''s live session.
Acceptance: test that a fork pane after /clear starts an empty session (no fork source passed to the orchestrator on respawn); first bind still forks from the source.', NULL, '2026-06-11 21:58:16', '2026-06-11 21:58:16', '2026-06-11 21:58:16', NULL, 'ce8c670160f5180e3eb35263c28d4f78', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCC6AR37VTF4SY8DR99JHC', 'description', NULL, 'lib/kernel/src/settings.dart:199-219 — the writer emits toString() for maps nested inside lists, corrupting them on the next read; this breaks the documented keymap overlay across restarts. Writes are also non-atomic (a crash mid-write truncates the file), and a parse failure on load silently resets ALL settings instead of preserving the file and surfacing the error.
Fix: serialize with a real encoder (JSON/YAML emitter, whatever the file format is) covering nested structures; write to a temp file + rename for atomicity; on parse failure keep the original file (e.g. move aside as .broken) and log via the kernel Logger instead of resetting.
Acceptance: round-trip test for a keymap overlay (list of maps) across save/load; simulated partial write leaves previous settings intact; corrupt file does not silently reset and produces a logged diagnostic.', NULL, '2026-06-11 21:58:33', '2026-06-11 21:58:33', '2026-06-11 21:58:33', NULL, '1f0f584e4c35a60c69b6ffdbe41caad9', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCEC25337J2AXXQNST56Y4', 'description', NULL, 'lib/kernel/src/extensions_manager.dart:133-192 — a throw mid-contribution leaves earlier contributions mounted while the extension records as failed; a retry then double-applies them. Also: disabling an extension ignores extensions that depend on it, and contribution registries silently clobber on id collision. Benign among curated builtins; hazardous the day Tier-6 Lua extensions (T-8) land.
Fix: make activation transactional collect contributions, mount only after the extension activates cleanly, and unwind mounted ones on failure; disable refuses (or cascades, pick one and record it) when dependents are active; registries reject or namespace duplicate ids with a logged diagnostic.
Acceptance: test that an extension throwing mid-activation leaves zero contributions mounted and can retry cleanly; disable-with-dependents behaves per the chosen rule; duplicate contribution id surfaces an error instead of clobbering.', NULL, '2026-06-11 21:58:49', '2026-06-11 21:58:49', '2026-06-11 21:58:49', NULL, 'fef851b7cad9f7a3307e04de1168792a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCG84F4SPFW111CC4K26A8', 'description', NULL, 'The owned terminal fork fixes what it trips over but has no vttest-style conformance suite. Verified debt:
- HTS (set tab stop) is a no-op: calls isSetAt instead of setAt (lib/src/terminal/src/terminal.dart:423).
- DECCKM (cursor-keys application mode) is tracked but never consumed when encoding arrow-key input.
- Legacy X10/X11 mouse reporting rows are off-by-one AND the existing test enshrines the bug fix both together.
- CPR (cursor position report) replies 0-based where every real terminal is 1-based.
- Scrollback is maintained but structurally unreachable: ViewportOffset.zero() is pinned on every build (lib/src/terminal/src/terminal_view.dart:224), so no scrolling UI path exists. (Blocks the semantic-terminal feature idea; fix is a prerequisite there.)
Approach: fix each item with a focused conformance test (vttest-style expectations); consider starting a small conformance_test.dart suite that future escape-sequence work extends. Coordinate with T-123 (parser split) and T-369 (SGR crash) which touch the same area.
Acceptance: each of the five items has a test that fails on the old behavior and passes after; mouse test corrected, not deleted.', NULL, '2026-06-11 21:59:05', '2026-06-11 21:59:05', '2026-06-11 21:59:05', NULL, 'c47893702c6e206fb78da5e04c8ffdbe', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCJEYHC91PMVNVWVHBR2RG', 'description', NULL, 'lib/widgets/src/clide_markdown.dart:408-410 — hard line breaks and image nodes both fall through to empty text spans: words on either side of a hard break glue together, and images vanish entirely (no placeholder, no alt text).
Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up note the existing feedback that live-pane images go through clide image show).
Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', NULL, '2026-06-11 21:59:20', '2026-06-11 21:59:20', '2026-06-11 21:59:20', NULL, 'a839cd055a09b586f3693fabed212aa4', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCM1RBAF72SCZBKRTXJSYC', 'description', NULL, 'Three compounding costs on every streamed token:
1. The whole markdown document re-parses inside build on each streaming delta, including sync existsSync() calls in the parse path (clide_markdown.dart) sync I/O on the UI isolate per frame.
2. The conversation view re-derives its full item list O(n) on every controller notification.
3. Token streaming re-encodes the full reply text per delta (lib/builtin/claude/src/stream_json_session.dart:410-431) O(n²) churn over a long reply.
Fix directions: cache parsed markdown per card keyed by content hash and only re-parse the tail/dirty card; move file-existence link checks off the build path (async + cache); make the session accumulate deltas in a StringBuffer instead of string concat re-encode; derive conversation items incrementally. Profile before/after with a long synthetic reply.
Acceptance: a 1000-delta synthetic stream does no existsSync in build (assert via injected fs seam or profiling harness), and per-delta work is bounded (no full-document re-parse); existing rendering tests green.', NULL, '2026-06-11 21:59:37', '2026-06-11 21:59:37', '2026-06-11 21:59:37', NULL, '2ad44f254878d377bb9b54512a5e4947', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'description', NULL, 'lib/builtin/terminal/src/terminal_pane.dart:69 — the shell spawns with Directory.current as cwd. Launched from a desktop entry, that is $HOME, not the open workspace; after a project switch it is whatever the process started in. SpawnSpec.cwd already exists in the PTY layer.
Fix: pass the active workspace root as the spawn cwd (and re-derive it on project switch for new panes).
Acceptance: test that a terminal pane''s SpawnSpec.cwd equals the workspace root, not Directory.current.', NULL, '2026-06-11 21:59:50', '2026-06-11 21:59:50', '2026-06-11 21:59:50', NULL, '87e1c5e8220dd5fa5eb9d7b44335ed6a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'description', NULL, 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.', NULL, '2026-06-11 22:00:05', '2026-06-11 22:00:05', '2026-06-11 22:00:05', NULL, 'b97d86a5de4f9b7435f678fb44bcb25b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCST6CQ449VJGAP6C5KZ5W', 'description', NULL, 'lib/builtin/welcome/src/welcome_view.dart:173-174 — the Clone-from-git and Start-a-Claude-session tiles are inert on tap, and the keyboard shortcuts printed on the tiles are not registered anywhere. First-run users hit dead UI on the first screen.
Fix: either wire the tiles (clone flow; open a Claude pane) and register the shortcuts through the keymap subsystem, or remove the tiles until the flows exist no advertised dead ends. Note: the welcome screen also duplicates FileActions'' open-folder flow verbatim; the dedup is covered by the copy-paste sweep ticket under this epic, but if you touch this file, prefer calling into FileActions.
Acceptance: every tile on the welcome screen performs its action (widget test taps each); every shortcut shown is registered in the keymap.', NULL, '2026-06-11 22:00:22', '2026-06-11 22:00:22', '2026-06-11 22:00:22', NULL, 'e968c74a8637487134343841ae96ddfa', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCVPGCKEGDC54KKQ120SRM', 'description', NULL, 'tools/ui/*.sh still cd into the removed app/ directory, so make test-e2e, make ui-dev, and make ui-smoke fail immediately. The staged Gitea CI workflow would fail in three independent ways the day it is activated, while D-32 describes it as ready.
Fix: repoint the scripts at the repo root (post app/-flattening layout), run each target to prove it, and walk the Gitea workflow steps locally (or in a dry-run) until each step is green or consciously removed. Amend D-32 if the CI story changed.
Acceptance: all three make targets run; the workflow file''s steps each map to a working make target; D-32 matches reality.', NULL, '2026-06-11 22:00:37', '2026-06-11 22:00:37', '2026-06-11 22:00:37', NULL, '89d8eb48c066c985f1fb44270b0a95da', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'description', NULL, 'Verified-dead code worth one sweep (coverage denominator benefits too):
- Legacy free-function git API (~250 LOC duplicating GitClient, kept alive only by its own tests, and carrying its own latent pipe-deadlock bug) delete API + tests.
- ToolCheck zero callers.
- ~60% of lib/src/pty/ffi/libc.dart fd-passing-era bindings unused since D-56.
- GraphView unreachable placeholder (note: the Governance Graph idea (see Q-records from this review) may later want the slot; deleting now is still right, it is a 17-line stub).
- ColumnHat duplicated line-for-line in app.dart, kept alive by a zero-coverage test; the app.dart split ticket removes the duplicate, this sweep removes the orphan.
- tmux-era team pipeline: TranscriptPublisher, TeamMemberJoined nothing emits these events, yet the team roster UI listens to them exclusively (team tiles are populated by ghosts). Remove pipeline + dead listeners; if the roster UI stays, it needs a real data source first (surface that before deleting the UI).
- Dead ptyc binary still committed in native/linux-x64/ against D-62/D-63 remove binary + licenses.yaml entry if present.
- mocktail pinned, documented in D-25 as the IO-mocking strategy, imported by zero files: either adopt it where mocks are hand-rolled or drop the dep AND amend D-25.
Each bullet is one commit. Run make test + coverage after each; expect the floor to ratchet up.', NULL, '2026-06-11 22:01:02', '2026-06-11 22:01:02', '2026-06-11 22:01:02', NULL, 'eda1bd3f4f7db6bfe206a1c0c39e8dae', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'description', NULL, 'Broadcast streams that carry STATE (not events) drop the current value for late subscribers. This one shape caused T-274 (status bar blank — root cause appended there), the meta sidebar''s manual compensation, and the prompt-stream''s initialData workaround.
Fix: write one small ValueStream<T> wrapper (a broadcast stream that replays the latest value to each new subscriber, plus a .value getter) in the kernel; retrofit statusStream, busyStream, and pendingPromptStream in the claude builtin; delete the per-site workarounds it obsoletes. No third-party dep (rxdart) prefer-zero-deps; the wrapper is ~30 LOC.
Acceptance: unit tests for the wrapper (late subscriber gets latest value; no value yet = no synthetic emit unless seeded); T-274 repro covered: subscribing after the init event still yields the status. Closing this should make T-274 fixable in one line at the call site.', NULL, '2026-06-11 22:01:18', '2026-06-11 22:01:18', '2026-06-11 22:01:18', NULL, '8d7c4939861881d0de4c984be2df9b7c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD2FWTDFYA00W4QXTE41M0', 'description', NULL, 'The silent-swallow idiom turned an illegal-lookup-in-dispose into two resource leaks (T-366) and turned process-spawn failures into blank panes. Rule to apply: cleanup/teardown paths MAY swallow (with a comment saying why); spawn, read, and dispose-adjacent paths MUST log through the kernel Logger they already have access to.
Work: grep the tree for `catch (_)` and empty catch bodies; classify each site (cleanup vs load-bearing); add logging or rethrow on the load-bearing ones; leave a one-line justification comment on the legitimate swallows. Consider an analysis_options lint (avoid_catches_without_on_clauses or a custom assist) only if it can be enabled without suppressions per repo policy no lint suppressions without approval.
Acceptance: zero unjustified empty catches on spawn/read/dispose paths; each remaining swallow carries a why-comment; any new logging visible in the kernel log during testmode.', NULL, '2026-06-11 22:01:37', '2026-06-11 22:01:37', '2026-06-11 22:01:37', NULL, 'bac36bc044fb209147b1c3d13c59b1d5', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD4QYHYRTK0SGRZCBSHSQ0', 'description', NULL, 'The single-isolate app does sync I/O inside async IPC handlers: files.read does a sync 10MB read; the replace engine reads and rewrites workspace files synchronously — while grep right next to it fans out to isolates per D-79. There is no recorded rule, so each new handler guesses.
Work: claim a D-record (pql decisions claim D architecture "sync I/O policy in IPC handlers") deciding the rule suggested: async File APIs by default in handlers; offload to an isolate above N KB (align N with the D-79 grep design); sync allowed only in pure-Dart test seams. Then apply it to files.read and replace_engine, citing the new D-NNN at each site.
Acceptance: D-record confirmed; files.read and search.replace no longer block the UI isolate on large files (test with a multi-MB fixture asserting the event loop stays responsive, e.g. a timer keeps firing).', NULL, '2026-06-11 22:01:52', '2026-06-11 22:01:52', '2026-06-11 22:01:52', NULL, 'da3256b09a0299b9a127cb80478f6af0', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD6FQMXMQQQ877KMNCK6QC', 'description', NULL, 'Per-verb path confinement is a lottery: files.read remembered, search.replace half-remembered, editor.open/save forgot (T-363). The dispatcher registry (D-74) already co-registers a schema with every handler and the schema already knows which params are paths.
Fix: add a path-type marker to the schema layer (or derive from a naming convention recorded in the D-record amendment) and run path_safety confinement once in the dispatcher before the handler sees the request. Per-verb checks become defense-in-depth or get deleted.
Do after T-363 lands its point fix this is the structural follow-up that prevents the next forgotten verb. Amend D-74 with the confinement rule.
Acceptance: a registered verb with a path param automatically rejects traversal/escape without any handler code; a regression test registers a synthetic verb and proves confinement applies; existing verbs unchanged behavior for in-workspace paths.', NULL, '2026-06-11 22:02:08', '2026-06-11 22:02:08', '2026-06-11 22:02:08', NULL, '41a21f3eaedab8c7455484862d7f5247', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD8F2NBEFZNPKSJ9W253J0', 'description', NULL, 'Verified duplication clusters, each small, together the repo''s main rot vector:
- Welcome screen clones FileActions'' entire open-folder flow verbatim call FileActions instead (coordinate with T-383).
- Command palette and quick-open are ~230-line near-twins extract the shared list-overlay+filter core.
- Three private tail-a-growing-file implementations in the claude builtin alone one shared follower (coordinate with T-373, which gives it the chunked decoder).
- Five hand-rolled _userErr helpers across handlers one shared error-shaping helper.
- Five copy-pasted git test sandboxes, none isolating host git config (set GIT_CONFIG_GLOBAL/HOME in the shared fixture flaky-test risk today).
- Two parallel ANSI flag enums that already drifted: strikethrough is stored but never painted unify, then paint strikethrough.
One commit per cluster. Acceptance: each cluster has a single implementation with the call sites migrated and a test guarding the shared piece; git sandboxes isolated from host config.', NULL, '2026-06-11 22:02:25', '2026-06-11 22:02:25', '2026-06-11 22:02:25', NULL, '09ac8f7ccc448560db7318cb09d2a9a3', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDAK04ZA0PBT69ZWNBXPSR', 'description', NULL, '16 handlers in the claude builtin return result: ok with an error field in the payload instead of an error envelope, drifting from the D-6 exit-code contract every other subsystem honors. Scripted example: clide claude.agent.set-permission-mode bogus exits 0 today, so scripts cannot detect failure.
Fix: sweep the claude builtin handlers; on failure return the error envelope (non-zero CLI exit) like the rest of the dispatcher. Audit callers/UI that may currently rely on ok-with-error.
Acceptance: clide claude.agent.set-permission-mode bogus exits non-zero; a table-driven test walks the claude verbs'' failure paths asserting error envelopes; D-6 conformance restored.', NULL, '2026-06-11 22:02:40', '2026-06-11 22:02:40', '2026-06-11 22:02:40', NULL, 'c6b3edd27b9c8d00aad3eee4bd7cc227', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDC7B2MFQZVR1K9E30FXDR', 'description', NULL, 'CLAUDE.md and README still say "tmux owns Claude session persistence (D-41)" — superseded by D-75/D-77 per docs/architecture.md. README says "Pre-v2.0 (2.0.0-dev)" while pubspec is at v2.3.3, and headlines "canvas and graph surfaces" that are a 17-line stub and a flat ListView respectively (T-7 epic was cancelled).
Fix: rewrite the stale paragraphs in both files to match current architecture (clide-managed stream-json sessions); fix the README version line (or derive it); demote canvas/graph to roadmap wording or drop them. The repo''s honesty is its brand; the README is the one off-brand surface.
Acceptance: no tmux-persistence claim outside historical D-records; README version matches pubspec; every README feature claim maps to shipped behavior.', NULL, '2026-06-11 22:02:54', '2026-06-11 22:02:54', '2026-06-11 22:02:54', NULL, 'afb2a71c491410f5ca5d313d1b9c716b', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDE5A3965GNRFS5WPZW878', 'description', NULL, 'No git tag since v2.1.0 despite five CHANGELOG releases; ci/release.sh exits 64 and still references the dissolved sidecar; the pre-push fast path''s safety argument cites "release CI on tagged versions" that does not exist; and the fast-path regex skips ALL tests for pushes touching test/, ci/, or the hook itself.
Work items (separable commits):
1. Back-tag v2.2.0 through v2.3.3 at the release-cut commits (find them via the CHANGELOG version-bump commits).
2. Rewrite ci/release.sh for the single-process architecture or delete it and fold release steps into the Makefile either way, no stub that exits 64.
3. Add tagging to the release ritual in .claude/skills/git-commit/SKILL.md (version bump + changelog move + tag in one documented step).
4. Widen the pre-push fast-path regex so changes under test/, ci/, and the hook itself run the full gate.
This is also the blocking prerequisite the T-47 (self-update) refinement identified. Acceptance: git tag lists every released version; release.sh (or its replacement) runs end-to-end; fast-path regex covered by a hook test if feasible.', NULL, '2026-06-11 22:03:10', '2026-06-11 22:03:10', '2026-06-11 22:03:10', NULL, '50c2e5c64c6a78ac9e785fcd4a0a128d', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDGPXQN31NNRPJ00PFRAG4', 'description', NULL, 'lib/app.dart is 1187 LOC mixing five confirmed concerns. Split plan (verified against the file 2026-06-12):
Inventory: ClideApp (14-29), _AppRoot (31-45), _RootShell/_RootShellState keyboard+intent routing (47-229), RootLayout (231-350), hat bar family _HatBar/_LeftHatContent/_RightHatContent/_WinBtn (352-439), project switcher family _ProjectSwitcherButton/_ProjectSwitcherDropdown/_RecentProjectRow/_ActionRow (441-672), SlotHost/_SlotHostState/_SlotBody/_SidebarSlot/_WorkspaceSlot/_RevealedTab/_ContextSlot (674-1030), _BottomRail (1032-1078), StatusbarCollapseToggle (1093-1126), StatusbarHost (1128-1170), _EditorDragHandle (924-1010), _WelcomeOverlay (1172-1187).
Target layout:
- app.dart keeps ClideApp + _AppRoot and RE-EXPORTS the public symbols so tests keep importing package:clide/app.dart.
- lib/widgets/root_shell.dart: _RootShell/_RootShellState (keyboard routing; depends on ModifierTapTracker, MenuBarController).
- lib/builtin/hat/hat_bar.dart: _HatBar, _LeftHatContent, _RightHatContent, _WinBtn, hatHeight.
- lib/builtin/hat/project_switcher.dart: the switcher family (~230 LOC).
- lib/widgets/slot_host.dart: SlotHost + slot bodies (NOTE: _RevealedTab references _SlotBody._resolveTitle keep them together or extract the helper).
- lib/widgets/layout_status.dart: RootLayout internals, StatusbarHost, StatusbarCollapseToggle, _BottomRail, _EditorDragHandle(+Intent), _WelcomeOverlay.
Order: mechanical first (hat bar, switcher rows, welcome overlay), then root shell, then slot host (tangled: _SlotHostState registers focus scopes via ClideKernel.of in didChangeDependencies), then layout/status.
Tests importing app.dart: test/app_test.dart (RootLayout, StatusbarHost, ClideApp), test/app_statusbar_test.dart (StatusbarHost), test/app_collapse_toggle_test.dart (StatusbarCollapseToggle) re-exports keep them unchanged.
CORRECTION to fable-ous.md: ColumnHat is NOT duplicated line-for-line in app.dart (verified); ColumnHat lives only in lib/widgets/src/clide_column_hat.dart. The rat-sweep ticket T-385 was annotated accordingly verify whether ColumnHat is dead before deleting.', NULL, '2026-06-11 22:09:19', '2026-06-11 22:09:19', '2026-06-11 22:09:19', NULL, '4e73e28db0c7d196d2e6fec455c87a1a', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'description', 'Verified-dead code worth one sweep (coverage denominator benefits too):
- Legacy free-function git API (~250 LOC duplicating GitClient, kept alive only by its own tests, and carrying its own latent pipe-deadlock bug) delete API + tests.
- ToolCheck zero callers.
- ~60% of lib/src/pty/ffi/libc.dart fd-passing-era bindings unused since D-56.
- GraphView unreachable placeholder (note: the Governance Graph idea (see Q-records from this review) may later want the slot; deleting now is still right, it is a 17-line stub).
- ColumnHat duplicated line-for-line in app.dart, kept alive by a zero-coverage test; the app.dart split ticket removes the duplicate, this sweep removes the orphan.
- tmux-era team pipeline: TranscriptPublisher, TeamMemberJoined nothing emits these events, yet the team roster UI listens to them exclusively (team tiles are populated by ghosts). Remove pipeline + dead listeners; if the roster UI stays, it needs a real data source first (surface that before deleting the UI).
- Dead ptyc binary still committed in native/linux-x64/ against D-62/D-63 remove binary + licenses.yaml entry if present.
- mocktail pinned, documented in D-25 as the IO-mocking strategy, imported by zero files: either adopt it where mocks are hand-rolled or drop the dep AND amend D-25.
Each bullet is one commit. Run make test + coverage after each; expect the floor to ratchet up.', 'Verified-dead code worth one sweep (coverage denominator benefits too):
- Legacy free-function git API (~250 LOC duplicating GitClient, kept alive only by its own tests, and carrying its own latent pipe-deadlock bug) delete API + tests.
- ToolCheck zero callers.
- ~60% of lib/src/pty/ffi/libc.dart fd-passing-era bindings unused since D-56.
- GraphView unreachable placeholder (note: the Governance Graph idea (see Q-records from this review) may later want the slot; deleting now is still right, it is a 17-line stub).
- ColumnHat duplicated line-for-line in app.dart, kept alive by a zero-coverage test; the app.dart split ticket removes the duplicate, this sweep removes the orphan.
- tmux-era team pipeline: TranscriptPublisher, TeamMemberJoined nothing emits these events, yet the team roster UI listens to them exclusively (team tiles are populated by ghosts). Remove pipeline + dead listeners; if the roster UI stays, it needs a real data source first (surface that before deleting the UI).
- Dead ptyc binary still committed in native/linux-x64/ against D-62/D-63 remove binary + licenses.yaml entry if present.
- mocktail pinned, documented in D-25 as the IO-mocking strategy, imported by zero files: either adopt it where mocks are hand-rolled or drop the dep AND amend D-25.
Each bullet is one commit. Run make test + coverage after each; expect the floor to ratchet up.
Correction (2026-06-12, verified during T-394 breakdown): ColumnHat is NOT duplicated line-for-line in app.dart it exists only in lib/widgets/src/clide_column_hat.dart. Before deleting it, verify it actually has zero non-test callers; if it is genuinely used by app chrome, drop that bullet from this sweep.', NULL, '2026-06-11 22:09:30', '2026-06-11 22:09:30', '2026-06-11 22:09:30', NULL, '96c700c252728e81f7477b0f18e68192', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'description', NULL, 'lib/builtin/claude/src/claude_meta_sidebar.dart is 1192 LOC. Split plan (verified against the file 2026-06-12). Public API (ClaudeMetaSidebar widget + SidebarTab enum) stays in the root file — tests and extension.dart need zero import changes.
Inventory: consts _labelColumnWidth/_rowPitch (43-44), SidebarTab enum (46), ClaudeMetaSidebar (48-79), _ClaudeMetaSidebarState monolith (81-638: lifecycle, stats polling, team membership streams, primary-session binding, broker subscription, config listener, inject state, accordion state, plus the three tab bodies), _ConfigSection/_ConfigPermKind enums (641/644), _MetaSection/_MetaRow models (646-657), _AgentRosterRow(+State) (680-928), _permissionModeBadge + _PermissionModeBadge T-181 (935-1009), _IconButton (1012-1039), _InjectTextField (1043-1070), _TaskRow T-171 (1077-1141), _TabStrip (1145-1192).
Target layout under lib/builtin/claude/src/meta_sidebar/: models.dart (enums + _MetaSection/_MetaRow + layout consts), activity_tab.dart (ActivityTabView ~70 LOC, from _activityBody/_runtimeSection 272-302), team_tab.dart (TeamTabView ~120 LOC, from _teamBody/_taskSection 304-380, props-driven with callbacks), config_tab.dart (ConfigTabView ~200 LOC, from _configBody family 382-597; accordion _expanded state stays in parent, passed as prop+callback), roster_row.dart (_AgentRosterRow ~250 LOC incl. bypass-confirm state), permission_badge.dart (~80), task_row.dart (~70), tab_strip.dart (~55), icon_button.dart + inject_field.dart (~30 each keep here initially; promote to lib/widgets/ only when a second consumer appears). Root file shrinks to ~150 LOC of lifecycle + event bindings + tab switch.
Execution order (each phase independently green): 1) primitives (icon button, inject field, permission badge, models); 2) stateless tab views (activity, config, tab strip) with prop threading; 3) team tab + roster row (most orchestrator coupling: verify show/hide, mute, inject submit-and-clear, shift-click bypass confirm, fork, close, task reassign cycle, auto-front on TeamMemberJoined at line 158); 4) cleanup of moved methods from the root state.
Tests: test/builtin/claude/claude_meta_sidebar_test.dart imports only the root file and public symbols no changes needed; no goldens reference the sidebar. Caveat from the rat sweep (T-385): TeamMemberJoined currently has no emitter coordinate before investing in the team tab plumbing.', NULL, '2026-06-11 22:09:55', '2026-06-11 22:09:55', '2026-06-11 22:09:55', NULL, '02f7234d8c395607fe5aa477e6bf291f', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RJ9K19P5AX14RHRR', 'description', 'parser.dart is a single 1139-line file containing the full ESC/CSI/OSC/DCS handler tree for the terminal emulator. Functional but unwieldy; the consultant flagged it as ''consider splitting'' in the T-107 review.
Suggested split (sequenced with the T-91 coverage sweep on lib/src/terminal/, so the split doesn''t fight in-flight test work):
- parser.dart entry point + state machine driver
- esc_handlers.dart single-char ESC dispatch table + handlers
- csi_handlers.dart CSI parameter parsing + handlers
- osc_handlers.dart OSC string handlers (title, colour set, etc.)
- dcs_handlers.dart DCS/SOS/PM/APC string handlers
Each handler module exports a registrar that the driver wires at construction.
Done when:
- parser.dart < 400 LOC
- All existing parser tests pass without changes
- No new public surface; everything stays library-private
Source: T-107 / consultants.md "Code quality — Findings — [Major]".', 'parser.dart is a single 1139-line file containing the full ESC/CSI/OSC/DCS handler tree for the terminal emulator. Functional but unwieldy; the consultant flagged it as ''consider splitting'' in the T-107 review.
Suggested split (sequenced with the T-91 coverage sweep on lib/src/terminal/, so the split doesn''t fight in-flight test work):
- parser.dart entry point + state machine driver
- esc_handlers.dart single-char ESC dispatch table + handlers
- csi_handlers.dart CSI parameter parsing + handlers
- osc_handlers.dart OSC string handlers (title, colour set, etc.)
- dcs_handlers.dart DCS/SOS/PM/APC string handlers
Each handler module exports a registrar that the driver wires at construction.
Done when:
- parser.dart < 400 LOC
- All existing parser tests pass without changes
- No new public surface; everything stays library-private
Source: T-107 / consultants.md "Code quality — Findings — [Major]".
Split breakdown from the 2026-06-11 Fable review (epic T-359), verified against the file 2026-06-12:
Structure today: main parser + routing (11-116), CSI core _escHandleCSI (196-209) + _consumeCsi (217-272), _csiHandlers table of 27 final bytes (274-304), cursor movement handlers (306-807), erase/scroll/line/char ops (809-945), SGR monolith (411-622, 212 LOC), mode set/reset (395-409, 946-1030), DA/DSR (334-343, 624-635), window manipulation (658-712), OSC (1034-1110), _Csi state object (1113-1131 note the commented-out `intermediates` field at 1117/1125).
Target layout under escape/: parser.dart keeps EscapeParser (queue, tokenization, top-level dispatch, ~400 LOC); csi_parser.dart (CsiSequence value object prefix, params, RESTORED intermediates, finalByte plus the consume logic from _consumeCsi); csi_handlers.dart (dispatch table, now keyed on final byte + intermediates); cursor_handlers.dart; sgr_handler.dart (the 411-622 monolith); mode_handler.dart; osc_parser.dart + osc_handlers.dart. EscapeHandler interface unchanged. Preserve the zero-allocation/reset-able design goal noted at lines 14-16.
Bug this split must fix (verified): _consumeCsi DISCARDS intermediate bytes lines 258-261 have `// intermediates.add(char);` commented out and `continue`, so CSI Ps SP q (DECSCUSR, cursor style) and CSI ! p / SP-intermediate forms dispatch on the bare final byte and fall to unknownCSI. Fix: restore the intermediates field on CsiSequence, capture them during consume, dispatch on (intermediates, finalByte), and add EscapeHandler.setCursorStyle for DECSCUSR. (DECSTR is CSI ! p soft terminal reset same intermediate mechanism.)
Related bug with its own ticket (T-369): unguarded params[i+1] lookahead in SGR 38/48 at lines ~502/512/547/557 (RangeError on truncated sequences) + colon-form sub-parameters unhandled. The split makes the fix natural: sgr_handler.dart owns guarded lookahead helpers; if T-369 lands first, carry its tests over; if this lands first, fix it inside sgr_handler.dart and close T-369 with it.
Tests: test/terminal/escape/parser_test.dart (786 LOC) splits along the same seams keep parser_test.dart for top-level dispatch/SBC/rollback, add csi_parser_test.dart (intermediates capture, DECSCUSR), sgr_handler_test.dart (bounds + colon form + 256/RGB), mode_handler_test.dart, osc_parser_test.dart, window/DA splits as convenient. The _RecordingHandler fixture is reusable across all of them.', NULL, '2026-06-11 22:10:19', '2026-06-11 22:10:19', '2026-06-11 22:10:19', NULL, '2942deccaff5b534ac3e33ac23b4d9fa', 2) ON CONFLICT(hash) DO NOTHING;
+303
View File
@@ -3670,3 +3670,306 @@ Claude Code CLI executes every Bash tool itself; clide only ingests stream-json
- Expanding a Bash card whose command tails a file shows a live scrolling terminal sub-card that updates as the file grows; collapsing it tears the follower down.
- A Bash card with no file-backed source shows a clear "nothing to follow" affordance, never a broken/empty terminal.
- No follower process is spawned until the card is expanded.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 12:00:52', '2026-06-11 17:09:12', NULL, '050a31bfe1ef9cf220f306ab3922f84c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBGHNEQTAEPGNJKN42C1E8', 'epic', NULL, 'Address 2026-06-11 Fable review (fable-ous.md)', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:54:36', '2026-06-11 21:54:36', NULL, 'a4819e309facb7a990372e7e2eec2561', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBGHNEQTAEPGNJKN42C1E8', 'epic', NULL, 'Address 2026-06-11 Fable review (fable-ous.md)', 'Thirteen parallel subsystem reviewers + adversarial verification over ~58k LOC produced ~14 high-severity and ~35 medium findings plus systemic patterns and a feature backlog. Source: fable-ous.md (committed alongside this epic). Children are filed individually so they can land independently; feature proposals went to governance/questions as Q-records, not tickets.
Acceptance: all dragon (high-severity) findings resolved or formally rejected with a D-record; scorpions shipped or moved to follow-up work with rationale; rat-extermination and systemic-pattern batches closed; god-file split plans written into their tickets.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:54:36', '2026-06-11 21:54:44', NULL, '77ba1d6c54e7c950dd8eac675ce91d4f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBJ5T7HAQ9CA8XQMX43A2C', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'PTY master fd leaks on every natural child exit', NULL, 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:54:49', '2026-06-11 21:54:49', NULL, '925f4826638fbf190a370b08892d47cd', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBJ5T7HAQ9CA8XQMX43A2C', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'PTY master fd leaks on every natural child exit', 'lib/src/pty/native_pty.dart:444-450 — on child EOF, _reap() sets _dead = true but never closes _fd; a later close() short-circuits at `if (_dead) return;` (line ~460) so _nativeClose(_fd) (line ~477) never runs. Every terminal/Claude pane whose child exits on its own leaks an fd and a pty device for the life of the app. Two independent reviewers confirmed.
Fix: close the master fd in the natural-exit path (or let close() proceed to fd teardown when dead). Add a test asserting the fd is released after child EOF.', 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:54:49', '2026-06-11 21:54:56', NULL, '0ce16b345f5e129abea63d6314b61bad', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBKK2TZQK683J8FS0ZH5A4', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Claude child process observed only via stdout: stderr never drained, exit never watched', NULL, 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:01', '2026-06-11 21:55:01', NULL, '70beb5d53ac502257e86c82b520886f5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBKK2TZQK683J8FS0ZH5A4', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Claude child process observed only via stdout: stderr never drained, exit never watched', 'lib/builtin/claude/src/stream_json_session.dart:43-78 — the claude child stderr is never drained: >=64KB of --verbose spew fills the pipe, the child blocks mid-turn, and the flagship pane wedges with zero diagnostics. Nothing watches exitCode or stdout onDone (line ~303), so a crashed/dead session just looks busy.
Fix: drain stderr into a bounded ring buffer (surface it on failure), watch exitCode/onDone, and emit a terminal SessionEnded state to the pane. Intersects T-283 (resume hang has no timeout/fallback). Tests: dead-child surfaces SessionEnded; stderr flood does not wedge the session.', 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:01', '2026-06-11 21:55:07', NULL, 'b9048d2475722e1d364044346f61cc2f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'MCP HTTP server exposes the full dispatcher with zero auth', NULL, 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:13', '2026-06-11 21:55:13', NULL, 'f4f3b80272a12a8ec2aa561a353c0249', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'MCP HTTP server exposes the full dispatcher with zero auth', 'lib/src/ipc/mcp_server.dart:138-195, started unconditionally at boot (lib/main.dart:174-180). D-71 threat model (another user on the same host must not drive my IDE) is enforced with 0600 on the unix socket — then bypassed wholesale by an unauthenticated localhost HTTP port that, since D-86, serves every clide verb as a tool.
Fix: generate a token in the lock file (Claude Code /ide lock format has a slot for it) and require the auth header on every request. Tests: request without token is rejected; token round-trips via the lock file.', 'backlog', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:13', '2026-06-11 21:55:21', NULL, '50afa532cebd52e717eb790ad77f705e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'editor.open/editor.save skip path confinement entirely', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:26', '2026-06-11 21:55:26', NULL, 'dd2945aa9bd4bb2b4a908790243170c7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'editor.open/editor.save skip path confinement entirely', 'lib/src/editor/registry.dart:215-219 returns absolute paths verbatim — no .. normalization, no path_safety call — an unconfined read AND write primitive over IPC while files.read is carefully guarded.
Fix: route editor.open/editor.save through path_safety like files.*. Tests: traversal and absolute-escape attempts rejected for both verbs. Longer-term the confinement should move to the dispatch layer (see the systemic ticket filed with this epic).', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:26', '2026-06-11 21:55:33', NULL, 'e6ce37ceaff954053227d353033886f1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'search.replace silently ignores its include/exclude globs', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:38', '2026-06-11 21:55:38', NULL, '674a5d3b1a455b9f03a0a2383f7ca4e3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'search.replace silently ignores its include/exclude globs', 'lib/src/search/replace_engine.dart:124-143 — the include/exclude glob filters the user typed are accepted but never applied; replace will happily rewrite files outside the filter.
Fix: apply the same glob filtering the search side uses before rewriting. Test: replace with an include glob touches only matching files; exclude glob is honored.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:38', '2026-06-11 21:55:44', NULL, '749e1d21bafd775069eb747233632779', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBSG6356MZJ2DCCCSBMBGM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'listDir symlink detection is dead code; walkFiles descends symlinked dirs', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:49', '2026-06-11 21:55:49', NULL, '468bd54e7a036cd26fa707df54c4ea97', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBSG6356MZJ2DCCCSBMBGM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'listDir symlink detection is dead code; walkFiles descends symlinked dirs', 'lib/src/files/listing.dart:46-54 — stat() follows links, so isSymlink is always false; walkFiles therefore descends symlinked directories the docs claim it skips (escape hatch out of the workspace, plus cycle risk).
Fix: use lstat (FileStat via Link check / FileSystemEntity.isLinkSync on the raw path) for symlink detection. Tests: symlinked dir is reported as symlink and not descended; symlink cycle does not hang the walk.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:55:49', '2026-06-11 21:55:56', NULL, '2fc8186724d131d03b43a68e46dd5f70', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBV0465906BY3QFAY9F1YM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Kernel lookup in dispose() leaks shell PTYs and settings listeners', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:01', '2026-06-11 21:56:01', NULL, 'e0ff148be03a173e407a256ba72d19b6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBV0465906BY3QFAY9F1YM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Kernel lookup in dispose() leaks shell PTYs and settings listeners', 'lib/builtin/terminal/src/terminal_pane.dart:131-137 calls ClideKernel.of(context) from dispose() — illegal ancestor lookup, swallowed by catch (_) — so pane.close is never sent and the backend PTY + daemon pane leak on every closed terminal pane. The same idiom leaks the settings listener in every disposed ClaudePane (lib/builtin/claude/src/claude_pane.dart:460-466).
Fix: cache the kernel ref in didChangeDependencies, delete the catch-alls. Combined with the PTY natural-exit fd leak this is a two-stage leak pipeline. Tests: closing a terminal pane sends pane.close; disposing a ClaudePane removes its settings listener.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:01', '2026-06-11 21:56:09', NULL, '4f703d28761e63c84de96abdee86cd9e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBWE2W1226T58CX37E50HC', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Project switch leaks the entire previous workspace service set', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:13', '2026-06-11 21:56:13', NULL, '33ae9e7ac573f6d207297ca6f7758c98', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBWE2W1226T58CX37E50HC', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Project switch leaks the entire previous workspace service set', 'lib/main.dart:335-344 — switching projects builds a new dispatcher with fresh PaneRegistry, FilesService, EditorRegistry, etc., but nothing calls the old set''s shutdown() methods (which exist and have zero callers). Old file watchers keep emitting into the new workspace''s bus.
Fix: dispose/shutdown the previous service set before (or while) standing up the new one. Test: after a workspace switch, the old FilesService watcher no longer delivers events.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:13', '2026-06-11 21:56:19', NULL, 'dc274dbd0cd8573daae37acfcf54b6f9', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W6VN98RQM6S22X28', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Claude bottom status bar often stays empty / doesn''t update', 'The bottom status-bar context slot for the Claude pane (model · permission-mode · context, T-145/T-150) frequently renders empty and doesn''t update. The slot is fed by the active pane''s status widget via the focus service (PaneContextStatusItem -> ClidePane.statusWidget in claude_pane.dart), sourced from StreamJsonSession.statusStream (model/permissionMode from the ''system/init'' event; cost/contextWindow from ''result'' events in stream_json_session.dart).
Repro: open clide; the status line is often blank and stays blank until/unless a turn completes (or never populates).
Likely suspects to investigate:
- status only published while the pane is the focused contribution (active==true) if focus isn''t on the Claude pane, the slot clears.
- statusStream may not emit until the first ''result''/''init'' event; a resumed session (--resume) may not re-emit init, so model/mode never arrive.
- _statusWidget returns null when _status.isEmpty AND no skills, so an unstarted/!init session shows nothing.
- focus-slot wiring (FocusTracker) may not re-publish on pane (re)build / session rebind.
Acceptance: the Claude status line shows model · mode · context promptly after a session starts/resumes and stays current across turns and focus changes; add a test covering the resumed-session (no fresh init) case.
Root cause found and verified by the 2026-06-11 Fable review (fable-ous.md, epic T-359): statusStream is a plain broadcast controller the system/init event fires while spawn() is still awaiting a 256KB transcript-tail read, before the pane ever subscribes (lib/builtin/claude/src/claude_pane.dart:322, lib/builtin/claude/src/session_orchestrator.dart:222-225). Fix: seed from session.status on bind, or make the stream replay-latest (see the ValueStream systemic ticket under T-359).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-08 07:26:13', '2026-06-11 21:56:28', NULL, '1e65d7073c7407585c2e6a79c330881e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBYTJ4E7ZBY6DWWNT1S16M', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Conversation auto-scroll yanks a scrolled-up reader to the bottom on every streamed token', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:33', '2026-06-11 21:56:33', NULL, '4792af8e6089499f70a87245d351573d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBYTJ4E7ZBY6DWWNT1S16M', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Conversation auto-scroll yanks a scrolled-up reader to the bottom on every streamed token', 'lib/builtin/claude/src/conversation_view.dart:268-277 — the _atBottom pin exists but is only consulted on viewport resize, not on new items. Anyone reading earlier output during a long streaming reply is dragged to the bottom continuously.
Fix: gate the new-item auto-scroll on _atBottom (one-line) and add the missing twin test: scrolled-up viewport stays put when items stream in; at-bottom viewport follows.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:33', '2026-06-11 21:56:40', NULL, '9efd0c7170841e18d9e21e29e3d75705', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC0HYRZ86CWW0DDQJ5CAQM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal throws RangeError on truncated SGR 38/48; colon sub-parameters mangled', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:47', '2026-06-11 21:56:47', NULL, '9ab6c8b23458dd442a74791765541d0e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC0HYRZ86CWW0DDQJ5CAQM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal throws RangeError on truncated SGR 38/48; colon sub-parameters mangled', 'lib/src/terminal/src/core/escape/parser.dart:501-516 — SGR 38/48 extended-color parsing does unguarded params[i + 1] lookahead, so a truncated sequence like printf ''\e[38m'' throws RangeError inside Terminal.write. An emulator must never throw on hostile bytes. Secondary, same code path: colon-form SGR sub-parameters (e.g. 38:2:r:g:b, emitted by modern terminfo) are not split out and get mangled into bogus params.
Fix: bounds-check the lookahead (ignore incomplete 38/48 sequences), and parse colon-form sub-parameters per ECMA-48/ITU T.416 treat 38:2::r:g:b and 38;2;r;g;b equivalently. Note T-123 (parser split) touches the same file; coordinate but do not block on it.
Acceptance: feeding any truncated/garbled SGR byte sequence never throws (fuzz-style test over partial sequences); colon-form truecolor sets the same fg/bg as semicolon form; existing SGR tests stay green.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:56:47', '2026-06-11 21:56:59', NULL, 'ee56f737a2da9d931af86807ea41db91', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC2NM7AYKENZ0ZD49HAX1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'ClideCollapserCard excludeSemantics wipes expanded children from the a11y tree', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:57:04', '2026-06-11 21:57:04', NULL, 'bbae48853b49cec865195440f76644d2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC2NM7AYKENZ0ZD49HAX1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'ClideCollapserCard excludeSemantics wipes expanded children from the a11y tree', 'lib/widgets/src/clide_collapser_card.dart:92-101 — excludeSemantics: true on the card wipes every expanded child from the a11y tree: a screen-reader user can expand a run/tool card and hear nothing inside it. A11y is a Tier-0 contract in this repo, so this is a contract breach, not polish.
Fix: exclude semantics only while collapsed (or scope the exclusion to the chrome, not the body); keep the header announcing expanded/collapsed state.
Acceptance: semantics test asserting expanded-card children are present in the semantics tree and absent (or summarized) when collapsed; make test-a11y green.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:57:04', '2026-06-11 21:57:12', NULL, 'f9c5330a7954c66ad65efea30ac5539f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC46SEHH8NQY481VMGK66R', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'A11y/i18n/theme gates hand-enumerate their subjects and have fallen behind lib/', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:57:17', '2026-06-11 21:57:17', NULL, 'bfb099275caa67eaae14c141f178dd3c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC46SEHH8NQY481VMGK66R', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'A11y/i18n/theme gates hand-enumerate their subjects and have fallen behind lib/', 'The three a11y gate tests hand-enumerate their subjects and have measurably drifted from lib/: the contrast gate checks fewer themes than lib/main.dart:443-454 actually loads (catppuccin is silently unvalidated — the same theme list also drifted between main.dart and the testmode harness), and the i18n gate checks 4 of 8 namespaces. The gates stay green while covering less — worst kind of drift.
Fix (pattern: hand-enumerated lists drift; export the truth): make lib/ export one canonical const each for bundled themes, a11y gate subjects, and i18n namespaces; the gates and the testmode harness iterate those exports instead of their own lists. Add a meta-assertion where feasible (e.g. namespace list derived from the assets dir at test time) so a new theme/namespace cannot ship unvalidated.
Acceptance: gates fail if a newly added theme/namespace is not covered; catppuccin contrast-checked; all 8 i18n namespaces checked; testmode harness consumes the same exported theme list.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 21:57:17', '2026-06-11 21:57:26', NULL, '290bcc744fe4b90080d3617048018a81', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC5ZE4EZEGXK8YY8J86CM0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'IPC server dispatch is not serial; shared StringBuffer framing can drop/double lines', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:31', '2026-06-11 21:57:31', NULL, '7a2cfcdde5be66cff55589267aa01d54', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC5ZE4EZEGXK8YY8J86CM0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'IPC server dispatch is not serial; shared StringBuffer framing can drop/double lines', 'lib/src/ipc/server.dart:151-180 — D-72 promises serial dispatch, but the async onData handler never pauses the subscription, so pipelined requests interleave; the shared StringBuffer framing can also drop or double lines when chunks split mid-frame, and per-chunk utf8 decode corrupts multi-byte characters split across chunks.
Fix in one move: client.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()) consumed with await for gives correct framing, persistent UTF-8 decoding, and true serialization at once.
Acceptance: test sending two pipelined requests in a single write (responses arrive in order, both handled); test a request split mid-UTF-8-rune across two socket writes; existing IPC tests stay green. Runs under dart test keep imports Flutter-free.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:31', '2026-06-11 21:57:40', NULL, '4235607b75073718a6cba068cfde7c42', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC7KDFW07S8WTCC3MD71J0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split-chunk UTF-8 corruption at byte-to-String seams: terminal write(String), FileTailFollower', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:44', '2026-06-11 21:57:44', NULL, '454708a914320a3286f49db1d2ac7319', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC7KDFW07S8WTCC3MD71J0', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split-chunk UTF-8 corruption at byte-to-String seams: terminal write(String), FileTailFollower', 'The terminal''s only ingestion API is write(String) (lib/src/terminal/src/terminal.dart:218), so both consumers decode bytes per-chunk — a multi-byte rune split across PTY reads renders as U+FFFD garbage. FileTailFollower starts reading mid-file by construction, so it can begin mid-character too.
Fix: add Terminal.writeBytes(List<int>) backed by a persistent chunked Utf8Decoder (allowMalformed) per terminal instance; migrate the PTY consumer and FileTailFollower to it. Keep write(String) for tests/programmatic use.
Acceptance: test feeding a multi-byte rune split across two writeBytes calls renders one glyph; FileTailFollower starting mid-rune resyncs without emitting replacement chars mid-stream.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:44', '2026-06-11 21:57:52', NULL, '2960aaecae3dfdedf5a660440e0f6d84', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC90B72A270CAKA7AP1ZX8', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Orchestrator.spawn() races itself — concurrent spawns for one id leak a live claude process', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:56', '2026-06-11 21:57:56', NULL, 'e2db785a676eadbf9dd3b78a655853f3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC90B72A270CAKA7AP1ZX8', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Orchestrator.spawn() races itself — concurrent spawns for one id leak a live claude process', 'lib/builtin/claude/src/session_orchestrator.dart:191-249 — spawn() does check-then-act on the sessions map across two awaits (transcript-tail read, process start). Two concurrent spawns for the same session id both pass the check; the loser''s live claude process is orphaned, never killed, never observed.
Fix: hold a Map<String, Future<ManagedSession>> first caller installs the future synchronously, later callers await the same future; remove the entry on failure.
Acceptance: test issuing two concurrent spawn() calls for one id yields the same ManagedSession instance and exactly one process spawn (count via injected spawner); failure path clears the in-flight entry so a retry can proceed.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:57:56', '2026-06-11 21:58:03', NULL, '79ae3a9297cb05e9afb8c22a01bf4ea4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCAFKK334YNJXZJQG4J6AW', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Fork panes: widget.forkSourceId wins forever — /clear re-forks the original conversation', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:08', '2026-06-11 21:58:08', NULL, '95294be3de8244f6177eb3bfc1f89abb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCAFKK334YNJXZJQG4J6AW', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Fork panes: widget.forkSourceId wins forever — /clear re-forks the original conversation', 'lib/builtin/claude/src/claude_pane.dart:266-281 — when (re)binding a session, widget.forkSourceId takes precedence forever, so /clear in a fork pane re-forks the original conversation instead of clearing, and /resume and /fork misbehave the same way. Related context: clide owns /clear, /resume, /compact interception (T-156); panes pin a session id.
Fix: treat forkSourceId as a one-shot spawn parameter consume it on first bind (clear it into pane state), so subsequent session-mutating commands operate on the pane''s live session.
Acceptance: test that a fork pane after /clear starts an empty session (no fork source passed to the orchestrator on respawn); first bind still forks from the source.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:08', '2026-06-11 21:58:16', NULL, '7ee9fdb25f18eee232aa4c9a968ea22c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCC6AR37VTF4SY8DR99JHC', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Settings persistence corrupts maps-inside-lists on write; non-atomic; parse failure silently resets', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:22', '2026-06-11 21:58:22', NULL, '21145621090d4acc5bbd8502de4e218e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCC6AR37VTF4SY8DR99JHC', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Settings persistence corrupts maps-inside-lists on write; non-atomic; parse failure silently resets', 'lib/kernel/src/settings.dart:199-219 — the writer emits toString() for maps nested inside lists, corrupting them on the next read; this breaks the documented keymap overlay across restarts. Writes are also non-atomic (a crash mid-write truncates the file), and a parse failure on load silently resets ALL settings instead of preserving the file and surfacing the error.
Fix: serialize with a real encoder (JSON/YAML emitter, whatever the file format is) covering nested structures; write to a temp file + rename for atomicity; on parse failure keep the original file (e.g. move aside as .broken) and log via the kernel Logger instead of resetting.
Acceptance: round-trip test for a keymap overlay (list of maps) across save/load; simulated partial write leaves previous settings intact; corrupt file does not silently reset and produces a logged diagnostic.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:22', '2026-06-11 21:58:33', NULL, 'c1f63e92d9c3708ee09c54a1b3ec9010', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCEC25337J2AXXQNST56Y4', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Extension activation is not transactional; disable ignores dependents; registries clobber on id collision', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:40', '2026-06-11 21:58:40', NULL, '3989745ea0ab1fbca279115f5e9a5088', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCEC25337J2AXXQNST56Y4', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Extension activation is not transactional; disable ignores dependents; registries clobber on id collision', 'lib/kernel/src/extensions_manager.dart:133-192 — a throw mid-contribution leaves earlier contributions mounted while the extension records as failed; a retry then double-applies them. Also: disabling an extension ignores extensions that depend on it, and contribution registries silently clobber on id collision. Benign among curated builtins; hazardous the day Tier-6 Lua extensions (T-8) land.
Fix: make activation transactional collect contributions, mount only after the extension activates cleanly, and unwind mounted ones on failure; disable refuses (or cascades, pick one and record it) when dependents are active; registries reject or namespace duplicate ids with a logged diagnostic.
Acceptance: test that an extension throwing mid-activation leaves zero contributions mounted and can retry cleanly; disable-with-dependents behaves per the chosen rule; duplicate contribution id surfaces an error instead of clobbering.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:40', '2026-06-11 21:58:49', NULL, '48334c3389949474ff1f341534126001', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCG84F4SPFW111CC4K26A8', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal conformance debt: HTS no-op, DECCKM unconsumed, mouse rows off-by-one, CPR 0-based, scrollback unreachable', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:55', '2026-06-11 21:58:55', NULL, 'a4f9ba0068e58b70a3567e1704dc1053', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCG84F4SPFW111CC4K26A8', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal conformance debt: HTS no-op, DECCKM unconsumed, mouse rows off-by-one, CPR 0-based, scrollback unreachable', 'The owned terminal fork fixes what it trips over but has no vttest-style conformance suite. Verified debt:
- HTS (set tab stop) is a no-op: calls isSetAt instead of setAt (lib/src/terminal/src/terminal.dart:423).
- DECCKM (cursor-keys application mode) is tracked but never consumed when encoding arrow-key input.
- Legacy X10/X11 mouse reporting rows are off-by-one AND the existing test enshrines the bug fix both together.
- CPR (cursor position report) replies 0-based where every real terminal is 1-based.
- Scrollback is maintained but structurally unreachable: ViewportOffset.zero() is pinned on every build (lib/src/terminal/src/terminal_view.dart:224), so no scrolling UI path exists. (Blocks the semantic-terminal feature idea; fix is a prerequisite there.)
Approach: fix each item with a focused conformance test (vttest-style expectations); consider starting a small conformance_test.dart suite that future escape-sequence work extends. Coordinate with T-123 (parser split) and T-369 (SGR crash) which touch the same area.
Acceptance: each of the five items has a test that fails on the old behavior and passes after; mouse test corrected, not deleted.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:58:55', '2026-06-11 21:59:05', NULL, '0a7b313f101913232e0de3d528aa8be7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCJEYHC91PMVNVWVHBR2RG', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Markdown renderer: hard breaks and images render as empty text', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 21:59:13', NULL, '73cdfc05da40b52f2d8712f589ba3734', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCJEYHC91PMVNVWVHBR2RG', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Markdown renderer: hard breaks and images render as empty text', 'lib/widgets/src/clide_markdown.dart:408-410 — hard line breaks and image nodes both fall through to empty text spans: words on either side of a hard break glue together, and images vanish entirely (no placeholder, no alt text).
Fix: emit a newline span for hard breaks; render images as at least an alt-text placeholder chip (full image rendering can be a follow-up note the existing feedback that live-pane images go through clide image show).
Acceptance: golden/widget test for hard-break line splitting; image node renders alt text; no regression in existing markdown goldens.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:13', '2026-06-11 21:59:20', NULL, 'eec49027f1c3e355070f5e36d0fcbc72', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCM1RBAF72SCZBKRTXJSYC', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Streaming render path is O(n²): full re-parse + sync existsSync in build, O(n) re-derive per notify, full re-encode per delta', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:26', '2026-06-11 21:59:26', NULL, 'aa0a2a2ca4769d495fb48ae91d7340bd', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCM1RBAF72SCZBKRTXJSYC', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Streaming render path is O(n²): full re-parse + sync existsSync in build, O(n) re-derive per notify, full re-encode per delta', 'Three compounding costs on every streamed token:
1. The whole markdown document re-parses inside build on each streaming delta, including sync existsSync() calls in the parse path (clide_markdown.dart) sync I/O on the UI isolate per frame.
2. The conversation view re-derives its full item list O(n) on every controller notification.
3. Token streaming re-encodes the full reply text per delta (lib/builtin/claude/src/stream_json_session.dart:410-431) O(n²) churn over a long reply.
Fix directions: cache parsed markdown per card keyed by content hash and only re-parse the tail/dirty card; move file-existence link checks off the build path (async + cache); make the session accumulate deltas in a StringBuffer instead of string concat re-encode; derive conversation items incrementally. Profile before/after with a long synthetic reply.
Acceptance: a 1000-delta synthetic stream does no existsSync in build (assert via injected fs seam or profiling harness), and per-delta work is bounded (no full-document re-parse); existing rendering tests green.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:26', '2026-06-11 21:59:37', NULL, 'ea41886fb8c4e6768980bacea594b182', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal panes spawn in Directory.current, not the open project root', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:42', '2026-06-11 21:59:42', NULL, 'fbee567d26a6c3bd5ee068ffbdd92a8d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Terminal panes spawn in Directory.current, not the open project root', 'lib/builtin/terminal/src/terminal_pane.dart:69 — the shell spawns with Directory.current as cwd. Launched from a desktop entry, that is $HOME, not the open workspace; after a project switch it is whatever the process started in. SpawnSpec.cwd already exists in the PTY layer.
Fix: pass the active workspace root as the spawn cwd (and re-derive it on project switch for new panes).
Acceptance: test that a terminal pane''s SpawnSpec.cwd equals the workspace root, not Directory.current.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:42', '2026-06-11 21:59:50', NULL, 'cec13ab104ecf333fdf67ebbfc09a067', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Notifications service renders nowhere — warnings vanish into an unrendered list', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:55', '2026-06-11 21:59:55', NULL, 'f25cab7fb88724093d582cc9d72e3976', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Notifications service renders nowhere — warnings vanish into an unrendered list', 'lib/kernel/src/notify.dart has zero widget consumers — anything pushed through the Notifications service (e.g. cli_install''s dogfood warnings) accumulates in a list no surface renders. ToastService exists right next to it and does render.
Fix options (pick one, note it on this ticket): (a) route notify-level messages through ToastService with severity styling; (b) add a notifications tray/indicator surface; (c) delete the service and migrate callers to toasts. Option (a) or (c) is likely right for current scale avoid building a tray nobody asked for.
Acceptance: a notification posted by cli_install is visibly surfaced in the UI (test via whichever surface is chosen); no silent sink remains.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 21:59:55', '2026-06-11 22:00:05', NULL, '7fd6794ef2583587f29d082af5796bac', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCST6CQ449VJGAP6C5KZ5W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Welcome screen no-ops: Clone-from-git and Start-a-Claude-session tiles do nothing; advertised shortcuts don''t exist', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:00:14', '2026-06-11 22:00:14', NULL, '8b87dd014b7e1925a59c3f9e0f9869ef', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCST6CQ449VJGAP6C5KZ5W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Welcome screen no-ops: Clone-from-git and Start-a-Claude-session tiles do nothing; advertised shortcuts don''t exist', 'lib/builtin/welcome/src/welcome_view.dart:173-174 — the Clone-from-git and Start-a-Claude-session tiles are inert on tap, and the keyboard shortcuts printed on the tiles are not registered anywhere. First-run users hit dead UI on the first screen.
Fix: either wire the tiles (clone flow; open a Claude pane) and register the shortcuts through the keymap subsystem, or remove the tiles until the flows exist no advertised dead ends. Note: the welcome screen also duplicates FileActions'' open-folder flow verbatim; the dedup is covered by the copy-paste sweep ticket under this epic, but if you touch this file, prefer calling into FileActions.
Acceptance: every tile on the welcome screen performs its action (widget test taps each); every shortcut shown is registered in the keymap.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:00:14', '2026-06-11 22:00:22', NULL, '8c66b985baf074abdc23d761ce10abae', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCVPGCKEGDC54KKQ120SRM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'make test-e2e / ui-dev / ui-smoke are dead (tools/ui cd into removed app/); staged Gitea CI would fail on activation', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:00:29', '2026-06-11 22:00:29', NULL, '40c383af82388001daad2e99ee9d046e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCVPGCKEGDC54KKQ120SRM', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'make test-e2e / ui-dev / ui-smoke are dead (tools/ui cd into removed app/); staged Gitea CI would fail on activation', 'tools/ui/*.sh still cd into the removed app/ directory, so make test-e2e, make ui-dev, and make ui-smoke fail immediately. The staged Gitea CI workflow would fail in three independent ways the day it is activated, while D-32 describes it as ready.
Fix: repoint the scripts at the repo root (post app/-flattening layout), run each target to prove it, and walk the Gitea workflow steps locally (or in a dry-run) until each step is green or consciously removed. Amend D-32 if the CI story changed.
Acceptance: all three make targets run; the workflow file''s steps each map to a working make target; D-32 matches reality.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:00:29', '2026-06-11 22:00:37', NULL, 'c519d8d5e20db12785db272799738638', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Rat extermination: dead-code sweep (legacy git API, ToolCheck, libc, GraphView, ColumnHat, tmux-era team pipeline, ptyc binary, mocktail)', NULL, 'backlog', 'low', NULL, NULL, NULL, '2026-06-11 22:00:44', '2026-06-11 22:00:44', NULL, '940a9f2b3f061d4701f2eb86cb37f10e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Rat extermination: dead-code sweep (legacy git API, ToolCheck, libc, GraphView, ColumnHat, tmux-era team pipeline, ptyc binary, mocktail)', 'Verified-dead code worth one sweep (coverage denominator benefits too):
- Legacy free-function git API (~250 LOC duplicating GitClient, kept alive only by its own tests, and carrying its own latent pipe-deadlock bug) delete API + tests.
- ToolCheck zero callers.
- ~60% of lib/src/pty/ffi/libc.dart fd-passing-era bindings unused since D-56.
- GraphView unreachable placeholder (note: the Governance Graph idea (see Q-records from this review) may later want the slot; deleting now is still right, it is a 17-line stub).
- ColumnHat duplicated line-for-line in app.dart, kept alive by a zero-coverage test; the app.dart split ticket removes the duplicate, this sweep removes the orphan.
- tmux-era team pipeline: TranscriptPublisher, TeamMemberJoined nothing emits these events, yet the team roster UI listens to them exclusively (team tiles are populated by ghosts). Remove pipeline + dead listeners; if the roster UI stays, it needs a real data source first (surface that before deleting the UI).
- Dead ptyc binary still committed in native/linux-x64/ against D-62/D-63 remove binary + licenses.yaml entry if present.
- mocktail pinned, documented in D-25 as the IO-mocking strategy, imported by zero files: either adopt it where mocks are hand-rolled or drop the dep AND amend D-25.
Each bullet is one commit. Run make test + coverage after each; expect the floor to ratchet up.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-11 22:00:44', '2026-06-11 22:01:02', NULL, '7eda667d2a552b742eb9a8d7d6069f0c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'ValueStream (replay-latest) wrapper; retrofit statusStream, busyStream, pendingPromptStream', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 22:01:07', '2026-06-11 22:01:07', NULL, '0fc9a635cb4884d5974e8675baff028e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'ValueStream (replay-latest) wrapper; retrofit statusStream, busyStream, pendingPromptStream', 'Broadcast streams that carry STATE (not events) drop the current value for late subscribers. This one shape caused T-274 (status bar blank — root cause appended there), the meta sidebar''s manual compensation, and the prompt-stream''s initialData workaround.
Fix: write one small ValueStream<T> wrapper (a broadcast stream that replays the latest value to each new subscriber, plus a .value getter) in the kernel; retrofit statusStream, busyStream, and pendingPromptStream in the claude builtin; delete the per-site workarounds it obsoletes. No third-party dep (rxdart) prefer-zero-deps; the wrapper is ~30 LOC.
Acceptance: unit tests for the wrapper (late subscriber gets latest value; no value yet = no synthetic emit unless seeded); T-274 repro covered: subscribing after the init event still yields the status. Closing this should make T-274 fixable in one line at the call site.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 22:01:07', '2026-06-11 22:01:18', NULL, '3dd1b6a453892cb3c99e1fedb51cf371', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD2FWTDFYA00W4QXTE41M0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Ban catch (_) {} on I/O and lifecycle paths — sweep and log through kernel Logger', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:25', '2026-06-11 22:01:25', NULL, '7d028bfb1fd47d81c63b33e9ea931864', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD2FWTDFYA00W4QXTE41M0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Ban catch (_) {} on I/O and lifecycle paths — sweep and log through kernel Logger', 'The silent-swallow idiom turned an illegal-lookup-in-dispose into two resource leaks (T-366) and turned process-spawn failures into blank panes. Rule to apply: cleanup/teardown paths MAY swallow (with a comment saying why); spawn, read, and dispose-adjacent paths MUST log through the kernel Logger they already have access to.
Work: grep the tree for `catch (_)` and empty catch bodies; classify each site (cleanup vs load-bearing); add logging or rethrow on the load-bearing ones; leave a one-line justification comment on the legitimate swallows. Consider an analysis_options lint (avoid_catches_without_on_clauses or a custom assist) only if it can be enabled without suppressions per repo policy no lint suppressions without approval.
Acceptance: zero unjustified empty catches on spawn/read/dispose paths; each remaining swallow carries a why-comment; any new logging visible in the kernel log during testmode.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:25', '2026-06-11 22:01:37', NULL, '0831ea558c5ebf30d44b8a5b3167a4ee', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD4QYHYRTK0SGRZCBSHSQ0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Decide and apply the sync-I/O-in-async-handlers rule (new D-record; offload above a size threshold)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:43', '2026-06-11 22:01:43', NULL, 'bd614146db5d02385cf948333a489b99', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD4QYHYRTK0SGRZCBSHSQ0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Decide and apply the sync-I/O-in-async-handlers rule (new D-record; offload above a size threshold)', 'The single-isolate app does sync I/O inside async IPC handlers: files.read does a sync 10MB read; the replace engine reads and rewrites workspace files synchronously — while grep right next to it fans out to isolates per D-79. There is no recorded rule, so each new handler guesses.
Work: claim a D-record (pql decisions claim D architecture "sync I/O policy in IPC handlers") deciding the rule suggested: async File APIs by default in handlers; offload to an isolate above N KB (align N with the D-79 grep design); sync allowed only in pure-Dart test seams. Then apply it to files.read and replace_engine, citing the new D-NNN at each site.
Acceptance: D-record confirmed; files.read and search.replace no longer block the UI isolate on large files (test with a multi-MB fixture asserting the event loop stays responsive, e.g. a timer keeps firing).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:43', '2026-06-11 22:01:52', NULL, 'b0dfc4e32215d3500f671bce4342b965', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD6FQMXMQQQ877KMNCK6QC', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Move path confinement to the dispatch layer, keyed off the co-registered schema', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:57', '2026-06-11 22:01:57', NULL, '369ef0dc4b4fa905a8858388bce405e3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD6FQMXMQQQ877KMNCK6QC', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Move path confinement to the dispatch layer, keyed off the co-registered schema', 'Per-verb path confinement is a lottery: files.read remembered, search.replace half-remembered, editor.open/save forgot (T-363). The dispatcher registry (D-74) already co-registers a schema with every handler and the schema already knows which params are paths.
Fix: add a path-type marker to the schema layer (or derive from a naming convention recorded in the D-record amendment) and run path_safety confinement once in the dispatcher before the handler sees the request. Per-verb checks become defense-in-depth or get deleted.
Do after T-363 lands its point fix this is the structural follow-up that prevents the next forgotten verb. Amend D-74 with the confinement rule.
Acceptance: a registered verb with a path param automatically rejects traversal/escape without any handler code; a regression test registers a synthetic verb and proves confinement applies; existing verbs unchanged behavior for in-workspace paths.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:01:57', '2026-06-11 22:02:08', NULL, '41cf2ffe1acca214218b6804b73ae191', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD8F2NBEFZNPKSJ9W253J0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Copy-paste dedup sweep: welcome/FileActions, palette/quick-open twins, three tail-followers, five _userErr, five git sandboxes, two ANSI flag enums', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:14', '2026-06-11 22:02:14', NULL, '445a9ac5a5b22f8d2efce7116a08a8ae', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD8F2NBEFZNPKSJ9W253J0', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Copy-paste dedup sweep: welcome/FileActions, palette/quick-open twins, three tail-followers, five _userErr, five git sandboxes, two ANSI flag enums', 'Verified duplication clusters, each small, together the repo''s main rot vector:
- Welcome screen clones FileActions'' entire open-folder flow verbatim call FileActions instead (coordinate with T-383).
- Command palette and quick-open are ~230-line near-twins extract the shared list-overlay+filter core.
- Three private tail-a-growing-file implementations in the claude builtin alone one shared follower (coordinate with T-373, which gives it the chunked decoder).
- Five hand-rolled _userErr helpers across handlers one shared error-shaping helper.
- Five copy-pasted git test sandboxes, none isolating host git config (set GIT_CONFIG_GLOBAL/HOME in the shared fixture flaky-test risk today).
- Two parallel ANSI flag enums that already drifted: strikethrough is stored but never painted unify, then paint strikethrough.
One commit per cluster. Acceptance: each cluster has a single implementation with the call sites migrated and a test guarding the shared piece; git sandboxes isolated from host config.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:14', '2026-06-11 22:02:25', NULL, '5f5034f42fcf5741805d8d4ef81e6376', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDAK04ZA0PBT69ZWNBXPSR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'claude builtin returns ok with an error payload in 16 handlers — honor the D-6 exit-code contract', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:31', '2026-06-11 22:02:31', NULL, '6aaf2b8af3b6596bcf35b13bcdc0a2b5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDAK04ZA0PBT69ZWNBXPSR', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'claude builtin returns ok with an error payload in 16 handlers — honor the D-6 exit-code contract', '16 handlers in the claude builtin return result: ok with an error field in the payload instead of an error envelope, drifting from the D-6 exit-code contract every other subsystem honors. Scripted example: clide claude.agent.set-permission-mode bogus exits 0 today, so scripts cannot detect failure.
Fix: sweep the claude builtin handlers; on failure return the error envelope (non-zero CLI exit) like the rest of the dispatcher. Audit callers/UI that may currently rely on ok-with-error.
Acceptance: clide claude.agent.set-permission-mode bogus exits non-zero; a table-driven test walks the claude verbs'' failure paths asserting error envelopes; D-6 conformance restored.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:31', '2026-06-11 22:02:40', NULL, '55e8cdd77883a14348bbb7e6f288529c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDC7B2MFQZVR1K9E30FXDR', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Docs drift at the front door: CLAUDE.md/README tmux claim superseded by D-75/D-77; README version + stub features overstated', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:44', '2026-06-11 22:02:44', NULL, '731e453c28c5fe59ebefdd9719e69d76', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDC7B2MFQZVR1K9E30FXDR', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Docs drift at the front door: CLAUDE.md/README tmux claim superseded by D-75/D-77; README version + stub features overstated', 'CLAUDE.md and README still say "tmux owns Claude session persistence (D-41)" — superseded by D-75/D-77 per docs/architecture.md. README says "Pre-v2.0 (2.0.0-dev)" while pubspec is at v2.3.3, and headlines "canvas and graph surfaces" that are a 17-line stub and a flat ListView respectively (T-7 epic was cancelled).
Fix: rewrite the stale paragraphs in both files to match current architecture (clide-managed stream-json sessions); fix the README version line (or derive it); demote canvas/graph to roadmap wording or drop them. The repo''s honesty is its brand; the README is the one off-brand surface.
Acceptance: no tmux-persistence claim outside historical D-records; README version matches pubspec; every README feature claim maps to shipped behavior.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:02:44', '2026-06-11 22:02:54', NULL, 'b4ab7763b2e791c559777dfa83c78fa3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDE5A3965GNRFS5WPZW878', 'story', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Close the release loop: back-tag 2.2.02.3.3, fix ci/release.sh, add tagging to the release ritual, widen the fast-path regex', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 22:03:00', '2026-06-11 22:03:00', NULL, '4efa9cc5cf36800ac88e9da767a20654', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDE5A3965GNRFS5WPZW878', 'story', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Close the release loop: back-tag 2.2.02.3.3, fix ci/release.sh, add tagging to the release ritual, widen the fast-path regex', 'No git tag since v2.1.0 despite five CHANGELOG releases; ci/release.sh exits 64 and still references the dissolved sidecar; the pre-push fast path''s safety argument cites "release CI on tagged versions" that does not exist; and the fast-path regex skips ALL tests for pushes touching test/, ci/, or the hook itself.
Work items (separable commits):
1. Back-tag v2.2.0 through v2.3.3 at the release-cut commits (find them via the CHANGELOG version-bump commits).
2. Rewrite ci/release.sh for the single-process architecture or delete it and fold release steps into the Makefile either way, no stub that exits 64.
3. Add tagging to the release ritual in .claude/skills/git-commit/SKILL.md (version bump + changelog move + tag in one documented step).
4. Widen the pre-push fast-path regex so changes under test/, ci/, and the hook itself run the full gate.
This is also the blocking prerequisite the T-47 (self-update) refinement identified. Acceptance: git tag lists every released version; release.sh (or its replacement) runs end-to-end; fast-path regex covered by a hook test if feasible.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-11 22:03:00', '2026-06-11 22:03:10', NULL, '223b9403ea9923579214e54b184969eb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDGPXQN31NNRPJ00PFRAG4', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split lib/app.dart (1187 LOC, five concerns)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:21', '2026-06-11 22:03:21', NULL, 'adf4ca4c52a805c0c686a18b317e5e08', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split lib/builtin/claude/src/claude_meta_sidebar.dart (1192 LOC)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:26', '2026-06-11 22:03:26', NULL, 'a0a11f91eda4b9041a5b059416cdb719', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDGPXQN31NNRPJ00PFRAG4', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split lib/app.dart (1187 LOC, five concerns)', 'lib/app.dart is 1187 LOC mixing five confirmed concerns. Split plan (verified against the file 2026-06-12):
Inventory: ClideApp (14-29), _AppRoot (31-45), _RootShell/_RootShellState keyboard+intent routing (47-229), RootLayout (231-350), hat bar family _HatBar/_LeftHatContent/_RightHatContent/_WinBtn (352-439), project switcher family _ProjectSwitcherButton/_ProjectSwitcherDropdown/_RecentProjectRow/_ActionRow (441-672), SlotHost/_SlotHostState/_SlotBody/_SidebarSlot/_WorkspaceSlot/_RevealedTab/_ContextSlot (674-1030), _BottomRail (1032-1078), StatusbarCollapseToggle (1093-1126), StatusbarHost (1128-1170), _EditorDragHandle (924-1010), _WelcomeOverlay (1172-1187).
Target layout:
- app.dart keeps ClideApp + _AppRoot and RE-EXPORTS the public symbols so tests keep importing package:clide/app.dart.
- lib/widgets/root_shell.dart: _RootShell/_RootShellState (keyboard routing; depends on ModifierTapTracker, MenuBarController).
- lib/builtin/hat/hat_bar.dart: _HatBar, _LeftHatContent, _RightHatContent, _WinBtn, hatHeight.
- lib/builtin/hat/project_switcher.dart: the switcher family (~230 LOC).
- lib/widgets/slot_host.dart: SlotHost + slot bodies (NOTE: _RevealedTab references _SlotBody._resolveTitle keep them together or extract the helper).
- lib/widgets/layout_status.dart: RootLayout internals, StatusbarHost, StatusbarCollapseToggle, _BottomRail, _EditorDragHandle(+Intent), _WelcomeOverlay.
Order: mechanical first (hat bar, switcher rows, welcome overlay), then root shell, then slot host (tangled: _SlotHostState registers focus scopes via ClideKernel.of in didChangeDependencies), then layout/status.
Tests importing app.dart: test/app_test.dart (RootLayout, StatusbarHost, ClideApp), test/app_statusbar_test.dart (StatusbarHost), test/app_collapse_toggle_test.dart (StatusbarCollapseToggle) re-exports keep them unchanged.
CORRECTION to fable-ous.md: ColumnHat is NOT duplicated line-for-line in app.dart (verified); ColumnHat lives only in lib/widgets/src/clide_column_hat.dart. The rat-sweep ticket T-385 was annotated accordingly verify whether ColumnHat is dead before deleting.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:21', '2026-06-11 22:09:19', NULL, 'ea4c03fb65ff1d332c15ed7df7a865e3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Rat extermination: dead-code sweep (legacy git API, ToolCheck, libc, GraphView, ColumnHat, tmux-era team pipeline, ptyc binary, mocktail)', 'Verified-dead code worth one sweep (coverage denominator benefits too):
- Legacy free-function git API (~250 LOC duplicating GitClient, kept alive only by its own tests, and carrying its own latent pipe-deadlock bug) delete API + tests.
- ToolCheck zero callers.
- ~60% of lib/src/pty/ffi/libc.dart fd-passing-era bindings unused since D-56.
- GraphView unreachable placeholder (note: the Governance Graph idea (see Q-records from this review) may later want the slot; deleting now is still right, it is a 17-line stub).
- ColumnHat duplicated line-for-line in app.dart, kept alive by a zero-coverage test; the app.dart split ticket removes the duplicate, this sweep removes the orphan.
- tmux-era team pipeline: TranscriptPublisher, TeamMemberJoined nothing emits these events, yet the team roster UI listens to them exclusively (team tiles are populated by ghosts). Remove pipeline + dead listeners; if the roster UI stays, it needs a real data source first (surface that before deleting the UI).
- Dead ptyc binary still committed in native/linux-x64/ against D-62/D-63 remove binary + licenses.yaml entry if present.
- mocktail pinned, documented in D-25 as the IO-mocking strategy, imported by zero files: either adopt it where mocks are hand-rolled or drop the dep AND amend D-25.
Each bullet is one commit. Run make test + coverage after each; expect the floor to ratchet up.
Correction (2026-06-12, verified during T-394 breakdown): ColumnHat is NOT duplicated line-for-line in app.dart it exists only in lib/widgets/src/clide_column_hat.dart. Before deleting it, verify it actually has zero non-test callers; if it is genuinely used by app chrome, drop that bullet from this sweep.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-11 22:00:44', '2026-06-11 22:09:30', NULL, 'dd3a3f6ec9a78df6044d44713218e989', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'task', '06FBHBGHNEQTAEPGNJKN42C1E8', 'Split lib/builtin/claude/src/claude_meta_sidebar.dart (1192 LOC)', 'lib/builtin/claude/src/claude_meta_sidebar.dart is 1192 LOC. Split plan (verified against the file 2026-06-12). Public API (ClaudeMetaSidebar widget + SidebarTab enum) stays in the root file — tests and extension.dart need zero import changes.
Inventory: consts _labelColumnWidth/_rowPitch (43-44), SidebarTab enum (46), ClaudeMetaSidebar (48-79), _ClaudeMetaSidebarState monolith (81-638: lifecycle, stats polling, team membership streams, primary-session binding, broker subscription, config listener, inject state, accordion state, plus the three tab bodies), _ConfigSection/_ConfigPermKind enums (641/644), _MetaSection/_MetaRow models (646-657), _AgentRosterRow(+State) (680-928), _permissionModeBadge + _PermissionModeBadge T-181 (935-1009), _IconButton (1012-1039), _InjectTextField (1043-1070), _TaskRow T-171 (1077-1141), _TabStrip (1145-1192).
Target layout under lib/builtin/claude/src/meta_sidebar/: models.dart (enums + _MetaSection/_MetaRow + layout consts), activity_tab.dart (ActivityTabView ~70 LOC, from _activityBody/_runtimeSection 272-302), team_tab.dart (TeamTabView ~120 LOC, from _teamBody/_taskSection 304-380, props-driven with callbacks), config_tab.dart (ConfigTabView ~200 LOC, from _configBody family 382-597; accordion _expanded state stays in parent, passed as prop+callback), roster_row.dart (_AgentRosterRow ~250 LOC incl. bypass-confirm state), permission_badge.dart (~80), task_row.dart (~70), tab_strip.dart (~55), icon_button.dart + inject_field.dart (~30 each keep here initially; promote to lib/widgets/ only when a second consumer appears). Root file shrinks to ~150 LOC of lifecycle + event bindings + tab switch.
Execution order (each phase independently green): 1) primitives (icon button, inject field, permission badge, models); 2) stateless tab views (activity, config, tab strip) with prop threading; 3) team tab + roster row (most orchestrator coupling: verify show/hide, mute, inject submit-and-clear, shift-click bypass confirm, fork, close, task reassign cycle, auto-front on TeamMemberJoined at line 158); 4) cleanup of moved methods from the root state.
Tests: test/builtin/claude/claude_meta_sidebar_test.dart imports only the root file and public symbols no changes needed; no goldens reference the sidebar. Caveat from the rat sweep (T-385): TeamMemberJoined currently has no emitter coordinate before investing in the team tab plumbing.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-11 22:03:26', '2026-06-11 22:09:55', NULL, '555ee8c21aa930e2310712747c9b2d97', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RJ9K19P5AX14RHRR', 'task', '06FB0TNQM4HGK6KR0314P2M32M', 'split lib/src/terminal/.../escape/parser.dart (1139 LOC)', 'parser.dart is a single 1139-line file containing the full ESC/CSI/OSC/DCS handler tree for the terminal emulator. Functional but unwieldy; the consultant flagged it as ''consider splitting'' in the T-107 review.
Suggested split (sequenced with the T-91 coverage sweep on lib/src/terminal/, so the split doesn''t fight in-flight test work):
- parser.dart entry point + state machine driver
- esc_handlers.dart single-char ESC dispatch table + handlers
- csi_handlers.dart CSI parameter parsing + handlers
- osc_handlers.dart OSC string handlers (title, colour set, etc.)
- dcs_handlers.dart DCS/SOS/PM/APC string handlers
Each handler module exports a registrar that the driver wires at construction.
Done when:
- parser.dart < 400 LOC
- All existing parser tests pass without changes
- No new public surface; everything stays library-private
Source: T-107 / consultants.md "Code quality — Findings — [Major]".
Split breakdown from the 2026-06-11 Fable review (epic T-359), verified against the file 2026-06-12:
Structure today: main parser + routing (11-116), CSI core _escHandleCSI (196-209) + _consumeCsi (217-272), _csiHandlers table of 27 final bytes (274-304), cursor movement handlers (306-807), erase/scroll/line/char ops (809-945), SGR monolith (411-622, 212 LOC), mode set/reset (395-409, 946-1030), DA/DSR (334-343, 624-635), window manipulation (658-712), OSC (1034-1110), _Csi state object (1113-1131 note the commented-out `intermediates` field at 1117/1125).
Target layout under escape/: parser.dart keeps EscapeParser (queue, tokenization, top-level dispatch, ~400 LOC); csi_parser.dart (CsiSequence value object prefix, params, RESTORED intermediates, finalByte plus the consume logic from _consumeCsi); csi_handlers.dart (dispatch table, now keyed on final byte + intermediates); cursor_handlers.dart; sgr_handler.dart (the 411-622 monolith); mode_handler.dart; osc_parser.dart + osc_handlers.dart. EscapeHandler interface unchanged. Preserve the zero-allocation/reset-able design goal noted at lines 14-16.
Bug this split must fix (verified): _consumeCsi DISCARDS intermediate bytes lines 258-261 have `// intermediates.add(char);` commented out and `continue`, so CSI Ps SP q (DECSCUSR, cursor style) and CSI ! p / SP-intermediate forms dispatch on the bare final byte and fall to unknownCSI. Fix: restore the intermediates field on CsiSequence, capture them during consume, dispatch on (intermediates, finalByte), and add EscapeHandler.setCursorStyle for DECSCUSR. (DECSTR is CSI ! p soft terminal reset same intermediate mechanism.)
Related bug with its own ticket (T-369): unguarded params[i+1] lookahead in SGR 38/48 at lines ~502/512/547/557 (RangeError on truncated sequences) + colon-form sub-parameters unhandled. The split makes the fix natural: sgr_handler.dart owns guarded lookahead helpers; if T-369 lands first, carry its tests over; if this lands first, fix it inside sgr_handler.dart and close T-369 with it.
Tests: test/terminal/escape/parser_test.dart (786 LOC) splits along the same seams keep parser_test.dart for top-level dispatch/SBC/rollback, add csi_parser_test.dart (intermediates capture, DECSCUSR), sgr_handler_test.dart (bounds + colon form + 256/RGB), mode_handler_test.dart, osc_parser_test.dart, window/DA splits as convenient. The _RecordingHandler fixture is reusable across all of them.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-18 10:29:02', '2026-06-11 22:10:19', NULL, '90450ed5bd162fe65bf0426662d65ee4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+428
View File
@@ -0,0 +1,428 @@
# fable-ous.md
*A fable about clide, as told by Fable.*
*Produced by 13 parallel subsystem reviewers reading ~58k LOC of Dart, ~100 raw
findings put through 70 adversarial verification passes (exactly one finding was
refuted — it had missed D-14), 5 feature-ideation lenses, and a full read of the
pql planning vault: 94 confirmed decisions, 24 open questions, 11 rejected
alternatives, 358 tickets. Everything below cites real file:line evidence that a
skeptical second agent re-read and failed to knock down.*
---
## TL;DR scoreboard
| Dimension | Verdict |
|---|---|
| Build health | **Green.** `make analyze` 0 issues, 1383 tests pass, format clean, coverage 95.30% over the 95 floor |
| Documentation discipline | **Best-in-class.** Near-every file cites its D-NNN/T-NNN; zero TODO/FIXME debt in 58k LOC |
| Honesty | **High.** CHANGELOG claims verified against code; stubs self-describe as stubs |
| Real bugs found | ~14 distinct high-severity, ~35 medium — mostly in process lifecycle, a11y, and terminal conformance |
| Systemic risks | Broadcast-streams-without-replay, silent `catch (_)`, kernel lookup in `dispose()`, copy-paste drift |
| Release process | **Stalled.** No git tag since v2.1.0 despite five CHANGELOG releases; `ci/release.sh` is a stub |
| Killer-feature headroom | Enormous — the moats (owned agent loop, owned renderer, pql vault) are real and barely exploited |
The fable in one sentence: **a castle with exceptional masonry, a few unlocked
side doors, and a dragon hoard of features in the basement nobody has spent yet.**
---
## Part I — The state of the realm (what's genuinely excellent)
Credit where due, because this codebase does several things better than most
production repos:
- **Governance traceability is real, not ceremonial.** Nearly every non-trivial
declaration carries its ticket/decision id. Multiple reviewers independently
called it "the best I've seen at this scale." Code-to-decision drift is
*auditable from the code itself* — and indeed most drift findings below were
found exactly that way.
- **Schema-validated IPC dispatch (D-74)** is beautifully executed: schemas
co-registered with handlers, the MCP tool surface and `clide capabilities`
both generated from the same registry (`lib/src/daemon/dispatcher.dart:90-137`)
so three surfaces cannot drift apart.
- **The keymap subsystem (D-82)** — layered precedence, headless
`SequenceMatcher`, clock-injected `ModifierTapTracker` — is exemplary,
near-fully test-mirrored design.
- **`ClideTappable`, the anchored-overlay/menu family (D-88), reduced-motion
honoring in every animated primitive** — the owned widget layer is coherent
and disciplined.
- **Battle scars are encoded where they happened.** `pumpAsync` documents why
`pumpAndSettle` wedges; `KernelFixture` encodes the T-280 teardown-hang fix;
flake postmortems live as comments in the test that almost shipped them.
- **Security instincts**: 0600 socket + live-listener probe before unlink, git
argv hardening with `--` terminators, `path_safety.dart` documenting its
threat models inline, workspace-relative binary resolution forbidden (T-98).
- **Zero TODO/FIXME/HACK** across the tree. The clean-board policy is lived.
Patterns worth *extending* (the reviewers kept wishing other code did this):
the coalesced-notify `Timer(Duration.zero)` trick in `ConversationController`,
the pure-Dart-core/thin-Flutter-shell split, and `RecordingEventSink`-style
event-driven test waits.
---
## Part II — The bestiary (bugs I would fix, in order)
### 🐉 Dragons (high severity, verified, fix this week)
1. **Every naturally-exited PTY leaks its master fd — forever.**
`lib/src/pty/native_pty.dart:444-450` — on child EOF, `_reap()` sets
`_dead = true` but never closes `_fd`; a later `close()` short-circuits at
`if (_dead) return;` (line 460) so `_nativeClose(_fd)` (line 477) never runs.
Two reviewers found this independently. Every terminal/Claude pane whose
child exits on its own leaks an fd and a pty device for the life of the app.
2. **The Claude child process is observed only via stdout.** Three reviewers
converged here. `lib/builtin/claude/src/stream_json_session.dart:43-78`
stderr is *never drained* (≥64KB of `--verbose` spew = pipe fills = child
blocks mid-turn = the flagship pane wedges with zero diagnostics), and
nothing watches `exitCode` or `onDone` (line 303), so a crashed/dead
session just looks… thoughtful. Drain stderr into a ring buffer, surface a
terminal `SessionEnded` state. This also intersects T-283 (no resume
timeout/fallback).
3. **The MCP HTTP server exposes the entire dispatcher with zero auth.**
`lib/src/ipc/mcp_server.dart:138-195`, started unconditionally at boot
(`lib/main.dart:174-180`). D-71's threat model ("another user on the same
host should not drive my IDE") is enforced with 0600 on the unix socket —
and then bypassed wholesale by an unauthenticated localhost HTTP port that,
since D-86, serves *every* clide verb as a tool. Generate a token in the
lock file (Claude Code's own `/ide` lock format has a slot for it), require
the header.
4. **`editor.open`/`editor.save` skip path confinement entirely.**
`lib/src/editor/registry.dart:215-219` returns absolute paths verbatim, no
`..` normalization, no `path_safety` call — an unconfined read *and write*
primitive over IPC while `files.read` is carefully guarded. Same family:
**`search.replace` silently ignores its include/exclude globs**
(`lib/src/search/replace_engine.dart:124-143`) and will happily rewrite
files outside the filter the user typed; and **`listDir`'s symlink
detection is dead code** (`lib/src/files/listing.dart:46-54``stat()`
follows links, so `isSymlink` is always false) which means `walkFiles`
descends symlinked dirs the docs claim it skips.
5. **Closing a terminal pane never closes the shell.**
`lib/builtin/terminal/src/terminal_pane.dart:131-137` calls
`ClideKernel.of(context)` from `dispose()` — illegal ancestor lookup,
swallowed by `catch (_)` — so `pane.close` is never sent and the backend
PTY + daemon pane leak. The same idiom leaks the settings listener in every
disposed `ClaudePane` (`claude_pane.dart:460-466`). Combined with dragon #1
this is a two-stage leak pipeline. Cache the kernel ref in
`didChangeDependencies`, delete the catch-all.
6. **Project switch leaks the entire previous workspace.**
`lib/main.dart:335-344` — a new dispatcher gets fresh `PaneRegistry`,
`FilesService`, `EditorRegistry`, etc., but nothing calls the old set's
`shutdown()` methods (which exist and have zero callers). Old watchers keep
emitting into the new workspace's bus.
7. **T-274's root cause, found and verified:** the status bar is empty because
`statusStream` is a plain broadcast controller — the `system/init` event
fires while `spawn()` is still awaiting a 256KB transcript-tail read, before
the pane ever subscribes (`claude_pane.dart:322`,
`session_orchestrator.dart:222-225`). Seed from `session.status` on bind, or
make it replay-latest. (The broadcast-without-replay shape is a recurring
bug factory — see Part III.)
8. **Auto-scroll yanks a scrolled-up reader to the bottom on every streamed
token.** `conversation_view.dart:268-277` — the `_atBottom` pin exists but
is only consulted on viewport *resize*, not on new items. Anyone reading
earlier output during a long streaming reply is dragged to the bottom
continuously. One-line gate + the missing twin test.
9. **The terminal can crash on garbled output.** SGR 38/48 extended-color
parsing does unguarded `params[i + 1]` lookahead
(`lib/src/terminal/src/core/escape/parser.dart:501-516`) — `printf '\e[38m'`
throws RangeError inside `Terminal.write`. An emulator must never throw on
hostile bytes. While in there: colon-form SGR sub-parameters are mangled
into bogus params.
10. **A11y has drifted despite being a Tier-0 contract.** Two independent
verified findings: `ClideCollapserCard`'s `excludeSemantics: true` wipes
*every expanded child* from the a11y tree
(`lib/widgets/src/clide_collapser_card.dart:92-101`) — a screen-reader user
can expand a run and hear nothing; and the three a11y gate tests
hand-enumerate their subjects and have measurably fallen behind `lib/`
(contrast checks fewer themes than `main.dart:443-454` loads; i18n checks 4
of 8 namespaces). The gates stay green while covering less. Make `lib/`
export the canonical lists and iterate them in the gates.
### 🦂 Scorpions (medium — real, will sting eventually)
- **D-72's "serial dispatch" isn't.** `lib/src/ipc/server.dart:151-180` uses an
`async` onData without pausing the subscription — pipelined requests
interleave, and the shared `StringBuffer` framing can drop/double lines.
`client.cast<List<int>>().transform(utf8.decoder).transform(LineSplitter())`
+ `await for` fixes framing, UTF-8 split chunks, and serialization at once.
- **Split-chunk UTF-8 corruption is endemic at byte→String seams**: the
terminal's only ingestion API is `write(String)` (`terminal.dart:218`) so
both consumers decode per-chunk; `FileTailFollower` starts mid-character by
construction. Add `writeBytes()` with a persistent chunked decoder.
- **`Orchestrator.spawn()` races itself** — check-then-act across two awaits;
concurrent spawns for one id leak a live `claude` process
(`session_orchestrator.dart:191-249`). Hold a `Map<String, Future<ManagedSession>>`.
- **Fork panes misbehave on `/clear`, `/resume`, `/fork`** —
`widget.forkSourceId` wins forever, so `/clear` *re-forks the original
conversation* instead of clearing (`claude_pane.dart:266-281`).
- **Settings persistence corrupts maps-inside-lists on write**
(`settings.dart:199-219` emits `toString()`), breaking the documented keymap
overlay across restarts; writes are non-atomic and a parse failure silently
resets all settings.
- **Extension activation isn't transactional** — a throw mid-contribution
leaves contributions mounted while the extension records as failed; retry
double-applies (`extensions_manager.dart:133-192`). Plus: disabling an
extension ignores dependents, and registries clobber silently on id
collision — fine among curated builtins, hazardous the day Tier-6 Lua lands.
- **Terminal conformance debt** (the fork fixes what it trips over but has no
vttest-style suite): HTS is a no-op (`isSetAt` instead of `setAt`,
`terminal.dart:423`), DECCKM is tracked but never consumed, legacy mouse rows
are off-by-one *and the test enshrines the bug*, CPR replies 0-based where
every real terminal is 1-based, scrollback is maintained but structurally
unreachable (`ViewportOffset.zero()` pinned every build,
`terminal_view.dart:224`).
- **Markdown renderer**: hard breaks and images render as empty text (words
glue together; `clide_markdown.dart:408-410`), and the whole document
re-parses with sync `existsSync()` calls *inside build* on every streaming
delta — multiplied by the conversation view re-deriving everything O(n) per
notification and token streaming re-encoding the full reply per delta
(O(n²) churn, `stream_json_session.dart:410-431`).
- **Terminal panes spawn in `Directory.current`**, not the open project root
(`terminal_pane.dart:69`) — desktop launches get `$HOME` shells.
- **The Notifications service renders nowhere** — `notify.dart` has zero widget
consumers; cli_install's dogfood warnings vanish into an unrendered list
while `ToastService` sits right there.
- **Welcome screen no-ops**: "Clone from git…" and "Start a Claude session"
advertise shortcuts that don't exist and do nothing on tap
(`welcome_view.dart:173-174`).
- **`make test-e2e`/`ui-dev`/`ui-smoke` are dead** — `tools/ui/*.sh` still `cd`
into the removed `app/` directory; the staged Gitea CI workflow would fail in
three independent ways on activation, while D-32 calls it "ready."
### 🐀 Rats (low, but they breed)
Dead code worth a one-day extermination sweep: the entire legacy free-function
git API (~250 LOC duplicating `GitClient`, kept alive only by tests, *with its
own latent pipe-deadlock bug*), `ToolCheck`, ~60% of `ffi/libc.dart` (fd-passing
era), `GraphView` (unreachable placeholder), `ColumnHat` (duplicated
line-for-line in app.dart, kept alive by a zero-coverage test), the tmux-era
team pipeline (`TranscriptPublisher`, `TeamMemberJoined` — *nothing emits these
events*, yet the team roster UI still listens to them exclusively, meaning team
tiles are populated by ghosts), the dead `ptyc` binary still committed in
`native/linux-x64/` against D-62/D-63, and `mocktail` — pinned, documented in
D-25 as the IO-mocking strategy, and imported by exactly zero files.
---
## Part III — Patterns I would change (the systemic stuff)
1. **Broadcast streams that carry state need replay-latest.** This one shape
caused T-274, the meta sidebar's manual compensation, and the
prompt-stream's `initialData` workaround. Write a tiny `ValueStream` wrapper
once; retrofit `statusStream`, `busyStream`, `pendingPromptStream`.
2. **Ban `catch (_) {}` on I/O and lifecycle paths.** The silent-swallow idiom
turned an illegal-lookup-in-dispose into two resource leaks and turned
process-spawn failures into blank panes. Cleanup paths may swallow; spawn,
read, and dispose paths must log through the kernel Logger they already have.
3. **Sync I/O in async handlers on the single isolate.** `files.read` does a
sync 10MB read; the replace engine reads and rewrites the workspace
synchronously *while grep right next to it fans out to isolates per D-79*.
Decide the rule (offload above N KB), write it into a D-record, apply it.
4. **Path confinement belongs at the dispatch layer, not per-verb.** files.read
remembered, search.replace half-remembered, editor.* forgot. A confinement
check keyed off the co-registered schema (the registry already knows which
params are paths) ends the per-verb lottery.
5. **Copy-paste is the repo's main duplication tax.** The welcome screen clones
FileActions' entire open-folder flow verbatim; palette and quick-open are
~230-line near-twins; three private "tail a growing file" implementations in
the claude builtin alone; five hand-rolled `_userErr` helpers; five
copy-pasted git test sandboxes (none isolating host git config); two
parallel ANSI flag enums that already drifted (strikethrough is stored but
never painted). Each is small; together they're how a solo-dev repo rots.
6. **Hand-enumerated lists drift; export the truth.** Bundled themes (already
drifted between `main.dart` and the testmode harness — catppuccin is
silently unvalidated), a11y gate subjects, i18n namespaces. One exported
const each, consumed by both sides.
7. **The claude builtin returns `ok` with an `error` payload in 16 handlers**,
drifting from the D-6 exit-code contract every other subsystem honors. A
scripted `clide claude.agent.set-permission-mode bogus` exits 0 today.
8. **God-files**: `app.dart` (1187 LOC, five concerns — split plan is in the
findings), `claude_meta_sidebar.dart` (1192), `parser.dart` (1139, T-123
already exists — and the split should also fix `_consumeCsi` discarding
intermediate bytes, which permanently blocks DECSCUSR/DECSTR).
9. **Docs drift at the front door**: CLAUDE.md and README still say "tmux owns
Claude session persistence (D-41)" — superseded by D-75/D-77 per
`docs/architecture.md`; README says "Pre-v2.0 (2.0.0-dev)" at v2.3.3 and
headlines "canvas and graph surfaces" that are a 17-line stub and a flat
ListView respectively. clide's honesty is its brand; the README is the one
place currently off-brand.
10. **Close the release loop.** Five CHANGELOG releases since the last git tag;
`ci/release.sh` exits 64 and references the dissolved sidecar; the pre-push
fast path's safety argument cites "release CI on tagged versions" that
doesn't exist; and the fast path skips ALL tests for pushes touching
`test/`, `ci/`, or the hook itself. Back-tag 2.2.02.3.3, add tagging to
the git-commit skill ritual, widen the fast-path regex. (This is also the
blocking prerequisite your own T-47 refinement identified for self-update.)
---
## Part IV — Killer features (the dragon hoard)
Five ideation lenses, 27 proposals, deduplicated and ranked. The convergence
test mattered: **two lenses independently invented the flight recorder, and two
independently invented the visual canvas round-trip** — when separate agents
with different briefs land on the same feature, that's the market talking.
Clide's structural moats, verified against code: it *spawns and owns* the agent
process (D-77/D-78) where competitors are sandboxed extension guests; it owns
every pixel (terminal, markdown, canvas); everything is local-and-committed
(transcripts, costs, decisions, tickets) where competitors' business models
require cloud custody; and the pql vault is structured planning data no
mainstream IDE has an analogue for.
### Tier 1 — do these (high leverage, mostly M-effort, plumbing exists)
1. **Agent Blame + Session Flight Recorder** `[L]` — gutter action on any line:
*which session, which turn, which prompt, which permission grant, what it
cost* — opening the native conversation at the exact `tool_use`. Timeline
scrubber to replay a session. The transcript pipeline
(`transcript_reader.dart`, `session_index.dart`) already parses everything
needed. Cursor/Copilot cannot ship this: their logs live server-side by
business design. *Two lenses converged here.*
2. **Context X-ray** `[M]` — per-card token attribution ("this 40KB Bash tail
is 12% of your window") + a real compaction indicator. `stream_json_session.dart`
already parses usage and contextWindow per event; the renderer owns the
cards. Fixes T-244 (invisible compaction) as a side effect. Context is the
scarcest resource in agent pairing and every tool renders it as one opaque
percentage.
3. **Trust Ledger + decision-aware permission prompts** `[M]` — every
permission rule with provenance (which prompt, which session, which ticket),
ticket-scoped expiry; and when a `can_use_tool` request arrives, chip the
relevant D-record onto the card (edit touching pubspec.yaml → D-31
prefer-zero-deps, one keystroke to deny *with the decision cited*).
Governance stops being documentation and becomes live agent policy. No
competitor has a queryable in-repo decision system to even attempt this.
4. **Agent Activity HUD** `[M]` — four backlog tickets and one open question
are secretly one feature: build T-59's `OperationsRegistry` once and feed it
git/pql progress (T-59), sidebar badges (T-58), compaction state (T-244),
the status strip (T-274), with Q-34's budget slot reserved. Fix the T-274
plumbing bug first or the HUD inherits blank-slot syndrome.
5. **Active Ticket Context** `[S]`*cheapest win in the whole list* — picking
up a ticket binds it to the session: a "working on T-244" chip, auto
`in_progress` flip, `(T-NNN)` pre-suggested in commit messages, changelog
reminder on done. Makes kanban ambient instead of homework, and makes every
trail/ledger feature below reliable.
### Tier 2 — the differentiators (L/XL, each could headline a release)
6. **Twin-timeline rewind** `[L]` — snapshot the worktree as hidden git refs
(`git write-tree``refs/clide/checkpoints`) at every turn boundary, keyed
to turn uuid; every user-message card gains "restore files to before this."
Claude's `/rewind` only restores what Claude itself edited; clide owns both
timelines.
7. **Visual Dialog** `[L]` — one bidirectional scene schema: Claude draws
(D-91 canvas cards), the user annotates in the interaction zone (T-260), and
the annotations return as *structured geometry + flattened PNG*, not prose.
Merge the T-317/T-318 and T-260 specs into one protocol before they become
two dialects. *Two lenses converged here.*
8. **Immortal terminals** `[M]` — T-258 (terminal as editor-mode peer) fused
with T-325 live-tails: any long process — user shell *or* agent-spawned
build — promotes to a full tmux-backed surface that survives restart.
"The build that never dies" is structural for clide, a plugin fantasy for
Electron. Resolve Q-27 (swap vs split) as part of it, as T-258 already notes.
9. **Local cost ledger** `[M]` — per-turn cost/tokens persisted against the
active pql ticket; the board shows what each feature actually cost.
Flat-subscription opacity is the competitors' business model; turn-level
local cost data is clide's birthright. (Tokens primary, dollars advisory —
subscription auth reports notional costs.)
10. **Label-routed work queues / ticket dispatch** `[M→XL]` — T-277 labels +
the shipped pick-up path turn the board into an agent control surface;
the XL extension dispatches a ticket to a teammate session in an isolated
git worktree (SpawnSpec.cwd already exists). Local, no-telemetry
background agents with the work item, isolation, review surface, and audit
trail all in-repo.
### Tier 3 — moonshots (XL, pick one per quarter, they compound)
11. **Semantic terminal** — OSC 133 markers (clide spawns the shell, injection
is trivial) lift scrollback into foldable command regions with recognizers
for test runners and stack traces; real widgets between rows is something
xterm.js structurally cannot do. *Note: fix the scrollback-unreachable bug
first — a semantic scrollback you can't scroll is a koan.*
12. **Living codebase map** — tree-sitter imports + pql links + git churn on
the owned canvas, with live agent heat from `tool_use` events: watch Claude
*move through your codebase* in real time.
13. **Live mixed documents** — fenced blocks in the owned markdown renderer
become live embeds (canvas scenes, pql query results, decision cards,
confirm-gated command buttons). Notebook-grade, zero webview. The
`ClideMarkdownHooks` seam already exists.
14. **Remote Claude over SSH** — T-329 is fully ticketed and undersold: agent
on the buildbox, permission prompts rendering natively local. VS Code
Remote moves the editor; nobody remotes the *agent control channel*.
15. **Sealed-workspace mode** — an egress-audit proxy around the whole agent
stack, operationalizing D-60/D-64 into a provable property. The one
feature in this list competitors *cannot* copy without breaking their own
products.
### Honorable mentions
Plan-to-Board bridge (ExitPlanMode approval files the plan as a ticket tree),
Release Cockpit (renders `[Unreleased]` with word-count badges + one-action
release cut — would also unstall Part III #10), Daily Helm ("since you were
last here" pulse on the welcome screen, computed from data the repo already
commits), Total-recall conversation search (D-79 grep over the transcript
corpus + fork-from-here), Shared Gaze (attach editor selection/scroll context
to each outgoing turn), Speakable Layouts (`clide layout apply review.yaml`
the agent stages your workspace), Agent fleet tray (enumerate per-workspace
sockets, show every repo's agent state — *requires fixing T-247's stale-socket
litter first*), Governance Graph (the D/Q/R/T web as a navigable map — gives
the placeholder graph view a flagship dataset).
---
## Part V — If I were you, Monday morning
1. **One leak-fix commit**: PTY fd on natural exit + kernel-lookup-in-dispose
(terminal & claude panes) + project-switch service disposal. Three findings,
one theme, one afternoon.
2. **One Claude-resilience commit**: drain stderr, watch exitCode, seed status
on bind, gate auto-scroll on `_atBottom`. The flagship pane stops having
silent failure modes.
3. **One security commit**: MCP auth token + editor.* path confinement +
search.replace glob filter + symlink-walk fix.
4. **One rat-extermination day**: dead git API, ToolCheck, libc bindings,
ColumnHat, GraphView, tmux-era team pipeline, ptyc binary, mocktail. The
diff is gloriously red and the coverage denominator thanks you.
5. **Tag your releases.** Five releases of honest changelog work are currently
unaddressable commits.
6. Then go build the **Active Ticket chip** (S!) and the **Context X-ray**, and
let clide start showing people things no other IDE can.
---
*Findings methodology: every medium/high claim above survived an independent
adversarial re-read of the cited lines (one claim did not — the proposed
"can't-disable core extensions" guard, which D-14 deliberately rejects, so it
stays out of this report). The full per-finding evidence, severities, and
suggested fixes live in the review transcripts; ~34 additional low-severity
findings were verified by citation only.*
*— Fable, 2026-06-11*