Files
clide/.pql/changelog/ticket_history/2026-05.sql
T
jpmschweitzerandClaude 89766df55c add pql to the README build prerequisites
pql is a hard dependency (the pre-push gate runs `pql decisions validate`;
the governance + ticket workflow is built on it) but the build setup only
mentioned Flutter. Add it as a prerequisite + the `pql init` setup step.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-31 18:42:49 +02:00

2325 lines
364 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
Neither happens today.
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
**Acceptance:**
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
**Acceptance:**
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
**Canvas pane (`builtin.canvas`):**
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
- Persist layout back to the `.canvas` file on disk.
**Graph view (`builtin.graph`):**
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
- Hover highlights connected subgraph; click opens the note in the editor.
- Filter pane: tag include/exclude, file glob, depth-from-active.
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
**Extension API (third-party Lua):**
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
- Manifest schema, capability gating, sandboxed FS/IPC access.
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
**Settings UI (`builtin.settings-ui`):**
- Schema-driven settings panel reading from the kernel SettingsStore.
- Render strategy: form fields keyed off the schema each subsystem registers.
- Edits write back to `.clide/settings.yaml`.
**Theming UI (`builtin.theme-picker` extends):**
- Live preview of the four bundled themes (D-44).
- Custom theme: import YAML, validate against schema, register at runtime.
- Per-component override surface (long horizon).
**Distributable builds:**
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
- Self-update mechanism — see T-47.
- License manifest auto-regen as part of the release build.
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
**Acceptance:**
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
- Fuzzy match against command title; recent / pinned commands float to the top.
**Implementation hints:**
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
**Acceptance:**
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
**Errno constants (audit item #23):**
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
**Logger standardization (audit item #22, partial #26):**
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
- The Flutter-host process often consumes stderr, so log lines disappear silently.
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
**Acceptance:**
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
- WCAG-AA contrast gate (D-22) passes against the theme.
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
**Acceptance:**
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
---
Refinement (2026-05-06):
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
**Concrete fix shape:**
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
| `lib/widgets/` | 537 / 1184 | 45.4% |
| `lib/src/pql/` | 45 / 94 | 47.9% |
| `lib/builtin/` | 269 / 550 | 48.9% |
| `lib/kernel/` | 889 / 1691 | 52.6% |
| `lib/src/daemon/` | 320 / 584 | 54.8% |
| `lib/src/pty/` | 130 / 216 | 60.2% |
| `lib/src/git/` | 356 / 518 | 68.7% |
| `lib/src/ipc/` | 110 / 156 | 70.5% |
| `lib/src/files/` | 98 / 133 | 73.7% |
| `lib/extension/` | 41 / 54 | 75.9% |
| `lib/src/editor/` | 107 / 129 | 82.9% |
| `lib/src/panes/` | 70 / 73 | 95.9% |
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
**End state:**
- Total line coverage ≥ 95% (no per-area carve-outs).
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
**Acceptance criteria for closing this epic:**
1. `coverage/floor.txt` reads `95` (or higher).
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
3. The pre-push gate computes coverage and rejects pushes below the floor.
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
**Strategy / child shape:**
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
**Out of scope:**
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
- Integration / E2E coverage (only widget + unit feed lcov).
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
**What lands:**
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
**Acceptance:**
- `make coverage-gate` passes on a clean checkout (floor matches reality).
- `make push-check` includes the gate.
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
**Out of scope:**
- Bumping the floor toward 95% — that''s the test-writing children''s job.
- Branch coverage — D-66 explicitly excludes.
- Per-area floors — D-66 chose a single global floor for simplicity.
**Notes:**
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
**Today''s distribution within `lib/src/terminal/src/`:**
| sub-area | hit / total | % | shape |
|---|---|---|---|
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
**Strategy — pure logic first, then UI:**
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
**Acceptance:**
- `lib/src/terminal/` line coverage ≥ 95%.
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
**Strategy on commit cadence:**
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
- Adds tests under `test/terminal/<sub-area>/`.
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
**Out of scope:**
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
- Branch coverage (D-66 explicitly excludes).
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
**Trigger paths in production code:**
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
**Acceptance:**
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
- Runs reflow shrink,
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
**Investigation notes (do not skip):**
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
**Two layers of "dead" here:**
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
**Two clean resolutions:**
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
- Collapse the two into a single hook and document the contract.
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
**Acceptance:**
- Either:
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
Trace:
1. `TerminalGestureHandler.build` (gesture_handler.dart:6582) wires:
```dart
onTertiaryTapDown: onSecondaryTapDown,
onTertiaryTapUp: onSecondaryTapUp,
```
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147152) are therefore **dead** — they''re declared but never invoked anywhere.
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 2425, 4547) and `TerminalGestureDetector` (lines 1314, 3031) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
**Two layers of "dead" again:**
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
**User-visible impact:**
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
**Resolutions, like T-93:**
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
```dart
details.kind == PointerDeviceKind.mouse
? renderTerminal.selectCharacters(details.localPosition)
: renderTerminal.selectWord(details.localPosition);
```
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
**Acceptance:**
- Either:
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
Neither happens today.
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
**Acceptance:**
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
**Acceptance:**
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
**Canvas pane (`builtin.canvas`):**
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
- Persist layout back to the `.canvas` file on disk.
**Graph view (`builtin.graph`):**
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
- Hover highlights connected subgraph; click opens the note in the editor.
- Filter pane: tag include/exclude, file glob, depth-from-active.
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
**Extension API (third-party Lua):**
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
- Manifest schema, capability gating, sandboxed FS/IPC access.
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
**Settings UI (`builtin.settings-ui`):**
- Schema-driven settings panel reading from the kernel SettingsStore.
- Render strategy: form fields keyed off the schema each subsystem registers.
- Edits write back to `.clide/settings.yaml`.
**Theming UI (`builtin.theme-picker` extends):**
- Live preview of the four bundled themes (D-44).
- Custom theme: import YAML, validate against schema, register at runtime.
- Per-component override surface (long horizon).
**Distributable builds:**
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
- Self-update mechanism — see T-47.
- License manifest auto-regen as part of the release build.
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
**Acceptance:**
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
- Fuzzy match against command title; recent / pinned commands float to the top.
**Implementation hints:**
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
**Acceptance:**
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
**Errno constants (audit item #23):**
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
**Logger standardization (audit item #22, partial #26):**
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
- The Flutter-host process often consumes stderr, so log lines disappear silently.
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
**Acceptance:**
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
- WCAG-AA contrast gate (D-22) passes against the theme.
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
**Acceptance:**
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
---
Refinement (2026-05-06):
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
**Concrete fix shape:**
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
| `lib/widgets/` | 537 / 1184 | 45.4% |
| `lib/src/pql/` | 45 / 94 | 47.9% |
| `lib/builtin/` | 269 / 550 | 48.9% |
| `lib/kernel/` | 889 / 1691 | 52.6% |
| `lib/src/daemon/` | 320 / 584 | 54.8% |
| `lib/src/pty/` | 130 / 216 | 60.2% |
| `lib/src/git/` | 356 / 518 | 68.7% |
| `lib/src/ipc/` | 110 / 156 | 70.5% |
| `lib/src/files/` | 98 / 133 | 73.7% |
| `lib/extension/` | 41 / 54 | 75.9% |
| `lib/src/editor/` | 107 / 129 | 82.9% |
| `lib/src/panes/` | 70 / 73 | 95.9% |
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
**End state:**
- Total line coverage ≥ 95% (no per-area carve-outs).
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
**Acceptance criteria for closing this epic:**
1. `coverage/floor.txt` reads `95` (or higher).
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
3. The pre-push gate computes coverage and rejects pushes below the floor.
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
**Strategy / child shape:**
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
**Out of scope:**
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
- Integration / E2E coverage (only widget + unit feed lcov).
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
**What lands:**
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
**Acceptance:**
- `make coverage-gate` passes on a clean checkout (floor matches reality).
- `make push-check` includes the gate.
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
**Out of scope:**
- Bumping the floor toward 95% — that''s the test-writing children''s job.
- Branch coverage — D-66 explicitly excludes.
- Per-area floors — D-66 chose a single global floor for simplicity.
**Notes:**
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
**Today''s distribution within `lib/src/terminal/src/`:**
| sub-area | hit / total | % | shape |
|---|---|---|---|
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
**Strategy — pure logic first, then UI:**
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
**Acceptance:**
- `lib/src/terminal/` line coverage ≥ 95%.
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
**Strategy on commit cadence:**
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
- Adds tests under `test/terminal/<sub-area>/`.
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
**Out of scope:**
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
- Branch coverage (D-66 explicitly excludes).
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
**Trigger paths in production code:**
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
**Acceptance:**
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
- Runs reflow shrink,
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
**Investigation notes (do not skip):**
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
**Two layers of "dead" here:**
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
**Two clean resolutions:**
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
- Collapse the two into a single hook and document the contract.
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
**Acceptance:**
- Either:
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
Trace:
1. `TerminalGestureHandler.build` (gesture_handler.dart:6582) wires:
```dart
onTertiaryTapDown: onSecondaryTapDown,
onTertiaryTapUp: onSecondaryTapUp,
```
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147152) are therefore **dead** — they''re declared but never invoked anywhere.
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 2425, 4547) and `TerminalGestureDetector` (lines 1314, 3031) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
**Two layers of "dead" again:**
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
**User-visible impact:**
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
**Resolutions, like T-93:**
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
```dart
details.kind == PointerDeviceKind.mouse
? renderTerminal.selectCharacters(details.localPosition)
: renderTerminal.selectWord(details.localPosition);
```
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
**Acceptance:**
- Either:
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
Neither happens today.
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
**Acceptance:**
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
**Acceptance:**
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
**Canvas pane (`builtin.canvas`):**
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
- Persist layout back to the `.canvas` file on disk.
**Graph view (`builtin.graph`):**
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
- Hover highlights connected subgraph; click opens the note in the editor.
- Filter pane: tag include/exclude, file glob, depth-from-active.
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
**Extension API (third-party Lua):**
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
- Manifest schema, capability gating, sandboxed FS/IPC access.
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
**Settings UI (`builtin.settings-ui`):**
- Schema-driven settings panel reading from the kernel SettingsStore.
- Render strategy: form fields keyed off the schema each subsystem registers.
- Edits write back to `.clide/settings.yaml`.
**Theming UI (`builtin.theme-picker` extends):**
- Live preview of the four bundled themes (D-44).
- Custom theme: import YAML, validate against schema, register at runtime.
- Per-component override surface (long horizon).
**Distributable builds:**
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
- Self-update mechanism — see T-47.
- License manifest auto-regen as part of the release build.
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
**Acceptance:**
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
- Fuzzy match against command title; recent / pinned commands float to the top.
**Implementation hints:**
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
**Acceptance:**
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
**Errno constants (audit item #23):**
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
**Logger standardization (audit item #22, partial #26):**
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
- The Flutter-host process often consumes stderr, so log lines disappear silently.
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
**Acceptance:**
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
- WCAG-AA contrast gate (D-22) passes against the theme.
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
**Acceptance:**
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
**Acceptance:**
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
**Options to evaluate:**
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
- Switch to a variable JetBrainsMono and request a specific weight axis position.
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
---
Refinement (2026-05-06):
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
**Concrete fix shape:**
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
| `lib/widgets/` | 537 / 1184 | 45.4% |
| `lib/src/pql/` | 45 / 94 | 47.9% |
| `lib/builtin/` | 269 / 550 | 48.9% |
| `lib/kernel/` | 889 / 1691 | 52.6% |
| `lib/src/daemon/` | 320 / 584 | 54.8% |
| `lib/src/pty/` | 130 / 216 | 60.2% |
| `lib/src/git/` | 356 / 518 | 68.7% |
| `lib/src/ipc/` | 110 / 156 | 70.5% |
| `lib/src/files/` | 98 / 133 | 73.7% |
| `lib/extension/` | 41 / 54 | 75.9% |
| `lib/src/editor/` | 107 / 129 | 82.9% |
| `lib/src/panes/` | 70 / 73 | 95.9% |
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
**End state:**
- Total line coverage ≥ 95% (no per-area carve-outs).
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
**Acceptance criteria for closing this epic:**
1. `coverage/floor.txt` reads `95` (or higher).
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
3. The pre-push gate computes coverage and rejects pushes below the floor.
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
**Strategy / child shape:**
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
**Out of scope:**
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
- Integration / E2E coverage (only widget + unit feed lcov).
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
**What lands:**
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
**Acceptance:**
- `make coverage-gate` passes on a clean checkout (floor matches reality).
- `make push-check` includes the gate.
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
**Out of scope:**
- Bumping the floor toward 95% — that''s the test-writing children''s job.
- Branch coverage — D-66 explicitly excludes.
- Per-area floors — D-66 chose a single global floor for simplicity.
**Notes:**
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
**Today''s distribution within `lib/src/terminal/src/`:**
| sub-area | hit / total | % | shape |
|---|---|---|---|
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
**Strategy — pure logic first, then UI:**
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
**Acceptance:**
- `lib/src/terminal/` line coverage ≥ 95%.
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
**Strategy on commit cadence:**
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
- Adds tests under `test/terminal/<sub-area>/`.
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
**Out of scope:**
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
- Branch coverage (D-66 explicitly excludes).
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
**Trigger paths in production code:**
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
**Acceptance:**
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
- Runs reflow shrink,
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
**Investigation notes (do not skip):**
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
**Trigger paths in production:**
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
**Acceptance:**
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
**Investigation notes:**
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
**Two layers of "dead" here:**
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
**Two clean resolutions:**
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
- Collapse the two into a single hook and document the contract.
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
**Acceptance:**
- Either:
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
Trace:
1. `TerminalGestureHandler.build` (gesture_handler.dart:6582) wires:
```dart
onTertiaryTapDown: onSecondaryTapDown,
onTertiaryTapUp: onSecondaryTapUp,
```
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147152) are therefore **dead** — they''re declared but never invoked anywhere.
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 2425, 4547) and `TerminalGestureDetector` (lines 1314, 3031) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
**Two layers of "dead" again:**
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
**User-visible impact:**
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
**Resolutions, like T-93:**
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
```dart
details.kind == PointerDeviceKind.mouse
? renderTerminal.selectCharacters(details.localPosition)
: renderTerminal.selectWord(details.localPosition);
```
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
**Acceptance:**
- Either:
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-96', 'description', 'The reader isolate spawned by `NativePty._spawnReaderAsync` (`lib/src/pty/native_pty.dart`) hangs intermittently — about 5% of fresh spawns — with poll() on the master fd never returning POLLIN or POLLHUP, even when the child has clearly written and exited. Probed: 2 hangs out of 30 sequential `/bin/sh -c "printf hello-pty"` spawns over a 30s/spawn timeout.
**Symptom:** test sees zero `pane.output` events and no `pane.exit`; the reader isolate sits in `poll(pfd, 1, 100)` returning 0 forever. The hang is per-spawn; a fresh `NativePty.start` recovers cleanly.
**Workaround in place:** `retry: 2` on every PTY-output-dependent test in `test/pty/session_test.dart` and `test/panes/registry_test.dart`. Three combined attempts at ~5% per-spawn fail rate ≈ 99.99% success. Comments at the test sites point back here.
**Acceptance:**
- Identify why a fresh master fd sometimes never delivers ready events. Candidates to investigate: a race between `forkpty()` and `Isolate.spawn` (the new isolate may see an FD table snapshot from an awkward moment), a missing fcntl flag on the master fd, lazy `DynamicLibrary.process()` symbol resolution in a freshly-spawned dart isolate, GC interference, or wasmtime/tree-sitter signal handlers (unlikely — the probe ran in a clean dart isolate with no wasmtime).
- Fix the underlying race so a single spawn delivers reliably.
- Remove the `retry: 2` decorations and the explanatory comments in both test files.
**Out of scope:** changing the PTY threading model or the reader-isolate architecture. The fix should keep `Isolate.spawn(_readLoop, ...)` as the I/O primitive.
**Cross-references:** D-5 (Dart core + sidecar dissolution; PTY is owned via FFI), `lib/src/pty/native_pty.dart#L235-262` (`_spawnReaderAsync`), `lib/src/pty/native_pty.dart#L265-293` (`_readLoop`).', 'The reader isolate spawned by `NativePty._spawnReaderAsync` (`lib/src/pty/native_pty.dart`) hangs intermittently — about 5% of fresh spawns — with poll() on the master fd never returning POLLIN or POLLHUP, even when the child has clearly written and exited. Probed: 2 hangs out of 30 sequential `/bin/sh -c "printf hello-pty"` spawns over a 30s/spawn timeout.
**Root cause — found 2026-05-17 via forensic probe:**
The "child" never reaches `execve`. `/proc/<child-pid>/stat` reports comm `(DartWorker)` (the Dart VM worker thread name) instead of `sh`, with state `S` (sleeping). The pty master fd is a real pty (tty-index assigned, fcntl flags 0x8002 = O_RDWR|O_NOCTTY), but `poll(fd, 500ms)` from the main isolate returns 0 — nothing was ever written.
This is **`fork()` in a multithreaded process** — a textbook async-signal-safety violation. The Dart VM runs multiple worker threads that hold libc locks (notably `malloc`). When `forkpty()` calls `fork()`, only the calling thread survives in the child, but the locks held by ghost-threads remain "locked forever." The child deadlocks before it can complete its post-fork → pre-execve setup.
Why not always? Lock state at fork() time is timing-dependent. ~95% of the time no Dart worker happens to hold a problematic lock, and execve proceeds. ~5% of the time it deadlocks.
**Workaround in place (test-only):** `retry: 2` on every PTY-output-dependent test in `test/pty/session_test.dart` and `test/panes/registry_test.dart`. Three combined attempts at ~5% per-spawn fail rate ≈ 99.99% combined success. Comments at the test sites point back here.
**Proper fix:** replace `forkpty()` with `posix_openpt()` + `unlockpt()` + `grantpt()` + `posix_spawn()` (with file actions wiring the pty slave to stdin/stdout/stderr). `posix_spawn` uses `vfork()` under glibc/musl, which keeps the parent suspended until execve completes — no Dart code ever runs in the child, no lock-deadlock possible. Linux and macOS both support this API.
**Immediate hardening alternative (Linux-only, simpler):** after spawn, sample `/proc/<pid>/comm` after ~250ms. If it equals the parent process''s comm, execve never ran → kill the child, surface `PtyException(''execve-deadlock'', ...)`, let the caller retry. This catches the deadlocked state deterministically instead of waiting for a poll timeout. It doesn''t fix the bug, but turns silent hangs into reportable errors.
**Acceptance for closing this ticket:**
1. Replace forkpty path with posix_openpt + posix_spawn (or implement the comm-check hardening as an interim).
2. Probe (200 sequential `printf hello`-and-exit spawns) reports zero hangs.
3. Remove the `retry: 2` decorations + explanatory comments in `test/pty/session_test.dart` (6 tests) and `test/panes/registry_test.dart` (1 test).
**Out of scope:** changing the reader-isolate architecture itself. `Isolate.spawn(_readLoop, ...)` for output stays.
**Cross-references:**
- D-5 (Dart core + sidecar dissolution; PTY is owned via FFI)
- `lib/src/pty/native_pty.dart:200` (the forkpty call site)
- `lib/src/pty/native_pty.dart:170-191` (child-side post-fork code that deadlocks)
- `lib/src/pty/native_pty.dart:235-262` (reader spawn — not the bug; downstream symptom)
- glibc posix_spawn docs: https://www.gnu.org/software/libc/manual/html_node/Process-Creation-Example.html
', NULL, '2026-05-17 18:05:33', '2026-05-17 18:05:33', '2026-05-17 18:05:33', NULL, '56d9ba7c49068a498a003e900cde416f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-96', 'status', 'backlog', 'done', NULL, '2026-05-17 18:11:18', '2026-05-17 18:11:18', '2026-05-17 18:11:18', NULL, '3215fc763c4f1dce75ea9e0bb2b9bbb6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-98', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:51:25', '2026-05-17 18:51:25', '2026-05-17 18:51:25', NULL, '0369c8264ec2c67b3bf3f28057404ec0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-98', 'status', 'in_progress', 'done', NULL, '2026-05-17 18:55:57', '2026-05-17 18:55:57', '2026-05-17 18:55:57', NULL, 'cb0ae539d5665bd9720b327d60a2cbed', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-101', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '55d14b4493a159695d4b40bbd19fff23', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-102', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '7fa075d7232df5302ffdfb5d1c3d79f2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-101', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '56e8da656c5fc53af241900e6313805e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-102', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '7f09d2a1501d3d072e071822c191f253', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-103', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, '2635d907f94402ec506d3b56dfc25b21', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-106', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, 'cc39b3fd718e99423823e640950ccf1b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-113', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, 'cc8605833ba5cb97e068eb5a6d57d8f4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-106', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, '8dccc83b93c5005db744a100a5b3baea', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-113', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, '8f7812831e564005eb796708543c54b3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-103', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, 'c7d1b76cb6854efb7e9990cc7204d899', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-100', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:23:24', '2026-05-17 19:23:24', '2026-05-17 19:23:24', NULL, '607b82f096b5d694fb75b7daf64dc2e0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-117', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:28:31', '2026-05-17 19:28:31', '2026-05-17 19:28:31', NULL, 'b5c56dff8a2c7d6f14ecf19231b63268', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-110', 'description', '`app.dart:90-148` routes all shortcuts through one root `KeyboardListener` — no per-context scoping; will conflict with text-input fields once any pane wants to capture keys. `KeybindingResolver.fromKeyEvent` keys off `logicalKey.keyLabel` which is layout-dependent (US-QWERTY `Ctrl+/` differs from AZERTY `Ctrl+:`).
**Fix:**
1. Replace the root `KeyboardListener` with scoped `Shortcuts`/`Actions` per slot.
2. Move `KeybindingResolver` to `physicalKey` or a stable mapping layer.
Coordinate with T-100 (Focus/Shortcuts wrapper for ClideTappable) and T-105 (focus traversal).
Source: consultants.md "UX — Findings — [Major]".', '**Superseded by T-117 — Done.**
The consultant findings here — single root `KeyboardListener` will conflict with text-input fields; `KebindingResolver.fromKeyEvent` keys off layout-dependent `logicalKey.keyLabel` — are both addressed by the keystroke mapper layer (T-117):
- Keymap-driven dispatch uses `LogicalKeyboardKey.keyId` (stable across layouts), not `keyLabel`.
- Root `KeyboardListener` now hands events straight to `KeymapService.resolveEvent`, which dispatches resolved Intents via `Actions.maybeInvoke` against the focused context — Actions providers per feature (palette, editor, etc.) handle their own intents; the root only handles global ones (text scale, generic command bridge). This is the scoped Shortcuts/Actions model the consultant prescribed.
Remaining cleanup (deletion of the now-vestigial `KeybindingResolver` class + the legacy `KeyboardListener` wrap once Flutter Shortcuts widget integration is on every feature) is small and lands as part of T-100 or its own follow-up.
Original text: Replace root KeyboardListener with scoped Shortcuts/Actions; move KebindingResolver off layout-dependent keyLabel.', NULL, '2026-05-17 19:39:08', '2026-05-17 19:39:08', '2026-05-17 19:39:08', NULL, '6364d644fbbfa903390ee78b5b402367', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-110', 'status', 'backlog', 'done', NULL, '2026-05-17 19:39:12', '2026-05-17 19:39:12', '2026-05-17 19:39:12', NULL, '6073b1bbc12001914d78d4cb419705dc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'description', 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
**Acceptance:**
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
- Fuzzy match against command title; recent / pinned commands float to the top.
**Implementation hints:**
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
**Progress (2026-05-17, T-117):** The keymap layer now binds `ctrl+shift+p` / `meta+shift+p` to `PaletteOpenIntent` in `assets/keymaps/default.yaml`. The intent resolves end-to-end through `KeymapService.resolveEvent` → `Actions.maybeInvoke`. **Still pending**: an `Actions` provider somewhere in the tree that handles `PaletteOpenIntent` by calling `kernel.palette.open()`, plus the arrow-key / Escape / Enter handlers on `ClidePalette` itself. Those land as part of T-100 (palette keyboard nav).
**Acceptance:**
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
- Fuzzy match against command title; recent / pinned commands float to the top.
**Implementation hints:**
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
- Bindings live in the keymap (T-117) — not in `lib/kernel/src/commands/keybindings.dart` (that file is legacy).
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-17 19:39:23', '2026-05-17 19:39:23', '2026-05-17 19:39:23', NULL, '50203b7db93490ac779fe496865cf3a0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-64', 'description', 'Ship a VS Code-compatible keybinding preset that maps standard VS Code shortcuts to clide commands. Users select it in settings. Covers file navigation, editor actions, panel toggles, search, and terminal.', 'Ship a VS Code-compatible keybinding preset that maps standard VS Code shortcuts to clide commands. Users select it in settings. Covers file navigation, editor actions, panel toggles, search, and terminal.
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is now in place. Implementation is now just authoring `assets/keymaps/vscode.yaml` against the typed Intents in `lib/kernel/src/keymap/intents.dart` (plus `command:<id>` bindings for VS-Code-specific commands the preset wants to bind to clide commands). Users will switch presets via `app.keymap.preset = vscode` once a settings UI exists, or directly via the setting today.
**Acceptance:**
1. `assets/keymaps/vscode.yaml` ships covering the documented VS Code default keybindings.
2. `KeymapService.setPreset("vscode")` activates the preset and all asserted bindings resolve as expected.
3. The preset uses when-clauses where VS Code does (`editor.focused`, `inputFocused`, `palette.open`, …).
4. A regression test loads the preset and asserts a representative subset (e.g. ctrl+p → quick-open command, ctrl+shift+p → palette).
**Out of scope:** clide commands that have no VS Code analogue (those keep their default-preset bindings).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'a97758128bd490f603f11a68883a7193', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-65', 'description', 'Ship a Vim-compatible keybinding preset with modal editing support (normal/insert/visual modes). Maps Vim motions and commands to clide editor and navigation actions. Users select it in settings.', 'Ship a Vim-compatible keybinding preset with modal editing support (normal/insert/visual modes). Maps Vim motions and commands to clide editor and navigation actions. Users select it in settings.
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is in place; modal Vim presets are more involved than the VS Code preset (T-64) because modes need to be expressed as scope flags (`vim.normal`, `vim.insert`, `vim.visual`) that the when-clause grammar can branch on. Implementation work:
1. Author `assets/keymaps/vim.yaml` using the typed Intents + `command:<id>` bindings.
2. Add a small mode-tracking service that publishes `vim.<mode>` scope flags via `KeymapService.setScopeFlag`.
3. Bind `Esc` to mode-reset → normal; `i` (when `vim.normal`) → enter insert; etc.
**Acceptance:**
1. `assets/keymaps/vim.yaml` ships covering the documented Vim default keybindings for editor / navigation / panes.
2. `KeymapService.setPreset("vim")` + the mode-tracking service together produce correct mode transitions.
3. A regression test exercises a representative motion (`j` → cursor down) and a mode change (`i` → insert).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'bf10f0149ccaa1e02050280b28305816', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-66', 'description', 'Ship a JetBrains/IntelliJ-compatible keybinding preset mapping standard JetBrains shortcuts to clide commands. Covers navigation, refactoring, search, run/debug, and tool windows.', 'Ship a JetBrains/IntelliJ-compatible keybinding preset mapping standard JetBrains shortcuts to clide commands. Covers navigation, refactoring, search, run/debug, and tool windows.
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is in place. Implementation is authoring `assets/keymaps/jetbrains.yaml` against the typed Intents + `command:<id>` bindings, plus when-clauses for the contexts JetBrains presets typically scope to (`editor.focused`, `inputFocused`, etc.).
**Acceptance:**
1. `assets/keymaps/jetbrains.yaml` ships covering the documented IntelliJ default keybindings.
2. `KeymapService.setPreset("jetbrains")` activates the preset and all asserted bindings resolve.
3. A regression test exercises a representative subset (e.g. shift+shift → quick-open command — see Q-9 if the search-everywhere overlay needs its own intent).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'c1129f4cb3454b99e1cbf645ca56bea5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-117', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:39:40', '2026-05-17 19:39:40', '2026-05-17 19:39:40', NULL, '75b2fd14ceb7daf28645ccf84cb041f1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-100', 'status', 'in_progress', 'in_progress', NULL, '2026-05-17 19:40:44', '2026-05-17 19:40:44', '2026-05-17 19:40:44', NULL, '65600862f5787501dcd057756cdb4313', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-100', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:47:50', '2026-05-17 19:47:50', '2026-05-17 19:47:50', NULL, '7d3c0ad4c47d3271ac35c5159e6e9d0e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-105', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:52:39', '2026-05-17 19:52:39', '2026-05-17 19:52:39', NULL, '129acb5d6b1882cb6ed4c9be46fd3e81', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-105', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:56:28', '2026-05-17 19:56:28', '2026-05-17 19:56:28', NULL, '62dfdadc31b2c231afd837fb46f0a114', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-108', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:58:13', '2026-05-17 19:58:13', '2026-05-17 19:58:13', NULL, 'a5dbfe5881e152e82538181b0fe56609', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-108', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:05:38', '2026-05-17 20:05:38', '2026-05-17 20:05:38', NULL, '6815472fbfaa6d7740945b42d4c87f57', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:06:30', '2026-05-17 20:06:30', '2026-05-17 20:06:30', NULL, '52271781bf987a984d9f219956034d1f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-112', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:11:16', '2026-05-17 20:11:16', '2026-05-17 20:11:16', NULL, '6d51964a3ad7f1948f54a7c769336841', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:13:36', '2026-05-17 20:13:36', '2026-05-17 20:13:36', NULL, '0bad40fa7f165f828bb0c0757044491e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-118', 'status', 'backlog', 'done', NULL, '2026-05-17 20:53:41', '2026-05-17 20:53:41', '2026-05-17 20:53:41', NULL, 'ff0126655805fed0ebe45f313c5ceff7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:04:38', '2026-05-18 07:04:38', '2026-05-18 07:04:38', NULL, '8cf198f8c824fa8f46277b7e22e68f0c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-111', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:43:45', '2026-05-18 07:43:45', '2026-05-18 07:43:45', NULL, '792efcf7c772de7cda7069ea3fc3ac0a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-104', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 07:49:44', '2026-05-18 07:49:44', '2026-05-18 07:49:44', NULL, 'bf7f2746c252f8b8893f4fdc06d459b8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-104', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:53:10', '2026-05-18 07:53:10', '2026-05-18 07:53:10', NULL, 'ce2b66658977edc3fddc78079d7852e5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-109', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 08:02:06', '2026-05-18 08:02:06', '2026-05-18 08:02:06', NULL, 'ee75fea173ae492a6ad8a1d888a2142d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-109', 'status', 'in_progress', 'done', NULL, '2026-05-18 08:07:23', '2026-05-18 08:07:23', '2026-05-18 08:07:23', NULL, '9dd61b76946909aa44edaf9d1c1741a0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-121', 'status', 'backlog', 'done', NULL, '2026-05-18 08:22:19', '2026-05-18 08:22:19', '2026-05-18 08:22:19', NULL, '861af9256207314d681e51abc77c386f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-115', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 08:23:34', '2026-05-18 08:23:34', '2026-05-18 08:23:34', NULL, '94f4a894c1dddfa87ee8306a9758c808', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-115', 'status', 'in_progress', 'done', NULL, '2026-05-18 08:26:05', '2026-05-18 08:26:05', '2026-05-18 08:26:05', NULL, 'b8978359fc1b83ea0bcfc6cd5d34d5de', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-115', 'status', 'done', 'done', NULL, '2026-05-18 09:30:59', '2026-05-18 09:30:59', '2026-05-18 09:30:59', NULL, '2039c525646caaf89a735454f7278162', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-107', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:27:25', '2026-05-18 10:27:25', '2026-05-18 10:27:25', NULL, 'e40d2c9ddd9915ad2e32302bd555c99f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-107', 'status', 'in_progress', 'done', NULL, '2026-05-18 10:30:30', '2026-05-18 10:30:30', '2026-05-18 10:30:30', NULL, 'd5ecc2578b5b48991d07531eb2166529', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-116', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:35:47', '2026-05-18 10:35:47', '2026-05-18 10:35:47', NULL, '59b70e93c913952d4f13b9ad2320e060', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-116', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:22:46', '2026-05-18 11:22:46', '2026-05-18 11:22:46', NULL, '16fd285587d7a37090d2a21f4307c8e8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'ready', 'in_progress', NULL, '2026-05-18 11:49:28', '2026-05-18 11:49:28', '2026-05-18 11:49:28', NULL, 'e25e13a5afcbc376b3c57312c6f320ea', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-74', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:51:40', '2026-05-18 11:51:40', '2026-05-18 11:51:40', NULL, '4343b4758e9ffbf3e046d11d65dfeb1f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-124', 'description', 'First slice of T-99(a). Foundation for the rest.
Open a per-user unix-domain socket on Flutter app boot. Accept JSON-lines per the existing IpcRequest envelope. Route each request through the existing DaemonDispatcher (already wired in main.dart via daemonClientFactory). Tear down on app shutdown.
Acceptance:
1. lib/src/ipc/server.dart exists; binds an AF_UNIX socket at a per-user path (resolve in this ticket; current Q-record references Q-1 / Q-3 for auth/persistence concerns but the bare path strategy belongs here).
2. App boot starts the server; app shutdown closes the socket file cleanly.
3. `socat - UNIX-CONNECT:$SOCK <<< ''{"command":"git.status"}''` returns a JSON-line response. Tests use a synthetic socket fixture.
4. Multi-connection accept loop — concurrent socat invocations serialise through the dispatcher (single Flutter event loop) but don''t fail each other.
5. No client yet — that lands in T-126.
Source: T-99 sketch. Coordinate with: T-127, T-130.', 'First slice of T-99(a). Foundation for the rest.
Open a per-workspace unix-domain socket on Flutter app boot. Accept JSON-lines per the existing IpcRequest envelope. Route each request through the existing DaemonDispatcher (already wired in main.dart via daemonClientFactory). Tear down on app shutdown.
**Architectural commitments (read these first):**
- D-70 — socket path is `$XDG_RUNTIME_DIR/clide/<sha256(workspace-root)[:16]>.sock` on Linux, `$HOME/Library/Caches/clide/<sha256(workspace-root)[:16]>.sock` on macOS. No env override. Workspace root = git toplevel.
- D-71 — socket file is `0600`, parent dir is `0700`. No token auth at this layer.
- D-72 — multi-connection accept loop, serial dispatch on the main isolate. Per-handler isolate offload as needed; not the IPC layer''s concern.
**Acceptance:**
1. lib/src/ipc/server.dart exists; binds an AF_UNIX socket at the D-70 path; creates the parent dir with the D-71 perms.
2. App boot starts the server; app shutdown closes the socket file cleanly and removes it.
3. `socat - UNIX-CONNECT:$SOCK <<< ''{"command":"git.status"}''` returns a JSON-line response. Tests use a synthetic socket fixture (tempdir + XDG_RUNTIME_DIR override at the env level).
4. Multi-connection accept loop — concurrent socat invocations interleave at the I/O level but serialise through DaemonDispatcher (per D-72) without failing each other.
5. Stale socket on boot (left over from a crashed clide) is detected and unlinked before binding — same `live-daemon probe` pattern already used elsewhere in the codebase.
6. No client yet — that lands in T-126.
Source: T-99 sketch. Coordinates with: T-127 (InProcessClient swap), T-130 (MCP).', NULL, '2026-05-18 12:42:56', '2026-05-18 12:42:56', '2026-05-18 12:42:56', NULL, 'abb7a039f73d430cdba594b2a4ac381a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-124', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 12:45:10', '2026-05-18 12:45:10', '2026-05-18 12:45:10', NULL, 'aa1b70849399f4caee043d40e48d1edf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-124', 'status', 'in_progress', 'done', NULL, '2026-05-18 12:51:08', '2026-05-18 12:51:08', '2026-05-18 12:51:08', NULL, 'b928ce09ae0b7dbb77bf93f8c0593657', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-125', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 15:49:20', '2026-05-18 15:49:20', '2026-05-18 15:49:20', NULL, 'ac0838fac9e3c97f1728256910776bed', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-125', 'status', 'in_progress', 'done', NULL, '2026-05-18 15:51:09', '2026-05-18 15:51:09', '2026-05-18 15:51:09', NULL, '938624de487aa2f02e034424cae40c77', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-126', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 15:54:20', '2026-05-18 15:54:20', '2026-05-18 15:54:20', NULL, '3266e7ea943867f49ff20a90b7f12ae6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-126', 'status', 'in_progress', 'done', NULL, '2026-05-18 16:04:29', '2026-05-18 16:04:29', '2026-05-18 16:04:29', NULL, 'b6c783f599585e55ecc36f69ccbcefb1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-127', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 09:57:18', '2026-05-19 09:57:18', '2026-05-19 09:57:18', NULL, '64a14cb5d8b9274fd9d72d1e7474040c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-127', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:03:26', '2026-05-19 10:03:26', '2026-05-19 10:03:26', NULL, 'b8bf877d2057f7baa5acf6af7a13d529', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-128', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 10:05:09', '2026-05-19 10:05:09', '2026-05-19 10:05:09', NULL, '763a80ec25b5fe712e5ad44b75dbb22f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-128', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:06:46', '2026-05-19 10:06:46', '2026-05-19 10:06:46', NULL, 'cc53b46764f28515d735aa77c8eeef2c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-129', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 12:06:39', '2026-05-19 12:06:39', '2026-05-19 12:06:39', NULL, '575584f46d21b51bab3e69da1b940718', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-129', 'status', 'in_progress', 'done', NULL, '2026-05-19 12:19:51', '2026-05-19 12:19:51', '2026-05-19 12:19:51', NULL, 'e9bf50652357521c7ee74bd6577e8108', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-130', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 12:24:44', '2026-05-19 12:24:44', '2026-05-19 12:24:44', NULL, '63dd7e0c4346a5d3cdd81c9c477339c3', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-130', 'status', 'in_progress', 'done', NULL, '2026-05-19 12:33:52', '2026-05-19 12:33:52', '2026-05-19 12:33:52', NULL, 'f3060e086423af96f99684e3718cac45', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-131', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 13:10:27', '2026-05-19 13:10:27', '2026-05-19 13:10:27', NULL, 'c49e76c430de0cc42d7919ade9c5cfa5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'status', 'backlog', 'ready', NULL, '2026-05-19 13:14:01', '2026-05-19 13:14:01', '2026-05-19 13:14:01', NULL, '726631190b62bd38fb34f7201d2f27ee', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-120', 'description', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
This needs design before code:
- Where the schema lives (per-handler? a central registry?)
- How it composes with the IpcRequest envelope
- Whether it generates the existing handler boilerplate or wraps it
- Coordination with T-99''s IPC architecture decision (if we go socket-server, the schema becomes the wire contract)
Source: consultants.md "Security — Findings — [Major]" item 1.', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
Re-scope (2026-05-19, post T-99): the wire contract is now real — IpcRequest envelopes flow over the unix socket (per D-70/71/72) and through the C clide client which serialises raw argv under the `_argv` sentinel (per D-72/D-6). The argv parser (lib/src/cli/argv_to_request.dart) is the de-facto schema today; T-120 should formalise it by:
- Lifting the per-cmd grammar from argv_to_request.dart into a typed registry keyed by cmd, sharing it with both the argv parser and the dispatcher.
- Using the same registry to validate inbound IpcRequests at dispatch time (regex/charset constraints on branch/remote/path args).
- Exposing the registry to the MCP server (T-130) so the `tools/list` payload is generated rather than hand-rolled.
Design still open:
- Where the schema lives (per-handler? central registry?).
- Whether it generates handler boilerplate or wraps existing handlers.
Source: consultants.md "Security — Findings — [Major]" item 1.', NULL, '2026-05-19 13:14:12', '2026-05-19 13:14:12', '2026-05-19 13:14:12', NULL, '3511c5e7731f568de7af33c1970ad97e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-99', 'status', 'backlog', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, '5ff47ab0fc7fb8cb8e9e039be48ada2a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-131', 'status', 'in_progress', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, 'a57b3fbe4e6815fcbb28691a08130801', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'status', 'ready', 'in_progress', NULL, '2026-05-19 14:44:43', '2026-05-19 14:44:43', '2026-05-19 14:44:43', NULL, '1d5048cf9a031c504aa3df570a7b0cae', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'status', 'in_progress', 'done', NULL, '2026-05-20 07:00:06', '2026-05-20 07:00:06', '2026-05-20 07:00:06', NULL, '1f2c2bcc722417d8f4c5bfa27faf0c93', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-120', 'description', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
Re-scope (2026-05-19, post T-99): the wire contract is now real — IpcRequest envelopes flow over the unix socket (per D-70/71/72) and through the C clide client which serialises raw argv under the `_argv` sentinel (per D-72/D-6). The argv parser (lib/src/cli/argv_to_request.dart) is the de-facto schema today; T-120 should formalise it by:
- Lifting the per-cmd grammar from argv_to_request.dart into a typed registry keyed by cmd, sharing it with both the argv parser and the dispatcher.
- Using the same registry to validate inbound IpcRequests at dispatch time (regex/charset constraints on branch/remote/path args).
- Exposing the registry to the MCP server (T-130) so the `tools/list` payload is generated rather than hand-rolled.
Design still open:
- Where the schema lives (per-handler? central registry?).
- Whether it generates handler boilerplate or wraps existing handlers.
Source: consultants.md "Security — Findings — [Major]" item 1.', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
---
Design resolved 2026-05-20 (see D-74). Co-registration model, NOT a central map:
- The argv parser (lib/src/cli/argv_to_request.dart) only knows syntactic shape (identifier/flag charset, -- terminator); it has NO per-command argument knowledge. So this is build-fresh, not a lift — each handler currently hand-reads its own keys.
- Extend the DaemonDispatcher registration API to carry an optional typed schema per command. The dispatcher accumulates a cmd->schema registry as commands register (built-in register*Commands modules supply theirs; extension CommandContributions carry their own — central static map rejected because it cannot see extension-contributed commands, D-46).
- DaemonDispatcher.dispatch validates req.args against schema[cmd] BEFORE invoking the handler (hook at dispatcher.dart line 39, the `return h(req)` call). Violation -> userError (sysexit 64). Commands with no schema dispatch unvalidated (opt-in per command) until migrated.
- Constraint vocabulary is hand-rolled (prefer-zero-deps): per-arg required/optional, kind, charset/regex, numeric range. Collapses the _ResizeArgs-style hand-lifts (panel_commands.dart, T-119) into schema.
- KEEP existing T-104 checks as defense-in-depth: validateGitRef (lib/src/git/operations.dart:46) and the count/path caps (lib/src/daemon/git_commands.dart:18,23). The git client is callable directly from the Flutter UI, not only via the dispatcher — do not remove them.
- OUT OF SCOPE: MCP tools/list generation from the registry — deferred to the T-130 track.
Acceptance:
1. Dispatcher validates req.args against a registered per-command schema before the handler runs; violations return userError.
2. At least the security-relevant commands (git ref/remote/path args, panel.resize) carry schemas; argv-injection + caps still enforced (no regression vs T-104).
3. validateGitRef + git caps retained as defense-in-depth.
4. No third-party validation dep; no lint suppressions.
5. Tests cover: schema accept/reject per constraint kind, dispatcher-level rejection, and that an unschema-d command still dispatches.
Source: consultants.md "Security — Findings — [Major]" item 1. Decision: D-74.', NULL, '2026-05-20 15:54:35', '2026-05-20 15:54:35', '2026-05-20 15:54:35', NULL, '7f972739c82134897869432a3978d6ec', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-120', 'status', 'backlog', 'in_progress', NULL, '2026-05-20 15:54:38', '2026-05-20 15:54:38', '2026-05-20 15:54:38', NULL, 'fa11190ac1bd9cd8f67e3041a8ca7236', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-120', 'status', 'in_progress', 'done', NULL, '2026-05-20 16:02:35', '2026-05-20 16:02:35', '2026-05-20 16:02:35', NULL, 'efffdd07c7281977d37ce0ad4285a36a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-97', 'status', 'backlog', 'done', NULL, '2026-05-20 16:02:46', '2026-05-20 16:02:46', '2026-05-20 16:02:46', NULL, '9c36d26b5ac3702441a87c7f88dbf830', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'description', 'T-115''s render-only widget test at test/builtin/welcome/widget_test.dart ''sticky-startup toggle renders on a seeded recent row'' verifies the toggle is constructed but does not exercise the tap → setStickyStartup path. Adding a tap-driven assertion hung the 10-minute Flutter test timeout consistently (find.byTooltip, find.bySemanticsLabel, and find.byKey + tap variants all reproduced).
Hypothesis: ClideTooltip wraps the toggle in a MouseRegion whose onEnter awaits Future.delayed(showDelay). Some path during tap simulation (or surrounding pump scheduling) keeps that timer pending, and the test runner waits the full 10-minute idle timeout before declaring it stuck.
Next steps:
- Reproduce in isolation against a stripped-down harness pumping just the _StickyToggle widget.
- Determine whether the hang is the MouseRegion timer or another future (e.g., the welcome-tab activation, toolchain check).
- Either fix the underlying cause or use fakeAsync.run() to drain the timers explicitly.
Until then, ProjectManager unit tests in test/kernel/src/project_test.dart cover the sticky-startup logic (14 cases including round-trip, no-op, idempotent flip, ambiguity, and openStickyOrNothing).', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle.
Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout.
Investigation 2026-05-22 (narrowed the cause):
- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting.
- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable).
- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation.
- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it).
- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer).
Next steps:
- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority.
- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target).
ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).', NULL, '2026-05-22 06:55:02', '2026-05-22 06:55:02', '2026-05-22 06:55:02', NULL, 'b7c13548cef5d962c7b5b4500ea39b9e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'priority', 'low', 'medium', NULL, '2026-05-22 06:55:02', '2026-05-22 06:55:02', '2026-05-22 06:55:02', NULL, 'e7d5dd1b89eded661de7f6d64edea04c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'description', 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
| `lib/widgets/` | 537 / 1184 | 45.4% |
| `lib/src/pql/` | 45 / 94 | 47.9% |
| `lib/builtin/` | 269 / 550 | 48.9% |
| `lib/kernel/` | 889 / 1691 | 52.6% |
| `lib/src/daemon/` | 320 / 584 | 54.8% |
| `lib/src/pty/` | 130 / 216 | 60.2% |
| `lib/src/git/` | 356 / 518 | 68.7% |
| `lib/src/ipc/` | 110 / 156 | 70.5% |
| `lib/src/files/` | 98 / 133 | 73.7% |
| `lib/extension/` | 41 / 54 | 75.9% |
| `lib/src/editor/` | 107 / 129 | 82.9% |
| `lib/src/panes/` | 70 / 73 | 95.9% |
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
**End state:**
- Total line coverage ≥ 95% (no per-area carve-outs).
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
**Acceptance criteria for closing this epic:**
1. `coverage/floor.txt` reads `95` (or higher).
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
3. The pre-push gate computes coverage and rejects pushes below the floor.
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
**Strategy / child shape:**
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
**Out of scope:**
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
- Integration / E2E coverage (only widget + unit feed lcov).
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
', '## OUTCOME (closed 2026-05-18)
Epic met. Final measured line coverage **95.27%** (9551 / 10025), floor `coverage_floor: 95` in `pubspec.yaml`, gate wired into `make push-check`.
Final per-area (vs. opening 34.9% total):
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 3185 / 3188 | **99.9%** (was 0.4%) |
| `lib/extension/` | 54 / 54 | 100.0% |
| `lib/src/editor/` | 129 / 129 | 100.0% |
| `lib/src/files/` | 139 / 140 | 99.3% |
| `lib/src/pql/` | 91 / 92 | 98.9% |
| `lib/src/panes/` | 75 / 76 | 98.7% |
| `lib/src/git/` | 517 / 531 | 97.4% |
| `lib/src/daemon/` | 616 / 644 | 95.7% |
| `lib/kernel/` | 2122 / 2233 | 95.0% |
| `lib/src/ipc/` | 429 / 452 | 94.9% |
| `lib/widgets/` | 1178 / 1261 | 93.4% |
| `lib/builtin/` | 754 / 875 | 86.2% |
| `lib/src/pty/` | 177 / 262 | 67.6% (forkpty paths excluded from gate) |
Acceptance: (1) floor reads 95 done (2) total >=95% done (3) gate hard-fails below floor done (4) no coverage-dodging suppressions done. The strategy''s tail per-area sweeps (widgets/builtin/pty) didn''t all individually reach 95%, but the contract is *total* line coverage, which is satisfied; remaining per-area headroom can be its own tickets if desired.
---
Today: total line coverage **34.9%** (2984/8549 lines). Per area:
| area | hit / total | % |
|---|---|---|
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
| `lib/widgets/` | 537 / 1184 | 45.4% |
| `lib/src/pql/` | 45 / 94 | 47.9% |
| `lib/builtin/` | 269 / 550 | 48.9% |
| `lib/kernel/` | 889 / 1691 | 52.6% |
| `lib/src/daemon/` | 320 / 584 | 54.8% |
| `lib/src/pty/` | 130 / 216 | 60.2% |
| `lib/src/git/` | 356 / 518 | 68.7% |
| `lib/src/ipc/` | 110 / 156 | 70.5% |
| `lib/src/files/` | 98 / 133 | 73.7% |
| `lib/extension/` | 41 / 54 | 75.9% |
| `lib/src/editor/` | 107 / 129 | 82.9% |
| `lib/src/panes/` | 70 / 73 | 95.9% |
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
**End state:**
- Total line coverage ≥ 95% (no per-area carve-outs).
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
**Acceptance criteria for closing this epic:**
1. `coverage/floor.txt` reads `95` (or higher).
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
3. The pre-push gate computes coverage and rejects pushes below the floor.
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
**Strategy / child shape:**
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
**Out of scope:**
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
- Integration / E2E coverage (only widget + unit feed lcov).
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
', NULL, '2026-05-22 13:37:43', '2026-05-22 13:37:43', '2026-05-22 13:37:43', NULL, 'b83aa45596e35a95e689b922c66c27ef', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-89', 'status', 'in_progress', 'done', NULL, '2026-05-22 13:37:46', '2026-05-22 13:37:46', '2026-05-22 13:37:46', NULL, '5698b8e8cbd5113a84791ae2dad26c6d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-91', 'status', 'in_progress', 'done', NULL, '2026-05-22 13:37:46', '2026-05-22 13:37:46', '2026-05-22 13:37:46', NULL, 'dad314c05e04d8f17357ac2ea054a9bd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-134', 'description', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
---
Refinement (2026-05-22, refinement-agent review):
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, '05456ee9ae72721893f6850158d59791', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'description', 'Make clide''s native text widgets selectable so a SelectionArea can select across them. Change lib/widgets/src/clide_markdown.dart RichText (line ~68) -> Text.rich; same for lib/widgets/src/clide_code_block.dart; add a selectionBackground token to SurfaceTokens (lib/kernel/src/theme/tokens.dart + resolver + bundled theme YAMLs); verify copy via existing ClideClipboard (lib/kernel/src/clipboard.dart). Acceptance: a SelectionArea-wrapped column of markdown + code blocks supports select-across-blocks + Ctrl+C copy; widget tests; a11y unaffected; coverage >= floor. Reusable foundation. Blocks T5.', 'Make clide''s native text widgets selectable so a SelectionArea can select across them. Change lib/widgets/src/clide_markdown.dart RichText (line ~68) -> Text.rich; same for lib/widgets/src/clide_code_block.dart; add a selectionBackground token to SurfaceTokens (lib/kernel/src/theme/tokens.dart + resolver + bundled theme YAMLs); verify copy via existing ClideClipboard (lib/kernel/src/clipboard.dart). Acceptance: a SelectionArea-wrapped column of markdown + code blocks supports select-across-blocks + Ctrl+C copy; widget tests; a11y unaffected; coverage >= floor. Reusable foundation. Blocks T5.
---
Refinement (2026-05-22, refinement-agent review):
- Convert ALL RichText sites, not one: clide_markdown.dart lines 68, 238, 242 + clide_code_block.dart:79 -> Text.rich (Text.rich wires the SelectionArea selectionRegistrar; same underlying render object).
- Tables (clide_markdown.dart:230) and tappable WidgetSpan links (ClideTappable ~293-310) do NOT participate in SelectionArea. v1 scope: text + code blocks are the selectable surface; tables/link-spans are known non-selectable islands (revisit later). Acceptance = select+copy across text/code blocks.
- selectionBackground token: add to SurfaceTokens (field + TokenKeys + TokenKeys.all) + resolver.dart (_defaultSurfaceMap default + constructor). Bundled theme YAMLs do NOT need it (default map covers it); per-theme override only if the default reads wrong. Do NOT retune existing palettes (published themes are user contracts).
- Add a selection contrast pair (globalForeground on selectionBackground) to lib/kernel/src/theme/contrast.dart canonicalPairs; re-run test/a11y. (Note: summer-night.yaml is missing from contrast_test bundledPaths -- pre-existing inconsistency, out of scope.)
- COPY PATH CORRECTION: SelectionArea Ctrl+C goes through Flutter services.Clipboard directly, NOT ClideClipboard. Test verifies Clipboard.getData returns the selection after copy.
- Coverage floor is 95%.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, '5e218db621577a309b493611dfb72b63', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-136', 'description', 'New lib/builtin/claude/src/transcript_reader.dart. Resolve munged project path (/ -> -) + session-id; tail the append-only JSONL; parse records (type: user/assistant/attachment/tool; message; agentId; isSidechain) into ConversationItem models; drift-guard on the version field (log + degrade on unknown schema). Paths validated this session: ~/.claude/projects/<munged>/<sid>.jsonl (lead) and <sid>/subagents/agent-<id>.jsonl (teammates). Acceptance: unit tests against a snapshotted JSONL fixture (assistant text, tool_use, tool_result); appended lines stream in; unknown version degrades gracefully + logs. Blocks T5.', 'New lib/builtin/claude/src/transcript_reader.dart. Resolve munged project path (/ -> -) + session-id; tail the append-only JSONL; parse records (type: user/assistant/attachment/tool; message; agentId; isSidechain) into ConversationItem models; drift-guard on the version field (log + degrade on unknown schema). Paths validated this session: ~/.claude/projects/<munged>/<sid>.jsonl (lead) and <sid>/subagents/agent-<id>.jsonl (teammates). Acceptance: unit tests against a snapshotted JSONL fixture (assistant text, tool_use, tool_result); appended lines stream in; unknown version degrades gracefully + logs. Blocks T5.
---
Refinement (2026-05-22, refinement-agent review): READY. Verified against real transcripts.
- Munge = absolutePath.replaceAll(''/'',''-'') (KEEP the leading ''-''; dirs start with -var-).
- Envelope fields: type, uuid, parentUuid, isSidechain, sessionId, version, timestamp. Render: user (message.content = String OR array of {type:text|tool_result}); assistant (message.content = array of {type:thinking|text|tool_use}). Skip attachment/system/last-prompt/permission-mode/file-history-snapshot/queue-operation (bookkeeping).
- SESSION-ID: claude does not expose its session UUID via argv/env (verified in claude_pane spawn). Approach: newest *.jsonl by mtime in ~/.claude/projects/<munged>/; poll mtime and switch the tail when a newer session appears.
- Subagent transcripts: <munged>/<session-uuid>/subagents/agent-<id>.jsonl + sibling .meta.json ({agentType,description}); each record carries agentId.
- Tail: dart:io File.openRead with a byte-position cursor (File.length + RandomAccessFile.setPosition on poll). Do NOT use lib/src/files/watcher.dart. Zero new deps.
- Logger: lib/kernel/src/log.dart for the version drift-guard. Coverage floor 95%.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, 'c040ed1bf6bad363c3d199bead879374', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-136', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '176db3f23c4a758fb601c50c9ae2644b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-134', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '5e5f91eab17e4acefcc0c9e286787742', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '79aed523589f14481c115538ae8b357c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, 'e11fb2f37bf99262989f1881a7deed9c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-133', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '010ab0fda58e4be2ea699c102e1b2695', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-135', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '1bfd08b75a99bbf3c46408b0c5f799f2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-136', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, 'eea74c531ac4796fe6dba221018e80dd', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-134', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:55:11', '2026-05-22 17:55:11', '2026-05-22 17:55:11', NULL, '13385e9b6de3d3979b03c22de9db73f6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-134', 'description', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
---
Refinement (2026-05-22, refinement-agent review):
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
---
Refinement (2026-05-22, refinement-agent review):
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.
---
Findings (2026-05-22): docs/spikes/cc-team-internals-2.1.148.md. All 5 checks answered from real artifacts + tmux manual + synthetic control-mode test (no live run/quota). Key risk for T-139: pane->teammate->transcript identity joins only on agentType (config <-> .meta.json), ambiguous for same-type teammates -> needs joinedAt/mtime or lead-transcript tiebreaker. tmux 3.6a has no %pane-died; poll list-panes -a. Teammate transcripts at <munged>/<sid>/subagents/agent-<hex>.jsonl + .meta.json{agentType,description}.', NULL, '2026-05-22 17:55:11', '2026-05-22 17:55:11', '2026-05-22 17:55:11', NULL, 'dbf4f56d1ec9d2234f255826c5f88f68', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-137', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 17:58:07', '2026-05-22 17:58:07', '2026-05-22 17:58:07', NULL, '4e67262d4b04f8541e71fe8f03106fbf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-137', 'status', 'in_progress', 'done', NULL, '2026-05-22 18:14:32', '2026-05-22 18:14:32', '2026-05-22 18:14:32', NULL, 'a284c7249dda3c63a52a202bad07f105', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 21:08:05', '2026-05-22 21:08:05', '2026-05-22 21:08:05', NULL, 'c3dfefe24d912081cd89ac0be066b641', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-138', 'status', 'in_progress', 'done', NULL, '2026-05-22 21:49:09', '2026-05-22 21:49:09', '2026-05-22 21:49:09', NULL, 'e4be02e339d638da70a4e7ce0f850691', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:09:09', '2026-05-22 22:09:09', '2026-05-22 22:09:09', NULL, 'b799657bf436bc44e306ef71658c80e5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-142', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:18:02', '2026-05-22 22:18:02', '2026-05-22 22:18:02', NULL, '374be08610920e404c8394bc10b1e12a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-143', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:31:25', '2026-05-22 22:31:25', '2026-05-22 22:31:25', NULL, 'e2a16eb8af0afc30a5d0fc88c523e507', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-143', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:36:09', '2026-05-22 22:36:09', '2026-05-22 22:36:09', NULL, '9b8859ea2e8db7083bfb9d2ab17c1c57', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:41:28', '2026-05-22 22:41:28', '2026-05-22 22:41:28', NULL, 'eda6f75e88401ee5b445e7edfc312f82', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-144', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:43:45', '2026-05-22 22:43:45', '2026-05-22 22:43:45', NULL, 'da17b2710e17b1821882d47724790cbb', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:50:41', '2026-05-22 22:50:41', '2026-05-22 22:50:41', NULL, 'ad063f0c3c65e5b35d3dcea036412ad9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-139', 'status', 'in_progress', 'done', NULL, '2026-05-22 23:16:28', '2026-05-22 23:16:28', '2026-05-22 23:16:28', NULL, 'add82dc118e2081500f19cbb321272d4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-140', 'status', 'backlog', 'done', NULL, '2026-05-22 23:31:46', '2026-05-22 23:31:46', '2026-05-22 23:31:46', NULL, 'e6204c6f0a0dbd38396a69dfef2c7080', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-145', 'description', NULL, 'A compact per-session status strip in the lead Claude pane header AND each teammate tile header (T-140), showing: current model, permission mode (accept-edits / plan / default / bypass), and context-window usage %.
Shared status source (this ticket owns it): extract per-session status in the drift-contained reader/observer (D-75) — do NOT parse CC internals in the widgets.
- Model: team config member.model, overridden by the latest assistant message.model in the transcript when present.
- Permission mode: the transcript''s `permission-mode` records (TranscriptReader currently SKIPS these — surface the latest instead). Also honor config member.planModeRequired for teammates.
- Context %: latest assistant message.usage (input_tokens + cache_read_input_tokens + cache_creation_input_tokens + output_tokens) over the model''s context limit.
Delivery: emit a per-session status to the UI via the bus/events (e.g. a TeamMemberStatus event or an extra MessageBus channel) so BOTH the header strip and the T-141 sidebar render from one source without re-tailing.
Applies to the lead session and every teammate. Re-validate field names against a live team run / CC version bump (undocumented, per T-134).
Acceptance: lead pane header and each teammate tile header show current model + an accept-edits/plan-mode badge + a context-usage indicator, updating live from the transcript. Blocked by T-137 (done), T-140 (done).
', NULL, '2026-05-23 06:07:59', '2026-05-23 06:07:59', '2026-05-23 06:07:59', NULL, '26bb1c89f3fef8d916f3eb6a1ba9d065', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-141', 'description', 'New sidebar panel contributed to Slots.sidebar: roster from ~/.claude/teams config; live status verb / elapsed / down+up tokens / edit-mode from transcript tailing; team token budget pulled from the Anthropic usage API on a timer. Acceptance: roster matches list-panes/config; budget refreshes on the timer; appears in team mode. Blocked by T7.', 'New sidebar panel (contributed to Slots.sidebar), visible only in team mode: a roster row per team member that MIRRORS the per-session status surfaced by T-145, plus team-wide meta.
Per-member row:
- Identity from the team config + observer: name, agentType, color; membership/lifecycle from TeamMemberJoined/Left.
- Mirror of T-145''s shared status: current model, permission-mode badge (accept-edits / plan / default / bypass), context-window usage %. Consume the SAME status source/event T-145 emits — do NOT re-tail transcripts or re-parse CC internals here (D-75).
- Live status verb + elapsed-since-last-activity + down/up token deltas from that same status feed.
Team-wide:
- Team/account token budget pulled from the Anthropic usage API on a timer (distinct from per-conversation context above).
Acceptance: roster matches list-panes/config; each row mirrors the member''s model + accept-edits/plan badge + context-usage; status verb and token deltas update live; account budget refreshes on its timer; panel appears only when a team is active. Depends on T-145 (status source) + T-139 (done).
', NULL, '2026-05-23 06:08:24', '2026-05-23 06:08:24', '2026-05-23 06:08:24', NULL, 'cfa1ce349be286d89a1f8e169605f04d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'description', NULL, 'Regression from T-137: every Claude pane renders the newest .jsonl in the workspace''s munged dir, so concurrent sessions in one workspace (primary + secondaries) all converge on the same transcript — a secondary tab shows the primary''s conversation (and the primary follows whichever session is newest). The pre-T-137 terminal pane rendered each tab''s own PTY, so tabs were independent.
Fix: bind each pane to a specific Claude session id.
- claude CLI supports `--session-id <uuid>` (verified via --help). Spawn each pane''s claude with its own UUID; a transcript is named <session-id>.jsonl, so the pane''s transcript is <munged-cwd>/<uuid>.jsonl.
- Reader targets that exact file via TranscriptReader''s `file:` param (added in T-139). Guard _tail against the file not existing yet (claude writes it shortly after spawn).
- Each pane publishes/subscribes on a per-session MessageBus channel (conversation/<uuid>) so primary and secondary controllers don''t cross-talk.
- UUID per pane, seeded by the tmux session name (the identity we already have): primary = deterministic from its stable session name (clide-claude-<slug>) so it RESUMES across restarts (D-41 continuity); secondary = random per spawn so each is a clean/fresh session (a deterministic secondary id would make starting a clean session impossible).
Migration caveat: an existing tmux session created by the OLD code (no --session-id, claude picked its own id) won''t match the new deterministic id — it must be killed (claude.kill-all-sessions) once so the next spawn binds the controlled id.
Open question to validate live: whether `claude --session-id <existing-uuid>` resumes vs. errors when re-spawned after the tmux session was killed (display works either way since the file is <uuid>.jsonl, but worth confirming claude starts).
Acceptance: a secondary tab shows its OWN fresh conversation, independent of the primary; the primary keeps its conversation; both update live. Regression of T-137; relates to D-41.
', NULL, '2026-05-23 06:19:51', '2026-05-23 06:19:51', '2026-05-23 06:19:51', NULL, '9e46b8eec7cc5e42132249a81d4925d1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 06:19:51', '2026-05-23 06:19:51', '2026-05-23 06:19:51', NULL, 'dd4ec6cfbbc3118cd4cf02604b91108b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'in_progress', 'done', NULL, '2026-05-23 06:30:47', '2026-05-23 06:30:47', '2026-05-23 06:30:47', NULL, '93f9c6814b9e82e5b60ac0e57fff5afe', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, 'b0bc1949f98ef05a05717cef80ba194f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'status', 'in_progress', 'done', NULL, '2026-05-23 07:06:54', '2026-05-23 07:06:54', '2026-05-23 07:06:54', NULL, 'b6b88954fdc04ecf7847dea204a92e14', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-149', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:14:36', '2026-05-23 07:14:36', '2026-05-23 07:14:36', NULL, '75bb1fa2d964f284910201b1c15995b9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-149', 'status', 'in_progress', 'done', NULL, '2026-05-23 07:27:32', '2026-05-23 07:27:32', '2026-05-23 07:27:32', NULL, 'd020ddfea99355676800af1e39249c85', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-145', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 08:03:56', '2026-05-23 08:03:56', '2026-05-23 08:03:56', NULL, 'e50db235944f48a5f581ae64a3589385', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-145', 'status', 'in_progress', 'done', NULL, '2026-05-23 08:43:19', '2026-05-23 08:43:19', '2026-05-23 08:43:19', NULL, 'c9cf511becf7e542d465701e51dba4e2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-150', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 09:04:54', '2026-05-23 09:04:54', '2026-05-23 09:04:54', NULL, 'fcc43ef1e1ae3c1dfa0e9c94ffa35bed', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-150', 'status', 'in_progress', 'done', NULL, '2026-05-23 09:33:03', '2026-05-23 09:33:03', '2026-05-23 09:33:03', NULL, '58c9b0734a70fe8c7083c3cc456a1bd6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-151', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 09:54:31', '2026-05-23 09:54:31', '2026-05-23 09:54:31', NULL, '52df08bff6479b6a84c128f5a63ea312', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-151', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:20:12', '2026-05-23 10:20:12', '2026-05-23 10:20:12', NULL, 'ed8ce28a1b0c7e995887dcfd32456aec', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-153', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 10:31:31', '2026-05-23 10:31:31', '2026-05-23 10:31:31', NULL, 'd5a1f02df5c8be27b9dc62b1ce277d67', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-153', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:35:56', '2026-05-23 10:35:56', '2026-05-23 10:35:56', NULL, 'ddf23a60cdfbfcd79b5d68af10f37928', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-152', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 10:37:57', '2026-05-23 10:37:57', '2026-05-23 10:37:57', NULL, 'e12bfc4f8896514fb13450e0f83cf6c6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-152', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:43:52', '2026-05-23 10:43:52', '2026-05-23 10:43:52', NULL, 'edab8c3daaabe51797afbe70b5f9bfde', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-156', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 11:30:35', '2026-05-23 11:30:35', '2026-05-23 11:30:35', NULL, '66000ad896a8ccd2ec91d2ae6c539f01', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-156', 'status', 'in_progress', 'done', NULL, '2026-05-23 12:05:19', '2026-05-23 12:05:19', '2026-05-23 12:05:19', NULL, '24db72624201c89a5c0bcb3b11840275', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-154', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:01', '2026-05-23 14:40:01', '2026-05-23 14:40:01', NULL, 'aff6dbb3095efd46cf786a53d8622d42', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-155', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:05', '2026-05-23 14:40:05', '2026-05-23 14:40:05', NULL, 'e46f5eba5bd678190954744c2f8338bc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-154', 'status', 'in_progress', 'done', NULL, '2026-05-23 14:50:17', '2026-05-23 14:50:17', '2026-05-23 14:50:17', NULL, '121736159d2d58380447b6a2803cb80b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-155', 'status', 'in_progress', 'done', NULL, '2026-05-23 14:50:17', '2026-05-23 14:50:17', '2026-05-23 14:50:17', NULL, 'b471e923f78ed91f05ca6f2dcbd5c85c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-148', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:51:55', '2026-05-23 14:51:55', '2026-05-23 14:51:55', NULL, '3002f91cea5919aaefc3e4bc530c7ac2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-141', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:51:55', '2026-05-23 14:51:55', '2026-05-23 14:51:55', NULL, '76975080bd78de4d6f69937fd256d4ef', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-148', 'status', 'in_progress', 'done', NULL, '2026-05-23 15:05:29', '2026-05-23 15:05:29', '2026-05-23 15:05:29', NULL, 'd9d9e4c96d94060f929c7c09935d127c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-141', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:49:44', '2026-05-23 20:49:44', '2026-05-23 20:49:44', NULL, 'fb2405478b0d422d39a924ccedec5022', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 20:52:47', '2026-05-23 20:52:47', '2026-05-23 20:52:47', NULL, 'b2b31ab5fe70fb5027773775b0945bd5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-157', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:57:07', '2026-05-23 20:57:07', '2026-05-23 20:57:07', NULL, 'ce114090979d31512a9aa09f1c9311a1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-159', 'status', 'backlog', 'done', NULL, '2026-05-24 08:54:30', '2026-05-24 08:54:30', '2026-05-24 08:54:30', NULL, '478956f24d051f603aab2c7ceec328b2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-161', 'status', 'backlog', 'done', NULL, '2026-05-24 09:45:07', '2026-05-24 09:45:07', '2026-05-24 09:45:07', NULL, 'd7b99c3dff59fc5a70a7866567fbfe0f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-165', 'status', 'backlog', 'in_progress', NULL, '2026-05-24 16:32:59', '2026-05-24 16:32:59', '2026-05-24 16:32:59', NULL, '1dbcae2366b429ad26e65f1a9cfebc33', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-173', 'status', 'backlog', 'in_progress', NULL, '2026-05-24 17:00:14', '2026-05-24 17:00:14', '2026-05-24 17:00:14', NULL, 'c0306e7ca9f755e69c76cdba93bca934', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-173', 'status', 'in_progress', 'done', NULL, '2026-05-24 17:10:07', '2026-05-24 17:10:07', '2026-05-24 17:10:07', NULL, 'f5831d8bfa3e484546cd5dd0c75bbf22', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-166', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 06:23:55', '2026-05-25 06:23:55', '2026-05-25 06:23:55', NULL, '36cc84f2de6519355a55014b107ecea8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-165', 'status', 'in_progress', 'done', NULL, '2026-05-25 06:29:25', '2026-05-25 06:29:25', '2026-05-25 06:29:25', NULL, '34961afff4d3384bced64de93c977460', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-166', 'status', 'in_progress', 'done', NULL, '2026-05-25 06:31:08', '2026-05-25 06:31:08', '2026-05-25 06:31:08', NULL, '3d6b8f30317a82d0aa810b6da47896be', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-176', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '7be78982c929ee18d8665a8e662c380d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-177', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '855a73a398eb53240d12729ac47e0051', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-175', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '8e9a19896103139fe8e43aa4b7212768', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-175', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:25:02', '2026-05-25 07:25:02', '2026-05-25 07:25:02', NULL, '3e63273af832240b9f782fc41ca0047d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-177', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:25:02', '2026-05-25 07:25:02', '2026-05-25 07:25:02', NULL, '73982bc82e335f2d0fd80633aef0c082', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-176', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:27:57', '2026-05-25 07:27:57', '2026-05-25 07:27:57', NULL, '926d5d2694207c65d0f620b2834962aa', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-179', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 08:10:25', '2026-05-25 08:10:25', '2026-05-25 08:10:25', NULL, '059ff72ce3229f45259b097721bfb08f', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-178', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 08:10:25', '2026-05-25 08:10:25', '2026-05-25 08:10:25', NULL, '389b95ddaaef1a5ce318bda840b1cde5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-178', 'status', 'in_progress', 'done', NULL, '2026-05-25 08:21:46', '2026-05-25 08:21:46', '2026-05-25 08:21:46', NULL, 'ddaad91987208966c2720dee370738ed', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-179', 'status', 'in_progress', 'done', NULL, '2026-05-25 08:26:21', '2026-05-25 08:26:21', '2026-05-25 08:26:21', NULL, '58dc0afa577b9aee651d683aa1b9bc69', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-183', 'description', 'A comprehensive read view (with edit affordances where sensible) of the Claude environment, as the Config sub-tab of the sidebar (T-182). Sources: ClaudeConfig (D-76 / T-151, already reads skills, commands, settings, permissions from ~/.claude + .claude layered) plus the stream-json init/initialize payloads (agents, output styles + available styles, MCP servers, models, plugins). Sections: SKILLS, AGENTS, COMMANDS, PERMISSIONS (allow/ask/deny, colour-coded), HOOKS, MCP SERVERS, SETTINGS (model, output style). Show local+global layering (local wins) and that it is watched/live. Covers the interaction-type surfaces documented on anthropic.com so the user can see everything configured for Claude in one place. Acceptance: each section lists its entries from ClaudeConfig + init data; permissions colour-coded by rule kind; layering + source shown; updates live on a watched .claude change; widget tests. Wireframe: docs/design/wireframes/claude-prompts/09-sidebar-config-tab.png. Blocked by T-182 (sub-tab shell).', 'The Config sub-tab (T-182): a browsable view of the Claude environment over ClaudeConfig (D-76 / T-151 — skills, commands, settings, permissions from ~/.claude + .claude, layered) plus the stream-json init/initialize payloads (agents, output styles, MCP servers, models, plugins). Each category is an EXPANDABLE accordion section (no ellipsis truncation): SKILLS, AGENTS, COMMANDS, HOOKS, PERMISSIONS, MCP SERVERS, SETTINGS, each with a count. File-backed entries are CLICKABLE: a skill (SKILL.md), agent (.claude/agents/*.md), or command (.claude/commands/*.md) — clicking fires an open-file action into the right-side markdown reader/editor pane (reusing the existing file-open plumbing, D-6 parity) to show what that skill/agent/command says. Non-file entries render inline: permissions (allow/ask/deny, colour-coded), MCP servers, model, output style; SETTINGS can open settings.json. Show local+global layering (local wins) and that it is watched/live. Acceptance: sections expand/collapse to the full list; clicking a file-backed item opens its .md in the reader; permissions colour-coded by rule kind; updates live on a watched .claude change; widget tests for expand + click-to-open. Wireframe: docs/design/wireframes/claude-prompts/09-sidebar-config-tab.png. Blocked by T-182.', NULL, '2026-05-25 09:31:37', '2026-05-25 09:31:37', '2026-05-25 09:31:37', NULL, '473e88ee9fa895f5dc30fcac5b001fa6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-183', 'description', 'The Config sub-tab (T-182): a browsable view of the Claude environment over ClaudeConfig (D-76 / T-151 — skills, commands, settings, permissions from ~/.claude + .claude, layered) plus the stream-json init/initialize payloads (agents, output styles, MCP servers, models, plugins). Each category is an EXPANDABLE accordion section (no ellipsis truncation): SKILLS, AGENTS, COMMANDS, HOOKS, PERMISSIONS, MCP SERVERS, SETTINGS, each with a count. File-backed entries are CLICKABLE: a skill (SKILL.md), agent (.claude/agents/*.md), or command (.claude/commands/*.md) — clicking fires an open-file action into the right-side markdown reader/editor pane (reusing the existing file-open plumbing, D-6 parity) to show what that skill/agent/command says. Non-file entries render inline: permissions (allow/ask/deny, colour-coded), MCP servers, model, output style; SETTINGS can open settings.json. Show local+global layering (local wins) and that it is watched/live. Acceptance: sections expand/collapse to the full list; clicking a file-backed item opens its .md in the reader; permissions colour-coded by rule kind; updates live on a watched .claude change; widget tests for expand + click-to-open. Wireframe: docs/design/wireframes/claude-prompts/09-sidebar-config-tab.png. Blocked by T-182.', 'The Config sub-tab (T-182): a browsable view of the Claude environment over ClaudeConfig (D-76 / T-151 — skills, commands, settings, permissions from ~/.claude + .claude, layered, local wins) plus the stream-json init/initialize payloads (agents, output styles, MCP servers, models, plugins). Layout: SETTINGS is a FIXED key->value table pinned at the top (not collapsible) — model, output style, permission mode, source. Everything variable-length is an EXPANDABLE accordion section showing the FULL list when expanded, never truncated (truncating to first-N + ellipsis falsely prioritises the first entries and makes the list pointless): SKILLS, AGENTS, COMMANDS, HOOKS, PERMISSIONS (grouped allow/ask/deny, colour-coded, each rule on its own line), MCP SERVERS — each with a count. File-backed entries are CLICKABLE: a skill (SKILL.md), agent (.claude/agents/*.md), or command (.claude/commands/*.md) — clicking fires an open-file action into the right-side markdown reader/editor pane (reusing the existing file-open plumbing, D-6 parity) to show what it says. Watched/live on .claude changes. Acceptance: settings render as a top table; every variable section expands to its complete list (no ellipsis); clicking a file-backed item opens its .md in the reader; permissions colour-coded by kind; widget tests for table + expand + click-to-open. Wireframe: docs/design/wireframes/claude-prompts/09-sidebar-config-tab.png. Blocked by T-182.', NULL, '2026-05-25 09:35:47', '2026-05-25 09:35:47', '2026-05-25 09:35:47', NULL, '33086bf1d766da9bb8e12ddeeaa8ed92', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-182', 'description', 'The Claude meta-sidebar is overloaded (roster + tasks + messages + stats + config do not fit one scroll). Split it into a sub-tab strip (reuse the search-panel mode-strip pattern) with three tabs: (1) Activity — stats + live runtime visibility (T-141/T-157); (2) Team — roster + tasks + message-feed widget (T-171 + T-180), mostly hidden when solo and auto-fronted when a team is live; (3) Config — the Claude-environment view. Default to Activity. Acceptance: a sub-tab strip switches the sidebar body between the three surfaces; existing stats/team content slots into Activity/Team unchanged; Team auto-fronts on team spawn; widget tests for tab switching. Wireframes: docs/design/wireframes/claude-prompts/07-sidebar-activity-tab.png, 08-sidebar-team-tab.png, 09-sidebar-config-tab.png. Relates to T-141, T-157, T-171, T-180.', 'The Claude meta-sidebar is overloaded (roster + tasks + messages + stats + config do not fit one scroll). Split it into a sub-tab strip (reuse the search-panel mode-strip pattern) with three tabs: (1) Activity — stats + live runtime visibility (T-141/T-157); (2) Team — roster + tasks + message-feed widget (T-171 + T-180), mostly hidden when solo and auto-fronted when a team is live; (3) Config — the Claude-environment view (T-183). Default to Activity. SHARED GEOMETRY: Activity values and the Config settings table use the SAME two-column label->value table (same column x + row pitch + section-header style) so toggling between tabs does not visually jump. Acceptance: a sub-tab strip switches the sidebar body between the three surfaces; Activity + Config settings render on the shared table so toggling is seamless; Team auto-fronts on team spawn; widget tests for tab switching. Wireframes: docs/design/wireframes/claude-prompts/07-sidebar-activity-tab.png, 08-sidebar-team-tab.png, 09-sidebar-config-tab.png. Relates to T-141, T-157, T-171, T-180.', NULL, '2026-05-25 09:38:15', '2026-05-25 09:38:15', '2026-05-25 09:38:15', NULL, 'a41f10a11412d0f7ee0cd5e0e7700488', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-169', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 10:49:30', '2026-05-25 10:49:30', '2026-05-25 10:49:30', NULL, '3269bcb75eb13dffc13be53c9f71ca73', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-169', 'status', 'in_progress', 'done', NULL, '2026-05-25 12:12:29', '2026-05-25 12:12:29', '2026-05-25 12:12:29', NULL, 'c93755e2b277ae1b120a58fe3ea59246', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-170', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 12:12:33', '2026-05-25 12:12:33', '2026-05-25 12:12:33', NULL, '494120eadfe8d6ba67c065fa63180cda', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-170', 'status', 'in_progress', 'done', NULL, '2026-05-25 19:38:16', '2026-05-25 19:38:16', '2026-05-25 19:38:16', NULL, '6a30f33668d2d24a78badd8fe680e724', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-182', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 20:01:12', '2026-05-25 20:01:12', '2026-05-25 20:01:12', NULL, 'b0f1c94a5b064c098881defb61d27ad4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-168', 'description', 'Exploit the structured stream the transcript-tail never cleanly gave. Per-tool cards from tool_use input (Edit/Write → diff, Bash → command+output, Read/Grep → file/query); pair each tool_result to its tool_use (with is_error) so diffs/failures render in place; stream assistant text + thinking blocks token-by-token via --include-partial-messages; surface live model / permission-mode / context-tokens / cost and rate_limit_event straight off the init + result events (retires the ClaudeConfig probe + status-bar guessing for the active pane); render structured errors as events. Acceptance: a session with edits/bash/reads shows the right card per tool with paired results; replies stream; the status line reflects live model/mode/context/cost from events; widget + fixture tests. Blocked by T-165.', 'Exploit the structured stream the transcript-tail never cleanly gave. Per-tool cards from tool_use input (Edit/Write → diff, Bash → command+output, Read/Grep → file/query); pair each tool_result to its tool_use (with is_error) so diffs/failures render in place; stream assistant text + thinking blocks token-by-token via --include-partial-messages; surface live model / permission-mode / context-tokens / cost and rate_limit_event straight off the init + result events (retires the ClaudeConfig probe + status-bar guessing for the active pane); render structured errors as events. Acceptance: a session with edits/bash/reads shows the right card per tool with paired results; replies stream; the status line reflects live model/mode/context/cost from events; widget + fixture tests. Blocked by T-165.
---
Refinement (2026-05-30, /whats-next) — five seam decisions resolved:
1. tool_use/result pairing: add a Map<String, AssistantToolUse> toolUseById index to ConversationController. The result card looks up its tool_use and renders the Edit/Write diff (reuse prompt_card''s _editBody style) or the is_error failure IN PLACE in the result card. Files: conversation_controller.dart (index), conversation_view.dart (_toolUse / _toolResult bodies — they currently render indented JSON only and can''t see each other).
2. Per-tool card bodies: Edit/Write → diff; Bash → command+output; Read/Grep → file/query. Selected by tool name off the paired tool_use input.
3. Partial-message streaming: pass --include-partial-messages in ClaudeStreamJsonProcess.start(); accumulate partials keyed by message.id in StreamJsonSession._onLine BEFORE emitting to the controller, so one item updates in place (no duplicate AssistantTextMessage items). Thinking blocks stream the same way. Do NOT add a new streaming item type in the controller.
4. Live cost/context (was an open gap, defaulted): extend SessionStatus (transcript_reader.dart) with cost + contextWindow fields; extract them from result events in stream_json_session.dart::_statusFromEvent (currently only handles system/init) — total_cost_usd + modelUsage.<model>.contextWindow per the spike doc docs/spikes/cc-stream-json-control-protocol-2.1.150.md.
5. rate_limit_event + the focus status line: surface live model/mode/context/cost/rate-limit in the in-pane status slot. This DEPENDS ON T-160 (status-bar overflow / marquee) which is pulled into this batch and sequenced FIRST — do not lengthen the status line until T-160''s Flexible/marquee fix lands. ClaudeConfig.ensureProbe() is retired only for the ACTIVE-pane status; the probe survives for slash-command typeahead.
Files an implementer touches: conversation_view.dart, conversation_controller.dart, transcript_reader.dart, stream_json_session.dart, claude_pane.dart, plus test/builtin/claude/*. Commit the in-flight prompt_card.dart change first (disjoint surface — permission-card Write-description dedup).
Sequencing within the batch: T-160 → T-168.', NULL, '2026-05-30 10:44:06', '2026-05-30 10:44:06', '2026-05-30 10:44:06', NULL, 'f5ea965167b5b95bbe5f24a445e05cc9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-160', 'description', 'Observed via make run (2026-05-24): the bottom status bar shows a RenderFlex ''overflowed by 77 PIXELS'' stripe after the in-pane context line (opus 4.7 · default · 21k ctx · 10 skills); the ClideMarquee never engages.
Cause: StatusbarHost (lib/app.dart ~1143) lays items in a Row [left items..., Spacer(), right items...], each at INTRINSIC width with no Flexible. PaneContextStatusItem wraps its text in ConstrainedBox(maxWidth:360) + ClideMarquee, but because the text (~280px) is under 360 the slot takes content width, so the marquee''s viewport == its content and it never scrolls. T-154 appended ''· N skills'', lengthening the line enough that git-branch + context slot + right items exceed the bar width -> the Row overflows (~77px) instead of the slot shrinking + marquee scrolling.
Fix direction: give the in-pane context slot a flexible/bounded width in the status bar (e.g. wrap it in Flexible(fit: loose), or have the bar allot it a shrinkable share) so it yields width when the bar is tight and ClideMarquee scrolls within the allotted width. Verify the interaction with the Spacer and the right-aligned items (priority>=100), and reconsider the fixed maxWidth:360. Acceptance: at narrow widths the in-pane slot marquee-scrolls and the status bar never shows an overflow stripe; widget test at a constrained width. Introduced by T-150 (marquee) + T-154 (longer line).', 'Observed via make run (2026-05-24): the bottom status bar shows a RenderFlex ''overflowed by 77 PIXELS'' stripe after the in-pane context line (opus 4.7 · default · 21k ctx · 10 skills); the ClideMarquee never engages.
Cause: StatusbarHost (lib/app.dart ~1143) lays items in a Row [left items..., Spacer(), right items...], each at INTRINSIC width with no Flexible. PaneContextStatusItem wraps its text in ConstrainedBox(maxWidth:360) + ClideMarquee, but because the text (~280px) is under 360 the slot takes content width, so the marquee''s viewport == its content and it never scrolls. T-154 appended ''· N skills'', lengthening the line enough that git-branch + context slot + right items exceed the bar width -> the Row overflows (~77px) instead of the slot shrinking + marquee scrolling.
Fix direction: give the in-pane context slot a flexible/bounded width in the status bar (e.g. wrap it in Flexible(fit: loose), or have the bar allot it a shrinkable share) so it yields width when the bar is tight and ClideMarquee scrolls within the allotted width. Verify the interaction with the Spacer and the right-aligned items (priority>=100), and reconsider the fixed maxWidth:360. Acceptance: at narrow widths the in-pane slot marquee-scrolls and the status bar never shows an overflow stripe; widget test at a constrained width. Introduced by T-150 (marquee) + T-154 (longer line).
---
Refinement (2026-05-30, /whats-next): pulled into the T-167/T-168 batch and sequenced FIRST. T-168 adds live cost / permission-mode / context / rate-limit to this same in-pane status slot; the Flexible/marquee fix here must land before T-168 lengthens the line. After this fix, T-168 can safely add fields to the slot.', NULL, '2026-05-30 10:44:10', '2026-05-30 10:44:10', '2026-05-30 10:44:10', NULL, '4ad98d4a7db42e295fba6dff0988ad3b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-167', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, '229c9cb92eb8554dbba51ca65e1eeba9', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-168', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, '4469254d98eba45964ca51d5855e5014', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-160', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 10:48:32', '2026-05-30 10:48:32', '2026-05-30 10:48:32', NULL, 'e410b6f78d0e4d96bc37f3a351d72767', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-160', 'status', 'in_progress', 'done', NULL, '2026-05-30 11:05:56', '2026-05-30 11:05:56', '2026-05-30 11:05:56', NULL, '0024103be5bf0ab0653b9dbe03c52f71', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-167', 'status', 'in_progress', 'done', NULL, '2026-05-30 11:05:56', '2026-05-30 11:05:56', '2026-05-30 11:05:56', NULL, 'fee8b67a0ad9c7c1c7ec7d9f75f42bfc', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-168', 'status', 'in_progress', 'done', NULL, '2026-05-30 11:44:28', '2026-05-30 11:44:28', '2026-05-30 11:44:28', NULL, '152c34a28e7005386a4a6829d942e569', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-172', 'description', 'Branch a conversation into a new managed session/pane, reusing the orchestrator''s spawn plumbing. ''Fork'' = spawn a managed session seeded from another session''s context via --resume + --fork-session (resume the source, but create a new session-id so the branch diverges without touching the original). Surface it as a slash action and a sidebar/roster button on a session. Acceptance: forking an active session opens a new pane continuing from its context, independent thereafter; the original is unaffected; tests for the fork argv + the new managed session appearing in the roster. Blocked by T-169 (orchestrator).', 'Branch a conversation into a new managed session/pane, reusing the orchestrator''s spawn plumbing. ''Fork'' = spawn a managed session seeded from another session''s context via --resume + --fork-session (resume the source, but create a new session-id so the branch diverges without touching the original). Surface it as a slash action and a sidebar/roster button on a session. Acceptance: forking an active session opens a new pane continuing from its context, independent thereafter; the original is unaffected; tests for the fork argv + the new managed session appearing in the roster. Blocked by T-169 (orchestrator).
---
Refinement (2026-05-30, /whats-next): --fork-session CONFIRMED against the live binary (claude --help: "When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)"). Fork argv = [''--resume'', <sourceSessionId>, ''--fork-session''] (no --session-id; the new branch gets its own id from the init event). Add forkSourceSessionId to SpawnSpec (session_orchestrator.dart); build argv via a forkSessionArgs helper in session_naming.dart; add ''fork'' to kClideOwnedCommands + a case in claude_pane._send; add a fork button to _memberRow in claude_meta_sidebar.dart. SEQUENCE AFTER T-171 (shares _memberRow + orchestrator).', NULL, '2026-05-30 17:12:17', '2026-05-30 17:12:17', '2026-05-30 17:12:17', NULL, '901eb461070872a8dcb61f21546382d6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-171', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 17:12:46', '2026-05-30 17:12:46', '2026-05-30 17:12:46', NULL, '04314327ba504ef372a338ed737650a8', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-174', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 17:12:46', '2026-05-30 17:12:46', '2026-05-30 17:12:46', NULL, '940531017064b45bd962a8fe7369ec6d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-171', 'status', 'in_progress', 'done', NULL, '2026-05-30 21:11:26', '2026-05-30 21:11:26', '2026-05-30 21:11:26', NULL, '26cb27e63ad95ae94017ada95daef9d2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-174', 'status', 'in_progress', 'done', NULL, '2026-05-30 21:11:26', '2026-05-30 21:11:26', '2026-05-30 21:11:26', NULL, '4df20d29d49e7dd95dd9caafbe956cb2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 21:14:56', '2026-05-30 21:14:56', '2026-05-30 21:14:56', NULL, '6426dc0355b68f0fd572e52d53101fc4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'status', 'in_progress', 'done', NULL, '2026-05-30 21:25:11', '2026-05-30 21:25:11', '2026-05-30 21:25:11', NULL, '81d8d388c7e4f53a4514f05cf5dbf37b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-172', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 21:25:12', '2026-05-30 21:25:12', '2026-05-30 21:25:12', NULL, 'e42556ec9d67731533b85e6da2a1bc9c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-172', 'status', 'in_progress', 'done', NULL, '2026-05-30 21:34:56', '2026-05-30 21:34:56', '2026-05-30 21:34:56', NULL, 'f97c3bb01ea2bc5f969b34d4de09617a', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-180', 'status', 'backlog', 'in_progress', NULL, '2026-05-30 21:34:57', '2026-05-30 21:34:57', '2026-05-30 21:34:57', NULL, 'd73e085a28c3c4b2a7ce515d33012de4', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-180', 'status', 'in_progress', 'done', NULL, '2026-05-31 08:49:07', '2026-05-31 08:49:07', '2026-05-31 08:49:07', NULL, '7a3ab42f4a4571c00dbb55b2079e1b9b', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-183', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 08:49:08', '2026-05-31 08:49:08', '2026-05-31 08:49:08', NULL, 'e7e9fd59ba8adf45431b759b693d4022', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-183', 'status', 'in_progress', 'done', NULL, '2026-05-31 09:30:11', '2026-05-31 09:30:11', '2026-05-31 09:30:11', NULL, 'd9c300f2f35571747572037bc8cb6983', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-184', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 09:49:55', '2026-05-31 09:49:55', '2026-05-31 09:49:55', NULL, 'c307a16d7851ec46ee57ff8422380ff7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-184', 'status', 'in_progress', 'done', NULL, '2026-05-31 10:17:09', '2026-05-31 10:17:09', '2026-05-31 10:17:09', NULL, '8d23aadcaeebbf9b08ba0ccf495578bf', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-185', 'status', 'backlog', 'done', NULL, '2026-05-31 10:22:02', '2026-05-31 10:22:02', '2026-05-31 10:22:02', NULL, 'a91a00cea5b083cc7c555aa0662495c2', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-164', 'status', 'backlog', 'done', NULL, '2026-05-31 10:22:03', '2026-05-31 10:22:03', '2026-05-31 10:22:03', NULL, '2738e39138046b8e3704fc64d6f8d787', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-95', 'status', 'backlog', 'done', NULL, '2026-05-31 11:20:21', '2026-05-31 11:20:21', '2026-05-31 11:20:21', NULL, '0236a359632b1fcf101f68a25131abb5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-162', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 11:42:11', '2026-05-31 11:42:11', '2026-05-31 11:42:11', NULL, '39d0ae4510e70f877b5c07417d486ac1', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-187', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 11:42:11', '2026-05-31 11:42:11', '2026-05-31 11:42:11', NULL, 'b4eea5fc17737821e8e9ce3d53ea62ba', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-188', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 11:42:11', '2026-05-31 11:42:11', '2026-05-31 11:42:11', NULL, 'fd62fc2dc8e6278288a7c52ad450ab0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-192', 'description', 'The pre-push gate (make push-check) and the inner-loop make test are slow; cost is dominated by ''flutter test --coverage --exclude-tags pty'' running the whole flutter suite WITH coverage instrumentation (ci/test.sh). Two concrete levers found by reading the pipeline: (1) FREE WIN — a11y tests run TWICE in push-check: the coverage run in ci/test.sh has no path filter so it already includes test/a11y/, then ci/test_a11y.sh (''flutter test test/a11y/'') runs them again in a separate flutter invocation, chained AFTER test (so not even fast-fail). Drop test-a11y from the push-check chain (keep ''make test-a11y'' as a standalone dev target) to remove a whole redundant flutter compile+boot+run; same tests still execute under test. (2) Coverage is on the dev inner loop — make test always runs --coverage, which is only needed where coverage-gate reads lcov (push-check/CI). Split a fast no-coverage ''make test'' for the edit loop from a coverage run used by push-check/CI; the script even claims <60s warm but coverage blows past that. MEASURE FIRST (cheap): time each ci/*.sh step, check ''flutter test --concurrency'' vs nproc, and look at per-file timing for golden/alchemist tests before further changes. Acceptance: dev ''make test'' runs without coverage and is meaningfully faster; push-check no longer double-runs the a11y suite; coverage is still gated (coverage-gate unchanged, floor 95); all gates stay green; the fast-vs-coverage split is documented in the Makefile/CONTRIBUTING. Domain: tooling/CI. Raised 2026-05-31 while the push gate kept interrupting flow.', 'The pre-push gate (make push-check) and the inner-loop make test are slow; cost is dominated by ''flutter test --coverage --exclude-tags pty'' running the whole flutter suite WITH coverage instrumentation (ci/test.sh). Two concrete levers found by reading the pipeline: (1) FREE WIN — a11y tests run TWICE in push-check: the coverage run in ci/test.sh has no path filter so it already includes test/a11y/, then ci/test_a11y.sh (''flutter test test/a11y/'') runs them again in a separate flutter invocation, chained AFTER test (so not even fast-fail). Drop test-a11y from the push-check chain (keep ''make test-a11y'' as a standalone dev target) to remove a whole redundant flutter compile+boot+run; same tests still execute under test. (2) Coverage is on the dev inner loop — make test always runs --coverage, which is only needed where coverage-gate reads lcov (push-check/CI). Split a fast no-coverage ''make test'' for the edit loop from a coverage run used by push-check/CI; the script even claims <60s warm but coverage blows past that. MEASURE FIRST (cheap): time each ci/*.sh step, check ''flutter test --concurrency'' vs nproc, and look at per-file timing for golden/alchemist tests before further changes. Acceptance: dev ''make test'' runs without coverage and is meaningfully faster; push-check no longer double-runs the a11y suite; coverage is still gated (coverage-gate unchanged, floor 95); all gates stay green; the fast-vs-coverage split is documented in the Makefile/CONTRIBUTING. Domain: tooling/CI. Raised 2026-05-31 while the push gate kept interrupting flow.
Add a per-test timeout to the gate''s flutter test (e.g. ci/test.sh: ''flutter test --coverage --timeout 60s''). Motivation discovered 2026-05-31: a hung pumpAndSettle has a 10-MINUTE default timeout, so a few hanging widget tests wedge make push-check for 30+ minutes — which is almost certainly why a MacBook session resorted to ''git push --no-verify'' (the gate never returned; it was not disabled, it was hung). A tight per-test timeout makes any future hang fail fast (60s) instead of wedging a pre-push, so the gate stays usable and nobody is tempted to bypass it. Pair with: keep writing pump()-bounded tests instead of pumpAndSettle() where a view has overlapping async loads (see the T-188 decision_reader_test hang).', NULL, '2026-05-31 12:31:58', '2026-05-31 12:31:58', '2026-05-31 12:31:58', NULL, 'e42f8392c580eee1269d266cb0fb9749', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-187', 'status', 'in_progress', 'done', NULL, '2026-05-31 14:17:22', '2026-05-31 14:17:22', '2026-05-31 14:17:22', NULL, '4bef7314b0b25fbed23934e915a2ce59', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-162', 'status', 'in_progress', 'done', NULL, '2026-05-31 14:17:22', '2026-05-31 14:17:22', '2026-05-31 14:17:22', NULL, '677a9a319e57dffeb7158b17a9a61392', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-188', 'status', 'in_progress', 'done', NULL, '2026-05-31 14:17:22', '2026-05-31 14:17:22', '2026-05-31 14:17:22', NULL, '9e04aeba4367316d03afb0855a306520', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-192', 'description', 'The pre-push gate (make push-check) and the inner-loop make test are slow; cost is dominated by ''flutter test --coverage --exclude-tags pty'' running the whole flutter suite WITH coverage instrumentation (ci/test.sh). Two concrete levers found by reading the pipeline: (1) FREE WIN — a11y tests run TWICE in push-check: the coverage run in ci/test.sh has no path filter so it already includes test/a11y/, then ci/test_a11y.sh (''flutter test test/a11y/'') runs them again in a separate flutter invocation, chained AFTER test (so not even fast-fail). Drop test-a11y from the push-check chain (keep ''make test-a11y'' as a standalone dev target) to remove a whole redundant flutter compile+boot+run; same tests still execute under test. (2) Coverage is on the dev inner loop — make test always runs --coverage, which is only needed where coverage-gate reads lcov (push-check/CI). Split a fast no-coverage ''make test'' for the edit loop from a coverage run used by push-check/CI; the script even claims <60s warm but coverage blows past that. MEASURE FIRST (cheap): time each ci/*.sh step, check ''flutter test --concurrency'' vs nproc, and look at per-file timing for golden/alchemist tests before further changes. Acceptance: dev ''make test'' runs without coverage and is meaningfully faster; push-check no longer double-runs the a11y suite; coverage is still gated (coverage-gate unchanged, floor 95); all gates stay green; the fast-vs-coverage split is documented in the Makefile/CONTRIBUTING. Domain: tooling/CI. Raised 2026-05-31 while the push gate kept interrupting flow.
Add a per-test timeout to the gate''s flutter test (e.g. ci/test.sh: ''flutter test --coverage --timeout 60s''). Motivation discovered 2026-05-31: a hung pumpAndSettle has a 10-MINUTE default timeout, so a few hanging widget tests wedge make push-check for 30+ minutes — which is almost certainly why a MacBook session resorted to ''git push --no-verify'' (the gate never returned; it was not disabled, it was hung). A tight per-test timeout makes any future hang fail fast (60s) instead of wedging a pre-push, so the gate stays usable and nobody is tempted to bypass it. Pair with: keep writing pump()-bounded tests instead of pumpAndSettle() where a view has overlapping async loads (see the T-188 decision_reader_test hang).', 'The pre-push gate (make push-check) and the inner-loop make test are slow; cost is dominated by ''flutter test --coverage --exclude-tags pty'' running the whole flutter suite WITH coverage instrumentation (ci/test.sh). Two concrete levers found by reading the pipeline: (1) FREE WIN — a11y tests run TWICE in push-check: the coverage run in ci/test.sh has no path filter so it already includes test/a11y/, then ci/test_a11y.sh (''flutter test test/a11y/'') runs them again in a separate flutter invocation, chained AFTER test (so not even fast-fail). Drop test-a11y from the push-check chain (keep ''make test-a11y'' as a standalone dev target) to remove a whole redundant flutter compile+boot+run; same tests still execute under test. (2) Coverage is on the dev inner loop — make test always runs --coverage, which is only needed where coverage-gate reads lcov (push-check/CI). Split a fast no-coverage ''make test'' for the edit loop from a coverage run used by push-check/CI; the script even claims <60s warm but coverage blows past that. MEASURE FIRST (cheap): time each ci/*.sh step, check ''flutter test --concurrency'' vs nproc, and look at per-file timing for golden/alchemist tests before further changes. Acceptance: dev ''make test'' runs without coverage and is meaningfully faster; push-check no longer double-runs the a11y suite; coverage is still gated (coverage-gate unchanged, floor 95); all gates stay green; the fast-vs-coverage split is documented in the Makefile/CONTRIBUTING. Domain: tooling/CI. Raised 2026-05-31 while the push gate kept interrupting flow.
Add a per-test timeout to the gate''s flutter test (e.g. ci/test.sh: ''flutter test --coverage --timeout 60s''). Motivation discovered 2026-05-31: a hung pumpAndSettle has a 10-MINUTE default timeout, so a few hanging widget tests wedge make push-check for 30+ minutes — which is almost certainly why a MacBook session resorted to ''git push --no-verify'' (the gate never returned; it was not disabled, it was hung). A tight per-test timeout makes any future hang fail fast (60s) instead of wedging a pre-push, so the gate stays usable and nobody is tempted to bypass it. Pair with: keep writing pump()-bounded tests instead of pumpAndSettle() where a view has overlapping async loads (see the T-188 decision_reader_test hang).
Measured 2026-05-31 (16-core box, warm cache, full flutter suite --exclude-tags pty): coverage default-concurrency = ~36s; coverage --concurrency=12 = ~37s (NO improvement — coverage runs are concurrency-insensitive, the instrumentation/collection dominates); NO-coverage --concurrency=12 = ~21s. Conclusions: (1) Concurrency only helps the NON-coverage path — do NOT bother adding --concurrency to the coverage gate, it buys nothing. (2) The dev inner-loop win is concrete: a no-coverage ''make test'' with --concurrency=12 runs ~21s vs ~36s today (~40% faster) — this is how we ''beat 40s'' for the edit loop. (3) The push-check coverage run is floored at ~36s by coverage itself; getting below that needs a different coverage approach (package:coverage via VM service, or coverage-on-changed-files) — out of scope for the quick win. So the actionable set is unchanged: no-coverage+concurrency dev target, coverage-only-in-push, drop the redundant a11y pass, add a per-test --timeout for hang-safety.', NULL, '2026-05-31 14:46:39', '2026-05-31 14:46:39', '2026-05-31 14:46:39', NULL, 'b6e7ea80f4abe35cbfc4ba9f2ff80e06', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-189', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 16:13:39', '2026-05-31 16:13:39', '2026-05-31 16:13:39', NULL, '51dba55aeb4ac02a4a4d7e72f09b3e3e', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-191', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 16:13:39', '2026-05-31 16:13:39', '2026-05-31 16:13:39', NULL, '76e72b3d04c0456e8a39d8ca7dba7644', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-190', 'status', 'backlog', 'in_progress', NULL, '2026-05-31 16:13:39', '2026-05-31 16:13:39', '2026-05-31 16:13:39', NULL, 'cc7072933e46f402bf65c02595843185', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-186', 'status', 'backlog', 'done', NULL, '2026-05-31 16:40:19', '2026-05-31 16:40:19', '2026-05-31 16:40:19', NULL, '8f5241e0d76f6cef282f5a04d274345a', 1) ON CONFLICT(hash) DO NOTHING;