default.yaml bound tab/shift+tab to focus.next/focus.previous, intent
ids absent from builtinIntents. parseIntentId returns null, so
KeymapLayer.fromYaml throws — and KeymapService.load catches that and
sets _preset = null. The whole default preset was silently dropped at
boot: palette, quick-open, find-in-files, and zoom bindings never fired.
It went unnoticed because every keymap_service_test injects a synthetic
bundle; the shipped asset was never parsed in a test. Add focus.next ->
NextFocusIntent and focus.previous -> PreviousFocusIntent (Flutter-
provided, for correct Tab widget traversal), and a test that reads every
real assets/keymaps/*.yaml through the loader so a future typo fails CI
instead of disabling the keymap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that the editor split actually opens (T-197), it exposed latent
issues, plus a coincidental Claude-session crash in the same log:
- T-203: the _EditorDragHandle's slider Semantics had value +
onIncrease/onDecrease but no increased/decreasedValue, so Flutter
asserted on every semantics flush — add them. And opening the split
reparented the Claude pane (direct child → Column/Expanded), tearing
down its SelectableRegion mid selection-update ('selectable not in
this registrar' / 'inactive element'); a stable GlobalKey on the
workspace primary makes Flutter move the element instead.
- T-202: rate_limit_event.resetsAt arrives as a unix-epoch number but
was cast `as String?`, throwing in the stream-json line parser. Accept
a num (epoch) or an ISO string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first T-197 fix flipped the wrong lever: it called
activateTab(Slots.workspace, 'editor.active'), but _WorkspaceSlot
renders its editor split off arrangement.editorOpen — not the active
tab — so clicking a file still showed nothing. Call arrangement
.openEditor() on editor.opened / active-changed(non-null), and
closeEditor() on active-changed(null) so the split collapses when the
last buffer closes. Test now asserts arrangement.editorOpen, the lever
the UI actually reads.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Search tab's Find mode stacked four ClideFilterBoxes (search,
replace, include, exclude) that all looked identical: every box drew the
magnifying glass and the hint was only a semantics label, never visible
text — so they read as four blank search boxes. Render the hint as
placeholder text while empty, and make the leading icon optional (the
replace + glob fields pass icon: null). General win — every filter box
now shows its placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pql sidebar panel and the find-in-files tab were duplicate search
surfaces. Consolidate into one Search tab with a mode switch: Find
(content grep), Vault (pql ranked search), Query (PQL DSL), Markdown
(the synced markdown-file listing, keeping focus-highlight + live
refresh). SearchPanelView holds both FindInFilesController and
PqlController; the pql body + result rows move into a reusable
PqlSearchBody. The standalone builtin.pql sidebar tab is removed (one
fewer tab — eases the rail); the pql extension keeps the Backlinks
context panel. No D-79 conflict — grep vs ranked search remain distinct
backends, this is UI consolidation.
Adds the pql builtin's first widget/controller tests (it was untested,
so folding it into the tested Search panel required covering the
Vault/Query/Markdown modes + PqlController).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rail was a fixed Row(center, max) — one button per tab — so adding
the Search tab pushed it 54px past its width and threw a RenderFlex
overflow. Center the icons when they fit and scroll horizontally when
they don't (LayoutBuilder + SingleChildScrollView + a minWidth floor),
so the rail stays correct at any tab count. (T-200)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reworded entry exceeded the 60-word changelog-gate limit; tighten
it. No code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pin/unpin toggle is a mode control, not navigation — grouping it
with back/forward/jump implied they work alike. Pull it out of
ReaderActionBar into a standalone ReaderPinButton placed before the
title (ClidePaneChrome.leading), leaving the right-hand navigator to
back/forward/jump-to-pin/edit. Applies to all three readers. (T-198)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the tickets detail in line with markdown/decisions (D-81). The
controller loads on 'load' (the channel the retained ReaderNav emits),
the extension reveals the static tickets.detail tab on selection instead
of the per-click uncontribute/contribute churn (the T-188 anti-pattern),
and the view grabs nav.current on mount and wraps in ClidePaneChrome
with a ReaderActionBar — pin toggle left, back/forward + jump-to-pin
right, no edit pencil (tickets are pql records, not files). The
controller drops its now-unused panels dependency.
Also adds the tickets builtin's first tests — the sidebar list
(load/sections/filter/select/empty/error/refresh) and the detail reader
(load, nav, pin, parents/decisions/status) — covering a pre-existing gap
exposed by bringing these files under test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per user feedback on the reader action bar: use the push-pin glyph (not
the chain/link), make the pin button toggle the pinned state (tap to pin
current, tap again to unpin) via ReaderNav.togglePin, and split the
layout so the pin/unpin toggle sits on the left while jump-to-pin joins
the navigator (back/forward) on the right — left toggles, right
navigates. The action button gains an active (accent) state for the
pinned indicator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two reveal-on-open bugs:
Decisions opened only on the second click (T-196): the detail view
subscribed in didChangeDependencies, which runs after the tab is
revealed, so the broadcast 'selection' that triggered the reveal was
already gone. Hoist the back/forward history out of per-view State into
a retained per-reader ReaderNav (kernel ChangeNotifier in a
ReaderNavRegistry, D-81). The nav records selections, emits 'load' (the
single channel readers display from), and survives mount/unmount — the
reader grabs nav.current on mount, so the first selection lands. Both
the markdown and decisions readers move to this model; the per-view
ReaderHistoryMixin and the markdown post-frame forward hack are gone.
The editor pane never opened (T-197): EditorExtension contributed a
workspace tab but nothing activated it on editor.open. Add an activate()
that reveals the tab on editor.opened / editor.active-changed; the
view's hydrate() pulls the active buffer on mount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents why the markdown/decisions readers load from a retained
per-reader nav-history (grab-current-on-mount + single 'load' path)
rather than per-view state (dies with the widget — the T-196 bug) or
MessageBus retention (wrong layer). The user chose the nav-history
helper over a bus fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reader opened repo-local .claude markdown but rejected user-scope
files under ~/.claude with "path outside workspace" — that dir is
global, outside the repo, and files.read was repo-confined (T-102).
Per D-76 the Claude config surface is clide-managed, so files.read now
resolves a path under an allow-list: the workspace root plus trusted
extra read roots (FilesService.extraReadRoots), wired in main.dart to
~/.claude when present. Reads widen; writes stay repo-confined, and the
symlink re-check still refuses a config-root symlink that escapes. Off-
root paths and `..` traversal are rejected as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveUnderRoot joined an absolute input onto the workspace root
(/repo + /repo/x → /repo/repo/x), so files.read 404'd on a file that
exists. The Claude Config tab hands the reader a skill's absolute
SKILL.md path, which hit this. Normalize an absolute input as-is; the
existing containment check still rejects absolute paths outside the
root, so the T-102 boundary is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the find-in-files engine. A replace engine applies the
query's replacement to each matching file — literal or regex with
capture-group expansion ($1, $&, $$) — and reports per-file, per-line
before/after edits computed with the same logic the apply uses, so
preview and apply never disagree.
The search.replace command previews (no disk writes) or applies
(writing each changed file through the workspace path-safety guard).
The panel gains a Replace field: each match row previews its rewritten
line, and Replace all is gated on a clean git working tree (git is the
undo) plus a confirmation before it writes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for the quick-open overlay's keymap-intent handlers (nav,
accept, dismiss), the no-match / truncated / walk-failure hints, the
search panel's error + no-results states and toggle re-run, the
controller's failed-grep and exclude paths, and engine glob/regex-group
cases. Restores the coverage floor (95.20%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The find-in-files UI on top of the search.grep engine. A
FindInFilesController drives search.grep, accumulates streamed
search.match events (scoped to the active searchId, stale ids
ignored) grouped by file, and opens a match in the editor at its line.
The SearchPanelView contributes a sidebar tab: a debounced query box,
regex + case toggles, include/exclude glob fields, and a grouped
results list with the matched span highlighted.
findInFiles.open (Ctrl/Cmd+Shift+F) reveals and activates the search
tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pure-Dart content-search engine behind find-in-files (D-79): walks
the ignore-pruned workspace, fans files across worker isolates
(Isolate.run) for parallelism, matches each line with a literal
indexOf fast-path or a RegExp, and streams match batches with
cooperative cancellation. No ripgrep dependency; the search.grep IPC
contract is engine-agnostic so an rg accelerator can slot in later.
search.grep returns a searchId and streams search.match / search.done
(or search.error) events, mirroring files.watch; search.cancel stops
an in-flight search. The service reuses the files service's resolved
ignore set so both honour the same ignore_files: layering.
editor.open gains an optional 1-based line argument: it converts the
line to a byte offset and sets the initial selection, enabling
click-to-line from search results.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A file picker overlay over the whole workspace, distinct from the
command palette. QuickOpenController holds the file list + a
subsequence fuzzy filter; the overlay loads the list via files.walk on
open, shows RecentFilesService entries on an empty query, and opens the
selection through a shared openWorkspaceFile helper (.md → markdown
reader bus, else editor.open) that the files panel now also routes
through, so recents stay in sync from every open site.
Bound to ctrl+p / meta+p with `when: !palette.open` so it never
collides with the palette's ctrl+p navigation; in-overlay arrows/enter/
escape reuse the palette's keymap-driven model via quickOpen.* intents.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hardcoded .gitignore + .clideignore read with the ordered
ignore_files: chain from .pql/config.yaml (D-4) — the single ignore
knob clide owns (D-3). readIgnoreFiles defaults to .gitignore (plus
.clideignore when present) when the config is absent or malformed, and
honours an explicit [] as "no file-based exclusions".
Add walkFiles + the files.walk command: a recursive, ignore-pruned,
capped flat file listing reused by quick-open (T-51) and the search
engine (T-52). Closes the never-filed ignore-layering placeholder in
files_commands.dart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Find-in-files / replace (T-52/T-53) run as an in-process isolate-pool
grep engine behind an engine-agnostic search.grep verb — not pql (its
search is a ranked document index, with no line numbers, regex, or
glob) and not a ripgrep shell-out (unvendored, not guaranteed
cross-platform). ripgrep is kept as a future optional accelerator
behind the same verb. Clarifies the D-3 wrap-pql boundary: content
grep is a code-navigation primitive pql does not offer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalizes the pty split into an explicit "parallel=false" opt-out: a
`serial` tag (declared in dart_test.yaml). The parallel flutter run now
excludes `pty || serial`; a separate `flutter test --tags serial
--concurrency=1` pass runs the vulnerable ones. For the coverage gate the
two passes are real-merged by ci/merge_lcov.py (union DA, max hits, recompute
LF/LH) — a plain concat would double-count and corrupt the total.
Tag transcript_publisher's bus-republish test serial (it flaked in the
parallel pool). Gate verified green end-to-end at 95.08%.
T-193.
Co-Authored-By: Claude <noreply@anthropic.com>
The pty-tagged tests spawn real PTYs and flaked when dart test ran them in
parallel (fd contention) — papered over with retry: 2. Run that pass with
--concurrency=1 and drop the retries: serialization is the correct fix for
resource-bound tests. Verified stable across repeated runs.
T-193.
Co-Authored-By: Claude <noreply@anthropic.com>
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>
The git-hooks line listed only pre-commit + post-merge; the load-bearing
one is the pre-push gate (core.hooksPath = .githooks). "The five commands"
listed seven. And push-check now runs test-coverage (a11y folded into it),
not a separate fast suite + test-a11y pass.
Co-Authored-By: Claude <noreply@anthropic.com>
The markdown and decision sidebar readers gain a shared action bar. A new
lib/builtin/shared/reader_chrome.dart provides ReaderHistory (browser-style
back/forward stack — push truncates forward), a ReaderHistoryMixin that also
holds a single pin slot, and a ReaderActionBar widget. Both readers push to
history only on external selection; back/forward and jump-to-pin reload
in-place without re-publishing a selection (no bus churn / no decision-tab
re-trigger). The edit pencil opens the current doc in the editor
(editor.open) — the markdown path, or the decision's file_path.
T-189, T-190, T-191.
Co-Authored-By: Claude <noreply@anthropic.com>
Wave A's widget tests pulled previously-untested files into the coverage
denominator (the decision extension loads decisions_view; the file-tree
tests load file_tree_controller), dropping total coverage to 93.6%. Add
tests for DecisionsView (list render + tap-to-select, the T-188 publisher
side), FileTreeController, and the remaining DecisionDetailView branches,
restoring the total to 95.03%.
Co-Authored-By: Claude <noreply@anthropic.com>
`make test` is now the fast dev inner loop: no coverage, parallel
(--concurrency=12), ~21s warm (down from ~36s). Coverage moves to a new
`make test-coverage`, which push-check runs to feed coverage-gate. Drop the
separate test-a11y pass from push-check — the coverage run already executes
test/a11y. Both runs get --timeout 60s so a hung test fails fast instead of
wedging the runner ~10min and stalling the gate.
Measured: coverage is the floor (~36s) and concurrency-insensitive, so the
gate keeps coverage without --concurrency; only the no-coverage dev path
benefits from parallelism.
T-192.
Co-Authored-By: Claude <noreply@anthropic.com>
The composer sourced its slash suggestions only from the CLI probe
(activeClaudeConfig.slashCommands), which never advertises clide-owned
commands, so /resume and /fork were missing from the typeahead. Union
kClideOwnedCommands onto the command source unconditionally — whether a
caller supplies a resolver or the default probe is used — de-duped via a
Set so /clear (in both) appears once.
T-162.
Co-Authored-By: Claude <noreply@anthropic.com>
The decisions extension tore down and re-contributed the decisions.detail
context-panel tab on every selection, racing the view's own subscription and
leaving the panel unrevealed — so clicking a decision often did nothing. Match
the working ticket panel: contribute the tab once (static), and on selection
just reveal the context panel and activateTab; DecisionDetailView loads via its
existing subscription.
T-188.
Co-Authored-By: Claude <noreply@anthropic.com>
The files panel and the Claude Config tab called ipc.request('editor.open')
for every file, which targets the editor — so a .md click never reached the
right-side markdown reader (it opens only when something publishes
('builtin.markdown','selection')). Route .md clicks from the files panel
(tree + filtered rows), the Config tab's file-backed rows, and .md wiki links
in the viewer to that channel; non-.md files still open in the editor. Also
remove the dead DaemonEvent fallback that listened for 'editor.buffer_activated'
(the registry emits 'editor.active-changed').
T-187.
Co-Authored-By: Claude <noreply@anthropic.com>
A reusable helper in the shared harness that drains microtasks + advances
one short fake-time tick, replacing the two patterns that have repeatedly
wedged the suite (and the pre-push gate) for ~10 minutes each: pumpAndSettle
(loops until quiescent — hangs on perpetual animation / overlapping async)
and `await Future.delayed(Duration.zero)` inside testWidgets (a real timer
that never fires under fake-async). Bounded by construction — cannot hang.
Co-Authored-By: Claude <noreply@anthropic.com>
Regenerated by `pql decisions sync` — moves resolved questions (Q-6, Q-19,
Q-21, Q-22) into a Resolved section.
Co-Authored-By: Claude <noreply@anthropic.com>
Dogfooding the skill surfaced it: `--status backlog,ready` is not a comma
list — it matches nothing and silently returns [], which would make batch
selection lie. Use a single `--status backlog` and note the `--unblocked`
filter still surfaces prose-"blocked on upstream" tickets (e.g. T-158).
Co-Authored-By: Claude <noreply@anthropic.com>
Batch selection no longer walks blockers per ticket or post-processes
JSON: read the landscape with `ticket show --tree`, select actionable
work with the composable `ticket list --under <epic> --leaf --unblocked
--status backlog,ready`, and record refinements with `ticket append`
instead of re-sending the whole description through `refine write`.
Co-Authored-By: Claude <noreply@anthropic.com>
The team cockpit / chat / config-tab work landed under-tested and pulled
total line coverage to 94.32%. Add tests for the team chat sidebar + pane
(@-completion, overlay, interrupt, message rows), the config loaders, the
stream-json MCP/streaming/rate-limit paths, and the conversation/prompt
card variants — restoring the total to 95.06%.
Co-Authored-By: Claude <noreply@anthropic.com>
A --fork-session branch is spawned without --session-id, so claude mints
a new session id that only arrives in the init event; the ManagedSession
was left holding its placeholder. StreamJsonSession now captures
session_id from the first event that carries it and exposes it via
claudeSessionId / sessionIdResolved; the orchestrator folds that back into
ManagedSession.sessionId (idempotent for normal sessions). A fork can now
itself be resumed or forked.
T-185.
Co-Authored-By: Claude <noreply@anthropic.com>
A live capture against claude 2.1.150 (both --print and the interactive
stream-json transport clide uses) confirms --include-partial-messages
emits the in-progress reply as stream_event envelopes wrapping Anthropic
streaming deltas — NOT assistant events with partial:true, which is what
T-168 assumed, so that handler never fired and streaming was inert.
Replace it: accumulate content_block_delta text per message id (tracked
from message_start, since deltas carry no id) and emit a placeholder under
a stable partial-<id> uuid the controller upserts in place; the matching
single-text-block assistant event reuses that uuid to finalize, while
tool_use / thinking blocks keep their own uuids and append in order. Tests
rewritten against the captured shape; spike doc records it.
T-184.
Co-Authored-By: Claude <noreply@anthropic.com>
The Config sub-tab grows from a static settings table into a browser of
the Claude environment: the pinned settings table stays, and below it
expandable accordions list the full (never-truncated) sets of skills,
agents, commands, hooks, permissions (grouped + colour-coded by allow/
ask/deny), and MCP servers. File-backed entries are clickable and open
their .md via editor.open. ClaudeConfig gains agents, hooks, and
mcpServers loaders plus path fields on skills/commands, kept live by the
existing .claude file watcher.
T-183.
Co-Authored-By: Claude <noreply@anthropic.com>
Renders broker traffic as a chat timeline and makes the user a first-class
participant. The broker grows a Stream<TeamMessage> and a recipient field,
auto-registers a virtual `user` member, and gains sendAsUser. A Flutter-free
TeamChatModel (owned by the orchestrator) accumulates the feed and exposes
postAsUser with @-routing (a new at_commands helper mirroring slash) and an
optional interrupt that cancels the target's turn before delivery. One model
backs two surfaces: a compact cockpit widget that pops out into a full
workspace chat pane. CLI parity via clide.team-chat.open / .post.
T-180.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds fork-into-a-pane: /fork in the composer, a roster Fork button, and a
clide.agent.fork command all branch a session via
--resume <source> --fork-session, so the branch gets its own claude
session id and diverges without touching the original. SpawnSpec/
ManagedSession gain forkSourceSessionId; the orchestrator selects the
fork argv via a new forkSessionArgs helper; the session host opens the
fork as a new secondary pane.
The branch's real claude session-id (assigned by --fork-session, arriving
in the init event) is not yet captured back — tracked as T-185.
T-172.
Co-Authored-By: Claude <noreply@anthropic.com>
Each roster row shows a D/A/P mode badge reflecting the session's live
permission mode. Click cycles the safe trio default -> acceptEdits ->
plan and sends a set_permission_mode control_request to that session
(mirrors interrupt(); fire-and-forget). bypassPermissions is a footgun,
so it is reachable only on Shift-click and behind an inline confirm. A
clide.agent.set-permission-mode command gives the CLI parity.
T-181.
Co-Authored-By: Claude <noreply@anthropic.com>
The meta sidebar's Team tab becomes a control surface for clide-managed
agents instead of a read-only roster. Each row gains show/hide, mute,
close, and inject-a-message; a live task list renders from the broker
with reassign. The broker grows a Dart change-stream (kept Flutter-free
for dart test) plus tasks/reassign; the orchestrator gains mute/unmute,
injectMessage, and member-name session resolution. Every new UI action
has a matching clide command (D-6 parity).
T-171.
Co-Authored-By: Claude <noreply@anthropic.com>
The copy button and custom message actions only rendered on hover, so
they were unreachable by keyboard or assistive tech. Keep them in the
tree always — revealed via opacity on hover OR focus — and route each
through ClideTappable (Tab traversal + Enter/Space activation) with a
Semantics button label and onTap so AT can discover and invoke them.
alwaysIncludeSemantics keeps them in the semantics tree while hidden.
T-174.
Co-Authored-By: Claude <noreply@anthropic.com>
The conversation pane now exploits the structured stream instead of
dumping tool input as JSON. ConversationController indexes tool_use by id
so a tool_result pairs back to its call and renders the Edit/Write diff or
is_error failure in place; per-tool bodies (Bash command+output, Read/Grep
file/query) reuse the shared renderers factored out of the permission
card. SessionStatus gains cost + contextWindow + rate-limit, read straight
off the init/result/rate_limit_event events, so the in-pane status line
reflects live state without the config probe.
Partial-message streaming is wired behind --include-partial-messages but
its event shape is unverified against the live binary and degrades to a
no-op if it differs — see T-184.
T-168.
Co-Authored-By: Claude <noreply@anthropic.com>
Session lifecycle now runs entirely on the stream-json model: argv
selection picks --resume <id> for an existing transcript and
--session-id <uuid> for a fresh one, and the managed-session orchestrator
owns spawn/close. With the transport off tmux, remove the tmux session
lifecycle (reaping, kill-all-for-repo) and the tmux-polling team observer;
kill-all-sessions now closes sessions through the orchestrator. Team
membership is orchestrator-driven since the coordination broker landed.
Amends D-41 (tmux persistence -> --resume). T-167.
Co-Authored-By: Claude <noreply@anthropic.com>
StatusbarHost laid every item at intrinsic width, so once the focused-pane
context line grew long (a model/mode/context/skills summary) the row's
content exceeded the bar width and overflowed instead of letting the slot
shrink. Add an opt-in flex factor to StatusItemContribution; the host wraps
flex>0 items in Flexible(loose) so they yield width when the bar is tight,
and the marquee then receives a bounded viewport and scrolls. Drops the
fixed maxWidth cap on the pane-context item.
T-160.
Co-Authored-By: Claude <noreply@anthropic.com>
_kOther carried a literal NUL byte so a real option labelled "other"
could never collide with the sentinel. The raw byte made the source
read as binary: git showed a binary diff and grep/file treated it as
data. Write it as a unicode escape instead — identical runtime value,
plain-text source again.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude often sends the file path itself as the tool description for
Write/Edit. The card body already renders that path, so printing the
description line above it showed the same path twice. Suppress the
description when it just repeats file_path.
Co-Authored-By: Claude <noreply@anthropic.com>
pql 1.5 re-introduced .pql/pql-plan.json as the canonical planning-
state export (regenerated on every decisions/ticket mutation). Bring
the un-ignore back so the backup rides commits across clones —
add028e dropped it when the file was dead under pql 1.4, but the
export is live again. Hooks stay ignored (still per-clone, absolute
paths baked in).
Co-Authored-By: Claude <noreply@anthropic.com>