Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Documentation

Everything written down about Magic Git, beyond the code: how it is built, how to build it, and every decision, plan and report behind it. This file is the table of contents, and the one place to see what is actually live. tool/records.dart checks that it stays complete: a record with no row here fails flutter test.

I want to…

I want to… Start here
understand how the app fits together architecture.md
build, install or sign the app the build guide
run the Xcode unit tests without a certificate the build guide
write a new decision record, plan or report CLAUDE.md, and dart run tool/records.dart next for its number
know why there is no libgit2 0001-MADR
know why the SSH transport works as it does 0011, 0012, 0014, 0024
understand the file watcher 0045-MADR, and the watch section of architecture.md
know why the entitlements files are never edited 0042-MADR, 0053-MADR
know why every provider declares retry: 0017-MADR
prove that a test or guard can actually fail 0029-MADR, 0030-MADR, and tool/mutate.py
know why the docs are laid out like this, and what checks them 0054-MADR

Architecture

architecture.md describes the system as it is now. The original design plan it replaced is kept as history in 0056-PLAN.

Guides

Guide For
build-macos.md building, signing, notarizing and installing the .app; running the Swift unit tests

Status vocabulary

Machine-readable in each file's YAML frontmatter. verified: is when the status was last checked against the code, not when the document was written.

The vocabulary is the madr-and-plan-writing skill's, which follows MADR 4.0.0.

status applies to meaning
proposed decision, plan written, not yet decided or approved. No code should exist for it.
accepted decision decided. The decision governs the codebase.
rejected decision decided against. Do not re-propose without new evidence.
deprecated decision no longer governs new work, but not replaced by a specific record.
superseded by NNNN-MADR-… decision replaced by the named record, which says why.
in-progress plan approved; phases are landing.
complete plan, report every acceptance criterion met, including maintainer-only checks.
superseded plan replaced by a later plan.
partial plan, report some of the work is real; the rest was deliberately never done. The document says which.
executed (retired) plan engineering phases shipped; the body names any residual. Every plan carrying it was mapped to complete, in-progress, partial or superseded on 2026-09-19; it survives only in dated history below.

A status is a summary. Where it and the document's body disagree, the body wins.

Decisions

Every MADR and every PLAN, by number.

# Record Status Plan Status
0001 Do not adopt libgit2/git2dart rejected
0002 Forge change-request models and merge UX accepted plan complete (Phase 6 partial)
0003 Base-relative branches workspace accepted plan in-progress
0004 UI/UX deep-debug audit backlog accepted plan complete
0005 Task-centered adaptive workspace accepted plan · UX baseline in-progress · baseline partial
0006 Hybrid native title bar accepted plan in-progress
0007 Docs completion audit accepted plan in-progress (Phase 7 maintainer)
0008 Unified repository chrome accepted plan complete
0009 UI/UX debug-pass backlog accepted plan in-progress
0010 In-app Help Book rewrite accepted plan complete; Phase 7 (maintainer review) discharged 2026-09-19 by 0053-PLAN Phase 12, against Help v3.0
0011 SSH transport stability hardening accepted plan in-progress
0011 Toolbar slot schema migration accepted (none — implementation and confirmation recorded in the MADR itself) executed — a v1→v2 preference-record migration (currentVersion/minReadableVersion/defaultVisibleToolbarSlots in lib/core/settings/repository_workspace_prefs.dart) repairing a hidden-toolbar defect on load; confirmed present in code and by its own named test group (test/repository_workspace_prefs_test.dart) 2026-09-17
0012 Adopt dartssh2 3.3.0 accepted plan superseded
0012 Focused commit composer sheet accepted plan in-progress
0013 Prefer dartssh2 over dartssh3 accepted plan in-progress
0014 SSH engine next-wave hardening accepted plan complete
0015 SSH engine and UI unit-test gaps accepted plan · tail both complete
0017 Enforce conventions in source accepted plan complete
0018 Transport readiness is not an error accepted plan in-progress (deviation recorded)
0019 Pin glab to the repo origin host accepted plan in-progress through Phase 7; Phase 8 open
0020 Fetch/pull/push lag accepted plan complete
0021 Create-repo identity stays per-repo accepted plan in-progress
0022 git/gh/glab engine debug audit accepted plan in-progress; Phase 15 run 2026-09-04 — M5 CONFIRMED on the real host and reopened (19 orphaned watchers, oldest 16.9 days; cleared, defect open)
0023 Commit-and-push perceived freeze accepted (amended) plan in-progress; live-app measurement done 2026-09-04 — 123 git processes vs the ≈22–40 assumed; refresh-repetition open
0024 SSH / remote-repo engine debug audit accepted (amended A1.1, A2.1, L2) plan complete; M2 confirmed and P1 measured live 2026-09-04. Phase 8 (P2) not executed
0025 Host process economy accepted plan complete; every phase resolved — 1-5, 7, 9 executed; 6, 8, 11, 12 declined on evidence; 10 already implemented. Measured 123→76 processes per commit+push and 0 while idle. D1 declined on measurement (amendment D1.1). Findings C4/C5 carried by 0026-0029. Residuals named in the plan: two aggregate ceilings unbuilt (nothing can exceed them), orphan target needs a week of elapsed time
0026 Degraded-watch poll diagnosis accepted (amended 0026.1) plan complete; H1 CONFIRMED by test (Expected: <2> / Actual: <1>) and fixed by serialising start(). Post-fix capture confirms the 5s poll regime is gone (142s idle silence, 2 watchers at the ceiling). H2/H3 open, not refuted; capture raised 0025 C5
0027 Watcher reclamation cannot reclaim accepted plan complete; all 5 phases executed. The sweep could never kill anything (recorded a shell pid, signalled only inotifywait/fswatch, via Linux-only /proc) — now ps-verified identity + per-instance lease/registry files + per-instance staleness + legacy reclamation. Amendment 0027.1: pre-fix orphans are irrecoverable (their pid record was overwritten) and need a one-time manual kill — done on the host
0028 Ceiling refusal and teardown residue accepted plan complete; all 5 phases executed. H2 confirmed and fixed — refusals are typed and a ceiling-refused repo takes a freed slot at once instead of polling at 48 processes/min. H3 resolved by consequence of 0026+0027 — an abandoned watcher self-terminated at 371s, measured; no remedy needed. Ceiling scope: no defect (amendment 0028.1)
0029 Host scripts must be executed by a test accepted plan complete; a contains(...) on script text may pin composition, never behaviour. Went from 1 of 6 host scripts executed by a test to 5 of 6, with the installer permanently exempt and its reason recorded; a scan reads lib/ and fails on any unclassified builder. This is the gap that shipped a sweep which could never reclaim anything (0027)
0030 Test coverage gaps are shaped, not sized accepted plan complete — measured 81.4 % → 81.7 %, and the parity gap closed (Scoped 40.9 %→100 %, Proxy 60 %→96.9 %). Originally: 81.4 % line coverage, zero files uncovered — and five defects in one session, four of them in covered code. Names four failure shapes coverage cannot see (seam, composition-vs-behaviour, parity, in-flight state), measures a lopsided executor seam (ProxyCommandExecutor 1 test file vs SSH's 126), and proposes 8 tests by risk. Recommended against a coverage target, and none was adopted. 9 phases executed; 2 partial and recorded as such
0031 Forge namespace on create accepted plan complete; live-verified 2026-09-05 — nested subgroups (2 levels) confirmed working, origin resolves to the created path; create-from-existing-folder can only use the default namespace. The transport already handles group paths — the block is a directory-name validator doing double duty. Verified live: glab 1.116 on both machines accepts namespace/name, and the API lists 24 groups this account may create in, incl. a nested group the user may create in. Chooses a picker with free-text fallback, passing the FULL PATH (a --group flag would break origin resolution)
0032 Recent and searchable forge namespaces accepted plan complete7 phases landed; a search bar with a sectioned RECENTLY ACTIVE / ALL YOU CAN CREATE IN dropdown (the chips were removed after maintainer review — MADR amended), local history written by both create and clone, and live verification run (events → 4 namespaces in 1.9 s; search narrowed 24 → 1; the creation-level arm stays fixture-only, as predicted); the field is now the search input, with local filtering plus a debounced server search, and the recency writer wired (a Phase 5 deviation found NamespaceHistory.record had no production caller, so "local history" was shipping empty); 5 phases — extract the palette's tier matcher, correct + page-walk the creatable list (paying the pagination debt 0034 deferred here), forge-events recency projected to namespaces (a repo is created in a namespace, never in a project), provider composition, then the searchable field. Phases 1-2 stand alone if 3-5 never land. Follows 0031. The suggestion chips show the account's login plus 7 of 24 creatable groups, selected alphabetically by the group's leaf name — a field the chips never display — so 17 of 24 are unreachable from the sheet. Measured live: events give the user's own activity in 0.4–0.8 s, the membership call that resolves namespaces in bulk costs 9.0 s, and search+min_access_level compose (37 → 24) with full-path matching at 0.67 s. Proposes recency ranking from the Events API plus a hybrid local/server search on the existing field, reusing the command palette's tier matcher and debounce. Also records two defects: min_access_level=30 is not GitLab's create gate (project_creation_level; 1 of 24 groups needs Maintainer), and api(paginate: true) returns concatenated JSON documents, not one — its comment says otherwise
0033 Decompose the create-repository sheet accepted plan completeall 7 commits landed (0a, 0b, 1, 2, 3, 4 + pipeline tests, 5); 7 commits across 6 phases, each independently revertible — the two behaviour changes isolated first (0a the _goBack guard, 0b the eager dial), then adopt WizardReviewRow/LabeledTextField, canonicalize path helpers, extract shared sheet code, extract the pipeline, extract the step bodies. Neutrality proven by the 37 existing tests, unedited against a captured baseline (expect( 9043, testWidgets( 999); phase 2 is neutral at 6 of 8 sites and deliberately changes 2. All 4 open questions resolved 2026-09-06; MADR amended twice (decisions, then the disproof). create_repo_sheet.dart is 2176 lines in a single 2068-line state class with 47 methods, of which _submit() alone is 424 — already carrying its own eight // --- phase --- banners. Method-level comparison against clone_sheet.dart finds 6 byte-identical methods (83 lines) and 6 more drifted (_destinationSection 95 %, _goBack 86 %, _onDestChanged 80 %, _recomputeTarget 78 %). The drift has already produced a live defectdisproven 2026-09-06 by executing the plan's own sabotage step: the guards differ (clone checks _finished in _goBack and its footer, create in neither) but _submitting already spans that window, so Back is never live. The divergence is real, the defect is not; Phase 0a is reframed as alignment + an invariant pin. App-wide, _basename exists 8 times in 2 behaviourally different families (verified to disagree on a/b//, /, //) and _stripTrailingSlashes twice, differing by one character. WizardReviewRow and LabeledTextField already exist and are bypassed. Proposes 6 phases — defect fix alone first, then adopt/canonicalize/extract — proven neutral by the 37 existing tests, unedited
0034 Debugging pass findings accepted plan completeall nine findings resolved across three tranches; covers tranche 1 only — the silence class (F1 observer, F9 prefs, F7 orgs pagination). Tranche 2 (F2/F3/F4) already done under 0035 and 44169ac; tranche 3 (F5/F6/F8) deferred. Explicitly leaves glab groups pagination to whichever plan executes 0032, since 0032 replaces that call. A sweep of lib/ (277 files, 103,530 lines) against a clean analyzer, 3,602 passing tests and one TODO-class marker (itself a false positive). Nine findings. The headline is not a line: retry is off by design, no ProviderObserver is wired to the main scope or any tab container (the pop-out window is the only place with one), and 49 .value ?? const [] sites collapse error into empty — so a failed provider produces no retry, no log and no UI. Plus a crash trio of mounted guards on the wrong side of an await (_batchHide, _bulkDeleteSelected, _onDestChanged ×2 — the last widened by this session's own 0033 Phase 0b, disclosed), two orphaned code islands, a silently-truncating user/orgs page, and swallowed prefs writes. Records what was checked and found clean too: teardown discipline (8/8 disposal candidates were scanner false positives), and 6 of 9 setState-after-await candidates. Proposes 3 tranches, severity first. F4 fixed 2026-09-07 — reproduced first (setState() called after dispose() in both sheet States), guarded in both, mirrored tests
0035 Multi-select coverage and the two guards it blocks accepted plan completeall 5 phases landed — all four batch actions covered; F2 fixed, and reproducing it revealed the defect is in shared code with 10 call sites, not the one 0034 named; 5 phases, 5 commits — entry point + fixture, then Pin/Unpin/Hide, then the unverified Delete path, then F2 and F3. Coverage first by maintainer priority. Whole plan changes exactly two lines of lib/, both if (!mounted) return;. 0034's F2 and F3 could not be reproduced, and the reason was not the fix. Both sit behind the branches batch bar, and multi-select is Review-mode-only by design (branch_navigator.dart:450) — in browse mode onMultiSelect is never called, so shift-arrow and command-click fall silently through to single selection. Three other hypotheses were measured and wrong first (modifier synthesis works; the list is focused; isActive defaults true). The technique is one line — tap(find.text('Review')) — and is demonstrated, not proposed: 2 selected, bar showing Pin, Unpin, Hide, Delete if merged…. Also measures the gap that made this hard: nothing in the suite reaches _batchHide, _batchPin or _bulkDeleteSelected — the components they drive are well tested, the wiring is not, and both defects live there. Measured that it works: Pin [feature, main], Unpin [], Hide [feature] (HEAD correctly skipped), all persisted, no exceptions — but Delete if merged… stayed unverified, disabled by a missing comparison base in the fixture. Chooses: build the technique, cover the four actions first (maintainer priority), then reproduce F2/F3 on it — over the two-line guard-without-proof that 0034 warns against
0036 Choosing a create destination while connected accepted plan completeall 6 phases landed; a connected create or clone now shows the Destination step (This Mac + every saved host, defaulting to the current session) and opens its result in its own tab — a saved host is dialled in a fresh tab at the first commitment (Browse… or Create), so the tab the wizard was opened from is never touched; unsaved local creates open in place (5B); the cap is refused up front (7A). Not a missing capability but a missing choice: the step was applicable: () => widget.landing because beginProvisioning claims a tab's session, and openOrFocus already ran work in another tab's container. Three deviations mid-execution (a picker dev-dependency, Browse… needing the host before submit, and the step shifting every connected test by one) and one production bug in the phase's own edit (a seeded popup value before its items loaded). Phase 7 (2026-09-08) brought the third sheet in: AddExistingRepoSheet already offered every saved host, but carried the third hand-rolled copy of the dial — the one the provisioning mixin's own header cites as already-fixed while clone and create still shared the 0022 H4 bug. It now uses the mixin (three sheets, one implementation) and this record's decisions, and routing its remote half surfaced a real defect: finalizeProvisionedInTab dropped gitDir, which would have opened a dotfiles work tree with no git-dir. It also fixed a pre-existing UI defect that phase surfaced: Open sat inside the sheet's scroll view, so at the app's 640x480 floor it was ~180 px off screen along with the caption explaining why it was disabled — create and clone pin their action row, this sheet did not. 30 mutations, all killed
0037 Namespace recency from the repositories you open accepted plan completeall 5 phases landed; the create sheet's recency list now learns from every repository you open, not only the ones this app created or cloned. The gap it closes: local history recorded only what we wrote, and the forge events feed sees 7 days and one 100-event page, so a repository you joined and only ever open was invisible to both however often you worked in it. Two halves — ConnectionController records at every open (SSH connect, local connect, in-tab repo switch, all hung off one _recordRecentOpen so they cannot drift), and a mount-time scan backfills the 30-entry recents log for everything readable without a handshake: a bookmarked local repo, and an SSH repo whose host a tab already holds a session on. A host with no session is skipped, never dialled — the sheet cannot wait on a handshake — and is covered instead at its next open. Only namespaces the account can create in are recorded (maintainer's decision), memoised per (forge, host) per session, with an unknown answer recording nothing. Two deviations: the creatable check was unreachable from a new file (resolved by extracting recordNamespaceFromOrigin, so open and scan share one implementation of the rule), and the planned Ref parameter could not compile from initState because WidgetRef is not a Ref. The sabotage round was the value here: four survivors across the phases, and not one was a hole in the code — every one was a test proving something other than what it claimed (a guard masked by an earlier guard, twice; two tests naming a throw where originUrl swallows every failure; an invalidate that only matters for the This-Mac store). One line deleted as dead code was proven load-bearing the same way. 21 mutations, all killed
0038 Workspace entry points debugging pass accepted plan completeall 6 phases landed; all 9 findings closed. A sweep over the three ways a repository enters the app — create, clone, open-existing — after two rebuilds in quick succession (0033 decomposed them, 0036 gave them a destination step and per-tab routing). The worst finding was silent: finalizeProvisioned recorded no per-repo recency, so every remote create, clone and open was missing from Recents and taught the namespace field nothing — it is a fourth open path, and 0037's claim that three were covered "by construction" is amended rather than quietly patched. Ad-hoc SSH sessions were untargetable: the destination was a String? where null meant This Mac, so a session with no saved id read as the Mac and no selection could reach the host you were on; the destination is now a sealed three-state type, because a sentinel id would have restored the capability and re-created its cause. WorkspaceFlow is a plain, widget-free object owning the tab lifecycle all three sheets had hand-copied — the mixin before it was on ConsumerState, which is why 257 lines of session/tab/grant handling had zero direct test callers and its copies could drift; it now has 26 tests, none of which pumps a widget. _openResult (one line of real divergence between two 70-line copies) and the local registration moved in too, which made the wrong-tab defect unrepresentable rather than fixed. Also: the add-existing sheet now opens where you are and reuses a saved folder's id instead of minting a second record for one path, and a dedupe-focus stopped reporting itself as the tab cap. Two deviations, both stopped on. 27 mutations, all killed — six survivors and four broken experiments along the way, of which exactly one survivor was a hole in the code; the rest were tests proving something other than what they claimed. Both recorded residuals were closed the same day: registerAndActivateLocal's unreachable save flag removed (its coverage moved onto saveLocalRepo, which was live and had no direct test), and _effectiveConnectionId deleted — its ?? activeId fallback was a fossil of the very model deficiency F2 fixed. 31 mutations, all killed
0039 Process-global state and control heuristics audit accepted plan in-progressall 10 phases shipped 2026-09-09, one commit each. An audit of internals, caching, session management, cross-application state, heuristics and algorithms. Six process-global singletons still encode "the app holds one session at a time", a precondition multi-tab (11689cc) falsified: any tab's connect empties every other tab's diff/blame caches and discards their ad-hoc workspace prefs, the two-watcher ceiling is one budget across all hosts, and session telemetry is the union of every tab truncated by whoever connected last. ScopedAccess is the one global that was made multi-session-safe, and is the model for the seam proposed. Three heuristics measure a proxy rather than the quantity: the adaptive read cap's gradient mixes rev-parse with 60 s branch-review batches in one distribution, so opening Branches reads as congestion; own-mutation suppression is held for a whole background fetch and drops ticks rather than deferring them, so an external change during an auto-fetch is lost until ⌘R; the channel-open error floor recovers on three successes of any lane and oscillates against a hard MaxSessions. Three algorithms: branch divergence runs one rev-list walk per branch where %(ahead-behind:) needs one for all (and would be the app's first consumer of the git version it already probes and only ever displays); commit-graph lane layout is rebuilt from scratch on every page of history; cache eviction is recency-only over entries whose refetch costs differ by three orders of magnitude. The paired plan is ten phases, each independently revertable and six of them droppable, and it carries amendment 0039.1: writing it found that CommitGraph.build also carries allHashes, which the record had claimed it did not, so the incremental-layout phase gained a recompute-from-index and a differential test and moved last — and amendment 0039.2, where the record's claim that nothing gates a code path on the host's git version turned out to be false (mergePreviewCapabilityForVersion does), caught by the same binary-grep trap the record itself warns about; A1 now follows that gate instead of building a second one, and got smaller. What shipped: one SessionScope seam, and the caches (F1/F2), ad-hoc workspace prefs (F3) and watcher ceiling (F4, keyed by host) partitioned onto it — plus deferred rather than dropped watcher ticks (F6/H2), bounded at 9 s so a multi-minute fetch can no longer hide a teammate's push. F3 also closed a collision neither record predicted: sessionEpoch is a per-controller counter, so two tabs' first ad-hoc sessions were both adhoc:ssh:1. 20 mutations, all killed, none did-not-apply; three deviations recorded, two of them mutation survivors that turned out to be gaps in the new tests rather than holes in the code. Telemetry (F5) is scoped too — the executors take a sink rather than reaching for one. Beyond the scoping: adaptive read samples are bucketed by normalised command, so opening Branches on a 500-ref repo no longer reads as congestion, and the channel-open error floor became a circuit breaker with an escalating dwell; branch divergence is one for-each-ref walk on Git ≥ 2.41 (the atom emits ahead-then-behind where rev-list --left-right emits behind-then-ahead — verified on 2.55.0, and the assertion that pins it is the catalogue's main target); cache eviction is Greedy-Dual-Size-Frequency over measured fetch cost; and history paging resumes the lane layout instead of rebuilding it. 47 mutations, all killed, none did-not-apply. Twelve deviations recorded — including a commit-message hook that credited five unimplemented phases when docs and code shared a commit, a mutation entry silently un-armed by a later phase's reformat, and four survivors that were each a test unable to see the thing it named. Residual: the A1 wall-clock measurement on a real host, and the optional gutter sub-step of Phase 10
0040 The watcher ceiling is the wrong limit accepted plan partialphase 1 shipped; phases 2 and 3 shipped then REVERTED the same day (amendment 0040.2). Runtime investigation on the reporting host, not inference. Every repo in a 15-repo directory was falling back to polling; the obvious reading — a leaked slot — is wrong, and the record says why at length because the wrong reading is the plausible one. Both held slots are live, leased watchers (ppid settles the duplicate-looking process rows), and there are zero orphans across the host, so the lease has taken over the job the cap was hired for after 0025 C3's 19-orphan measurement. What is actually wrong: maxConcurrentWatchers = 2 sits in front of maxConcurrentStreams = 8, which is already enforced separately and already degrades gracefully — a cap four times tighter than the budget it respects, on a host using 0.18 % of its inotify watches, forcing 13 repos onto a fallback measured at 48 git processes per minute each. Three tabs share the one host budget. The slot leak is real, reproduced on the current tree and at dc78436, pre-existing (7735f13/d10a334), and not what is biting — but it is worse the tighter the cap. Decision: fix the leak structurally, derive the cap from the stream budget, and reconcile it at the connect sweep. Amendment 0040.1: the maintainer identified the 36,749-directory outlier as archival and renamed its .git to _git, so the remaining 14 repos total 858 directories — 0.16 % of the watch budget; F7 was the only argument for a count cap and is now hypothetical, which makes the decision safer, not weaker. Three phases, one commit each; only phase 2 changes what the maintainer sees — the cap is now max(1, maxConcurrentStreams - 2) keyed per (session, host), so a healthy tab watches 6 repos rather than 2 shared across every tab. 12 mutations, all killed. Four deviations, including assertion_strength_scan (MADR 0029) catching the new tests asserting on generated shell text without running it. A rebuild is required — the reported build predates all of it. Amendment 0040.2 is partly corrected by 0041 (F3/F4): the count that triggered the revert reads ~3x high and the per-repo tallies were registry files, not watchers — the revert stands for 0041 F5's reason instead
0041 The client cannot kill the watcher it started proposed plan completeall 5 phases shipped 2026-09-09, one commit each; 3897 -> 3926 tests, 27 mutations killed, 0 survived. Answers the four questions left open when MADR 0040's phases 2 and 3 were reverted. The revert was right for a different reason than the one recorded. The count that triggered it, pgrep -af mg-watch | wc -l, reads about three times high — six matches for two live watchers, because each watcher is two matching shells (the lease loop plus the backgrounded subshell that runs inotifywait) and the query matches itself — and the per-repo tallies were registry files, not concurrent watchers: a repository captured live under the reverted build carries two token pairs and one live process, and the dead one's heartbeat reads fresh to any -mmin -5 test for four and a half minutes after its watcher is gone. What phase 2 really did was re-key the ceiling from host to (session, host), taking the host-wide bound from 2 to as much as 8 x 6 and leaving no host-wide bound at all — handed to a lease whose reclaim latency is up to six minutes. Underneath it all: the client cannot kill the watcher it started. session.kill(TERM) is an RFC 4254 "signal" request OpenSSH never implemented (dartssh2 says so itself), teardown does no host-side cleanup at all, and a probe on the reporting host reproduced the exact lease-loop shape and watched the whole tree survive a SIGKILLed client — with and without the HUP trap. The remedy is proven on the same host: a stdin-EOF watchdog on a saved descriptor removes the tree in under 5 s (and the POSIX trap that a bare ( cat; kill ) & reads /dev/null, which would ship as 0027 deviation (b) all over again, is recorded). Also measured: the ceiling's unit matches no scarce resource (0.17 % of the host's inotify watches for every repo at once, against an SSH channel budget already enforced a layer down); three of the four --exclude flags have never been in force — inotify-tools takes only the last, warns about it on the very stderr the app reads, and it was reproduced in a scratch dir; --exclude never prevents a watch being established, so 701 directories = 701 descriptors, 37.7 % of them on .git/objects, fixable with @-paths; and git fsmonitor--daemon is not supported on Linux (2.48.1), closing the obvious route. Proposes four ordered phases — make teardown reach the host, one watcher per repo via a host-side mkdir lock, re-derive the ceiling host-wide (phase 3's reconciliation not re-landed: it corrects the wrong direction at the wrong time), then fix the watch surface. Three deviations, each prompted on and recorded before it was executed: a fifth test file pinning the removed -t; the widened early-exit read costing every arm 250 ms of real time, which eleven tests settling with pumpEventQueue() could not see; and the test that asserted all four dead --exclude flags were present — they were, which is the record's own point made in a test. The sabotage round's value was again the two that did NOT confirm a fix: one survivor that was a test reaching the un-leased argv no live arm takes, and two anchors silently un-armed by later phases (DID-NOT-APPLY, not a pass — MADR 0039 D9). Two probes failed before proving anything and became the tests they now guard: the /dev/null stdin trap, and a double whose cancel() hung on an unsubscribed controller. Host acceptance is the maintainer's step — it needs a rebuild
0042 The macOS build mutates its own inputs accepted plan completeall 4 phases shipped 2026-09-09, one commit each, verified with two real --unsigned builds (one a negative control in an isolated worktree). Two build failures on a second machine, against a commit that builds cleanly here and whose push contained no file outside lib/, test/, docs/, tool/. The headline is not the error, it is the history: build_macos.sh --unsigned rewrites macos/Runner/Release.entitlements in place — the very file CODE_SIGN_ENTITLEMENTS points at (project.pbxproj:676) — and the stripped result has been committed three times (0789fee, a968b81, a008925), twice inside commits about a UI change and tag management, each repaired by hand afterwards. The .bak has been committed too, and .gitignore says so in its own comment. AGENTS.md already forbids it in prose; F3 is what prose produced. Also found: a second failed run does cp "$ENT" "$ENT.bak" unconditionally, so it destroys the good backup the first run made and then silently builds an unsandboxed app. The reported "modified during the build" error could NOT be reproduced here and the record says so — three candidate mechanisms are listed and none confirmed — but the decision does not depend on which: all three are downstream of mutating a signed input. Chooses two tracked entitlements files selected by a build-setting variable with an optional #include? override, deleting the strip, the cp, the .bak and the trap outright, over hardening the trap (leaves the mutation), generating the file (ships what no one reviewed), a separate Xcode configuration (fights Flutter's Debug/Profile/Release mapping) and Apple's own suggested CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION=YES — which the same error message warns can sign the product with incorrect entitlements, i.e. removes the check that noticed. Carries the invariant as a test with three historical failures to prove itself against. Also folds in the earlier failure on the same laptop: the Stamp Version From Git phase is alwaysOutOfDate with inputPaths = () and outputPaths = (), so nothing orders it after Info.plist processing. Negative control reproduced exactly: mid-build in an isolated git worktree pinned to the pre-fix commit, Release.entitlements read 0 of its two keys and the existing guard failed both assertions simultaneously, live. The mirror against the fixed tree, mid the SAME kind of real build: Local.xcconfig correctly selected the unsigned pair, git status --short macos/ stayed empty for the whole build, and the guard passed throughout. The built app's embedded entitlements (codesign -d --entitlements :-) matched Release-unsigned.entitlements exactly, plus get-task-allow which Xcode adds itself for ad-hoc signing — Release.entitlements was never opened for writing. The extended guard was sabotage-tested four ways against scratch copies (added key, removed key, sandbox leaking back in, a shared value flipped) and caught all four. The missing-plist guard was proven against the exact input that reproduces the original report's text verbatim, and a real build's stamped version (1.6.2.31) was checked against git describe + git rev-list --count computed independently — exact match. What is not claimed: the ordering fix could not be confirmed to address the actual reported failure, since it never reproduced on this machine either — that can only be settled on the machine that saw it
0043 A watcher refused by its own session accepted plan completeall 4 phases shipped 2026-09-09, one commit each; 3926 -> 3942 tests, 10 + 27 mutations killed, 0 survived. Reported from a running build hours after MADR 0041 shipped: a repository degrading to polling with arm unavailable: heldByAnother — 0041 phase 3's host-side mkdir lock refusing a second watcher. There was no second session. One remote tab (confirmed by the maintainer and by a host census finding exactly one triple-client SSH group and one live watcher); the client armed one repository twice and its own lock told it no. The proof is four words that are missing: degradationSummary printed armed(arm succeeded) -> armFailed(held by another watcher) adjacent, and every re-arm path records a transition first (rearmed, restartScheduled, recoveryAttempted) while stop() records stopped as its first statement — so no stopped means the first watcher was still running when the second was refused. Reproduced deterministically: one RemoteWatchService, one repoPath, two concurrent watch() calls → 2 watchers, 2 tokens, 2 slots, 2 armed records, no guard of any kind; startChain serialises arms within a watchLifecycle instance and cannot see across them. A second, independently demonstrated route: a Riverpod rebuild arms and COMPLETES generation 2 inside generation 1's teardown window. Cost of each spurious refusal: heldByAnother is deliberately excluded from the slot-release wake, so the repo waits out the 3-minute recovery timer polling at 0040 F5's ~48 git processes/minute — four times in the reported window. Also measured: the lock releases in under a second after a clean channel close, so 0041 F11's watchdog works for graceful closes and not just the SIGKILL its original probe used — and a first measurement claiming 19 seconds was a bug in the probe's own shell quoting, corrected in F5 rather than quietly dropped. Chooses a per-(host, gitDir) arm-serialisation gate plus a teardown that awaits its own lock release — neither works alone (the gate would wait on a future that completes while the remote still runs; the guarantee would exist with nothing awaiting it) — over retry-after-delay (a timing guess for an ordering problem), token-lineage self-recognition (fixes the symptom, leaves the duplicate arm), and provider-layer dedupe (guards above the invariant, not at it). Not established: what created the second subscription in a single-tab session — the fix closes the class rather than the one path taken. Planning corrected the record's own decision. The chosen fix had been "serialise arms against in-flight teardowns" — which closes the rebuild route and does nothing for the reported incident, because F1 establishes the first watcher was still RUNNING, not tearing down, so there is no teardown for a queue to wait on. It would have shipped, passed its own tests, and left the symptom exactly as reported. Re-checking the second rebuild route (a dependency change rather than invalidate()) found it also orders dispose-before-create, so no Riverpod rebuild can produce F1's record pair — narrowing the open question to genuinely concurrent subscriptions. The decision now reads as sharing: one watcher per repo path per service, later subscribers attached to it with the last event replayed, refcounted via StreamController.broadcast's own onListen/onCancel rather than hand-rolled — with the teardown gate demoted to covering only the seam where the last subscriber leaves and a new one arrives. Deliberately per-service, not process-wide: two tabs are two connections and the host lock is correct for them. Four phases; the host check that matters most is the one confirming the cross-session refusal still works, so the fix cannot be said to have disabled the feature it protects. Phase 1's first test was run against the pre-fix tree in an isolated worktree and reported two tokens for one path — the defect on demand — before being relied on. The sabotage round's value was again the two results that were not kills: one no-op mutation of my own writing (it created a lifecycle object without listening, so armed nothing — the mutation was wrong, not the test), and one real gap where the teardown gate had no test at all because every existing test settled between cancel and re-subscribe, leaving the feature unguarded with a green suite; closed with an ordering test asserting [arm, teardown, arm]. Plus five 0041 anchors un-armed by this work's renames, reported as DID-NOT-APPLY rather than passing — the third time this session that running the whole catalogue at a boundary caught that. Not confirmed: the reported symptom on the maintainer's machine, and the cross-session case end-to-end (two tabs, one repo, one watcher, one honest refusal) — the mechanism is unit-tested but the live behaviour needs a rebuild there; and MADR F9's trigger is still unnamed, so the record's open question stands rather than being quietly retired. Amended 2026-09-10 (0043.1): MADR 0044's live verification found the sharing layer could orphan a watcher — a subscriber leaving while a build was still deferred cleared the pending teardown, and the deferred build then ran on top of a newer one. Reproduced against the committed tree (live=1 after every subscriber left, against a clean control's live=0); attach and detach are being serialized onto one chain under 0044 PLAN deviation (c). Amendment 0043.2: the serialized chain was withdrawn before commit (it dropped new parameters on rebuilds); mechanism superseded by MADR 0045, accepted and executed 2026-09-11
0044 The watcher follows the active tab accepted plan complete Asked why the watcher follows the active tab instead of watching every open remote tab concurrently, using "stateful threads". Answered from a live two-tab session against the reporting host, running build 890bca1 (v1.6.3 — 0041 and 0042 in, 0043 not). The tab model is real and the mechanism is one widget key: KeyedSubtree(key: ValueKey(activeId)) in tabs_host.dart means exactly one AppShell exists, so a background tab's repoWatchProvider (autoDispose) has no listener and its watcher is gone. Measured: two tabs, six sshd sessions, one watcher, 438 descriptors — and the background tab ran git=0 sustained, not even polling. Its whole cost is three idle sshd sessions, which makes any concurrent-watching design a move from ~zero to some cost rather than a saving. The literal proposal is rejected on measurement: statusProvider, repoSnapshotProvider and refsProvider are autoDispose too, so a background watcher's ticks land on nothing and the data refetches on return regardless — while _withoutIgnoredPaths spends a host round trip per tick filtering events nobody will read. "Threads" is a category error: Dart has isolates, not threads; the watcher is I/O-bound (remote execution measured at 2 ms, the rest is socket wait); six concurrent watchers are already permitted. What is missing is lifetime, not parallelism — and window_manager_bridge already holds a non-widget container.listen on repoWatchProvider for pop-outs, so the pattern exists. The app also already implements the proposed design one level down: IndexedStack keeps pages mounted and _onWatchTick gates the work on widget.isActive while keeping the subscription. The finding worth acting on is one the question did not raise: an arm costs ~400 ms — three round trips (median 49.4 ms each on an established channel) plus a fixed 250 ms early-exit read that a healthy arm never completes, so 63 % of a tab switch is a timeout this project chose, widened from bounded arms to every arm by 0042's plan deviation (b) without being measured. Recommends racing that read against first-stdout instead, which helps every arm including the first after connect. Host capacity is not the constraint and is not close: 14 repositories watched at once would take 857 of 524 288 descriptors (0.16 %) — which also shows 0042's @-exclusions cut 2 476 to 857, a 65 % reduction against the ~38 % measured on one repo. Flags that maxConcurrentWatchers derives a host-wide cap of 6 from one session's 8-channel budget while each tab dials its own client, which is harmlessly conservative today and would be wrong under concurrent watching. Not established: the tab-switch transition itself was never observed (the switch cost is composed from timings, not watched end to end), and a stranded .hb with no pid beside it is left deliberately unattributed — a VPN drop mid-arm fits, 0043's refusal cleanup is not in this build, and 0043 would not have covered the throwing path anyway. Planning corrected the record's own mechanism (amendment 0044.1): option E said "the first byte of stdout proves the watcher armed", and it does not — inotifywait -m --format %w%f and fswatch -0 write only event records, so a healthy watcher at rest is silent and the race would have replaced a fixed 250 ms wait with an unbounded one, passing a test that fed it a fake event. The plan races an explicit mg-watch: armed marker on stderr instead, emitted after both refusal exits and after the watcher process starts, so the ordering is structural rather than a matter of timing. Because the readiness listener must attach to stderr before the race, _incumbentToken's second 250 ms stderr.join() is deleted and the refusal path gets faster too. Honest revision of the headline: ~400 ms to ~200 ms, a 50 % cut, not 63 %. Deviation (c), 2026-09-10: phase 4's host verification found MADR 0043's sharing layer orphaning a watcher — a backgrounded tab's watcher kept its lock and lease for over thirty minutes. The fix (serialize attach and detach, amendment 0043.1) is step 4.7; 4.3 is blocked until it is built. Deviation (c) revised: step 4.7 superseded; the watcher fix is re-planned under MADR 0045 (proposed); 4.3 waits on that plan. Closed 2026-09-11: 4.3 was measured under MADR 0045's plan step 7.6 on a live host — ten tab switches, heartbeat-to-lock median 228 ms against the 250 ms acceptance (the full switch, teardown to new lock, is a median 850 ms, most of it the tab rebuild ahead of the arm), so acceptance criterion 6 is met and the plan is complete
0045 One owner per watcher concern accepted plan completephases 0–7 landed (6c25baa, 1d71f14, 6defce4, d77e34a, 360b846, 806e90e, 9dc567d, 4ef6d73, 9216938); phase 7 verified live on three hosts, 2026-09-11: one watcher per armed repository and no arm marker in any registry file, a disconnect now releasing every host claim before the transport closes — found on the host, where a disconnect had stranded each watcher's heartbeat for at least the five-minute stale age (plan deviation (t), amendment 0045.4) — a foreign lock still refused with restarts spent 0 and the refusal naming its watcher, a linked worktree locked under its resolved git dir, and a tab-switch arm median of 228 ms against MADR 0044's 250 ms acceptance; a detached window on a linked worktree runs no command, which is window routing rather than watching and goes to its own record (plan deviation (v)); closing the criteria found the structural guard one name short and a 7.2 clause never checked, both fixed and checked live (deviations (w), (x)); every transition record names its watcher, the watcher logic tests run in fake time behind structural guards that were each seen to fail, and catalogue 0040 applies again after phase 3 had silently disarmed it (plan deviations (o)–(s)); repoWatchProvider is a facade over a watcherProvider(WatchTarget) family, so a reconnect's invalidation keeps each running watcher and a changed target replaces it — which needed the facade to close its own subscription, since Riverpod keeps a rebuilt stream provider's old ones until its stream is done (amendment 0045.3, plan deviations (m), (n)); one single-mailbox WatchEngine replaces watchLifecycle and drops stale attempt results by comparison, and a refusal arriving after cancel no longer starts two timers for a stream that is gone (amendment 0045.2, plan deviations (h)–(l)); remote and local watchers arm through a WatchSource seam, a linked worktree is locked by its resolved git dir, and no test calls pkill (plan deviations (f), (g)); the mutation harness refuses kills no test observed and has --check (plan deviations (a)–(c)); admission replaces sharing and the process-wide statics, and leaving repoWatchProvider now tears a quiet repository's watcher down (amendment 0045.1) An architectural review of the whole watcher stack, directed by the maintainer after two fixes in one day were each found wrong: the orphaned watcher (0043.1) and a serialized chain that silently dropped new watch parameters on provider rebuilds (Actual: ['arm']; arms=[recursive] against [recursive, bounded] on the unmodified code). The corner, stated plainly: sharing solved an exclusion problem. Riverpod already gives one provider instance per repository per container, and the watch services have exactly two production callers, both inside repoWatchProvider. Every fix since MADR 0043 added state to a layer the product did not need. Seven concerns (demand, identity, admission, the source process, health policy, output, observability) are spread over five owners: a 503-line arm closure doing eight jobs, three hand-rolled sequencers with a defect at each seam (0026 H1, 0043 F2/F3, 0043.1, today), process-wide statics for per-session resources, timing constants duplicated in four signatures and coupled only in prose, about 100 real-time waits across 22 watcher test files, diagnostics with no watcher identity, and a host lock keyed by '$repoPath/.git' — a file, not a directory, for linked worktrees and submodules. Proposes one owner per concern: WatchTarget value keys in Riverpod (a parameter change is a new key; an unchanged rebuild keeps its engine), a WatchAdmission with an injected host budget and a session exclusion keyed by the lock directory, a single-mailbox WatchEngine that discards stale attempt results by comparison, a WatchSource seam with the arm closure decomposed into small units, one WatchTimings value with its couplings asserted, and WatcherId on every record. The host protocol is unchanged. Migration is phased with admission first, because the orphan is live in the running build
0046 Restore the stale workspace mutation entries in the 0032 and 0036 catalogues — no MADR of its own; maintenance of contracts belonging to 0032 and 0036, opened by 0045's plan deviation (c) plan complete — 15 entries across the two catalogues had gone dark since 2026-09-08 (DID NOT APPLY, found by tool/mutate.py --check); re-anchored to matching code with no lib//test/ change, entry 1 retired as superseded by an existing 0038 entry. Catalogue annotations landed 2026-09-13 (dd0c29a, 9a39c05); this plan's own closing bookkeeping (status, this row) and Phase 2's verification were run 2026-09-17: --check 60/60 sound, 0032 catalogue 31 killed/0 survived, 0036 catalogue 29 killed/0 survived, flutter analyze clean
0047 A detached window on a linked worktree runs no command accepted plan complete Found by MADR 0045's phase 7 step 7.5 (plan deviation (v)) and deliberately left out of it: window routing, not watching. A detached window opened on a remote linked worktree runs nothing — every provider fails RELAY_DOWN ("the window's tab has closed") while the tab it was opened from is open, connected, and watching that same worktree. The window is pinned to (tab, worktree path) and the child asks for its own path, but _execContainerFor routes only to a session that OWNS the path — conn.repoPath or the saved connection's repoPaths, which never lists a worktree — and then to a tab whose repoPath equals it, of which there is none, so the third branch (RELAY_DOWN) takes every command. The message is untrue here: the tab has not closed. The same bridge already watches that path through the same pinned container (_subscribeTickrepoWatchProvider(worktree)), confirmed live on the host in 7.5, so watching and executing disagree about one (tab, path) pairing. Proposes routing a repo-bound window's own pinned path to its pinned tab while that tab is connected, leaving History's ownership rule — and the mid-switch lag its test pins — untouched, over asking git to resolve the path's common git dir (a host round trip on the first command, failing closed when the host blips), adding the worktree to the connection's persisted repoPaths (routing fixed by editing the user's saved connections), and teaching the connection layer about worktrees (right domain, but freshness-bound and silent about submodules). Keeps a4c03d7's guarantee that no command reaches a host that does not own its path. Executed and confirmed live 2026-09-17 (6234f76) — _execContainerFor gained a nullable handle parameter and one short-circuit for a repo-bound window's own pin, plus three new tests in test/window_bridge_follow_active_test.dart (seen to fail against the unmodified tree first). Repeating MADR 0045 step 7.5 on the maintainer's machine: the detached window populated, with a single watcher under the worktree's resolved git dir, unchanged
0048 Preferred editor and terminal as settings accepted plan complete Follows 11f9ed7, which fixed "Open file" opening nothing: FileActions.openFiles ran open -a 'Visual Studio Code' (pinned by 5e93607, PR #4) on a Mac without VS Code, so open exited non-zero and every caller discarded the result — no window, no error, no log line. That fix restored the per-type default application, added an open -t fallback and made failures loud; this record answers what it left open. The two halves are not symmetrical: Launch Services holds a default handler per content type, which plain open <path> already consults, so for files a setting is an override of something that now works — but macOS has no default-terminal role, so open -a 'Terminal' has nothing to defer to and an iTerm/Warp/Ghostty user has no way to be heard. That call also still ignores open's exit status, the same discarded-status bug, surviving outside FileActions. Chooses a setting storing the bundle identifier (open -b) with the display name kept only for the UI — measured: the editor shown as "Cursor" is com.todesktop.230313mzl4w4u92, so a name predicts nothing — over an absolute .app path (breaks silently when the app moves), the display name (open -a <name> is precisely what failed here), no settings at all (leaves the terminal imposed), and a generic role-keyed map (hides that the two roles have different defaults and fallbacks). Unset behaves byte-for-byte as today, and the app picker reads CFBundleIdentifier from the bundle — no native code, file_selector is already a dependency. Amendment 0048.1 (2026-09-12): the terminal half is withdrawn. A live check found open -b <id> <dir> is not a terminal-wide contract — WezTerm rejects a trailing directory argument and exits, silently, since open reports dispatch, not survival. Terminal.app is now the permanent, unconfigurable terminal opener; only the editor preference ships. Plan executed through Phase 6 (9eda3e6, b98b5ef, 39eb4b8, b79eb4c, 99fab68, aed271c), with three same-day deviations found and fixed along the way: (a) a flaky watch-source test that was silently coin-flipping every gate in this plan, root-caused (a broadcast stream dropping events, not a timeout) and fixed; (b) every launch path read the chosen editor before its tab's settings had loaded from disk, so an early click always fell through to the system default — fixed with a readiness future every launch now awaits, guarded structurally since the race cannot be reproduced through the UI; (c) the first disk load discarded every stored setting, not just the new ones, whenever any other setting was written before it resolved — fixed by replacing a sticky all-or-nothing abort with a genuinely-in-flight check that reloads once the write lands. Closed 2026-09-17: step 5.4(b) and 5.4(d) — a chosen editor opens a file whose system default is something else, and a chosen-then-deleted editor falls back and says so once — confirmed by the maintainer on the real machine (an earlier "pass" had been retracted after (b) showed it could have been the system default all along); acceptance criterion 11 is met
0049 A worktree row paints into the next pane accepted plan complete Reported from the running app and confirmed on screen: with a long-named worktree, the Worktrees row's name and branch chip cross the divider and paint over the detail pane, while the path line beneath them ellipsizes correctly — the clue that parts of the row are bounded and parts are not. Reproduced away from the tree as a widget test: A RenderFlex overflowed by 944 pixels on the right. The outer Expanded bounds the Column (hence the correct path line); the inner Row holding the name and up to five chips has no flexible child, so it lays out at intrinsic width, and Flex paints the excess outside its bounds (clipBehavior defaults to Clip.none) — with no debug stripes in a release build, it reads as spilling text rather than an error. Two independent causes, each measured alone: flexing the name still overflowed by 147 px, bounding the chip alone by 551 px, both together by nothing. LabelChip has no maximum width and no ellipsis at all, though History solved this exact class (a chip that caps its own width and a strip that collapses extras into +N) and Branches already flexes its name. The pane compounds it: ResizablePanePair sizes the navigator with a SizedBox, which bounds layout and not painting, so any overflow in any navigator lands on the canvas — and the pane is resizable down to 240 pt, so no width is safe. No test caught it because the worktree fixtures are named app and app-feature; a RenderFlex overflow throws in widget tests, which is exactly how History guards its chips. Chooses bounding both the name and the shared chip plus clipping the navigator pane — the first fixes the cause for four features at once, the second downgrades every future overflow from painting over a neighbouring pane to being cut at the divider — over name-only (measurably insufficient), the capped +N strip (deferred: it caps how many chips appear, not how wide one is), and wrapping chips (variable row heights break ensureRowVisible and keyboard nav). Executed in five phases. Bounding LabelChip fixed 97% of the Branches row too, untouched (181 px → 111 px), which is the four-features-at-once consequence confirmed; History's cap-and-collapse strip was extracted into a shared ChipStrip that Worktrees, Branches and History now share. Four deviations, each recorded: (a) the name measured too narrow, so the capped +N strip deferred by this record was adopted after all — and adopting History's shape literally made the name worse (46 pt against 97 pt at 240 pt), which is why chipsMayShrink is an explicit caller choice rather than inferred; (b) that extraction silently dropped every visible chip's tooltip, since a strip tooltips only what it hides, so the tooltip moved inside LabelChip where the bound already lives; (c) F8's blast radius was wrong — four of the "seven" surfaces were a substring match on ForgeLabelChip/MiniLabelChip, which are unbounded but sit in Wraps and so do not overflow, measured not assumed; (d) the Branches row and the shared CollapsibleSectionHeader both still overflowed, the latter outside the plan's file list. Capping alone could not bound the branch row — one chip may be 160 pt, so even maxVisible: 1 overflowed by 33 px. Stash, switcher and the forge chips were measured clean and left alone, which answers this record's "Not established". Catalogue 0049-row-bounding.json: 14 killed, 0 survived, after a first run whose 4 survivors exposed four guarantees no test covered. Full suite +4113, 48 goldens unshifted
0050 Providers use ref after an await accepted plan complete Reported from the Output pane: opening a worktree tab and leaving it logs Cannot use the Ref of FutureProvider<RepositoryWorkspacePrefs>/<RepoNode> after it has been disposed. Both are autoDispose.family providers keyed by repository path that use ref after their first awaitrepositoryWorkspacePrefsProvider watches appSettingsProvider after awaiting the UI identity, repoStructureProvider watches gitServiceProvider after awaiting status — and both awaits are slow (an identity read, an SSH status round trip), so leaving the tab disposes the family entry mid-build and the continuation runs on a dead Ref. Riverpod's message names two cases; only one happens, and it was measured against the real provider with its dependencies held open: rebuilt while pending, a live listener saw loading → data with zero failures reported — no pane damage, pane widths do not reset, the tree does not empty; disposed while pending reported exactly one failure, the verbatim reported message, with no listener left to see it. So the cost is not a broken pane but a line in the very channel MADR 0034 added so real provider failures stop being silent. The class is 28 provider bodies that touch ref after an await. The repair is not mainly a mounted check: both offending calls are ref.watch, Riverpod wants dependencies registered synchronously, and neither needs the awaited value, so they hoist above the await; ref.mounted (already used 55× in this file, including 12 lines from one of the offenders) stays for reads that genuinely must follow a gap. Chooses hoisting plus a scan test in the style of provider_retry_policy_test.dart — which already fails on an unannotated async provider — over hoisting alone (26 sites of the same shape remain), a hand sweep (decays at the 29th), blanket mounted guards (hides late dependency registration rather than removing it), and filtering the message out of the log (suppresses genuine disposal bugs too). Executed through Phase 4 (ec8e916, ef4c1da, 7d3267e, 5e6672b, 1c3e1ff): a scan test now enforces the rule repo-wide and found the authoritative list — 26 real sites (23 in app_providers.dart, plus 3 more in lib/core/forge/branch_forge_status.dart a hand-written glob had missed), each hoisted, guarded with ref.mounted, or allow-listed with a reason. A same-day baseline deviation was fixed along the way: a pre-existing activityIdle test flaked under full-suite scheduling contention (400ms budget for a 30ms pulse interval, measured insufficient — widened to 2s, no production code changed). Catalogue 0050: 9 killed, 0 survived. Closed 2026-09-17: the maintainer confirmed step 6 on the real machine — opening and quickly leaving a remote worktree tab logs nothing — after an unrelated Xcode 27/CoreSimulator version mismatch blocking the build was fixed (sudo xcodebuild -runFirstLaunch)
0051 Branches guided recovery for out-of-sync repositories accepted plan in-progress Multi-host development (Windows/Linux/macOS clones of the same repo) produces diverged branches, missing upstreams, branches never fetched to a clone, and stale/force-pushed remote tracking — the Branches tab reports these (an ahead/behind count, a gone badge) but offers no in-app resolution, and the reported "Set upstream…" menu item is confirmed broken for a never-pushed branch: git branch --set-upstream-to requires the remote-tracking ref to already exist, and the actual fix (git push -u, "Publish") lives in an untethered, different menu action. GitService already has nearly everything needed — merge/rebase(rebaseOnto is already a plain non-interactive rebase)/cherry-pick/revert/am with full abort/continue, journaled reset, the full stash suite, reflog (backing an existing single-clone Recovery sheet), and a pending-op banner on the Status tab — but none of it is tied to a named "why can't I sync" state in Branches, and the "More" menu (to be renamed Advanced) is missing several items the right-click menu already has. Proposes a per-branch BranchSyncState (synced/ahead/behind/diverged/unrelated-histories/no-upstream/stale-tracking), one new primitive (merge-base, plus a --allow-unrelated-histories merge option), a "Reconcile…" dialog offering named, risk-labeled actions (rebase/merge/reset — reset routed through the existing undo journal, not a new backup mechanism) instead of a bare count, a bulk stale-branch cleanup after Fetch & Prune, and a link from Branches to the Status tab's existing interrupted-operation banner — over fixing only the reported bug (leaves every other scenario unresolved), menu-parity-plus-explanations alone (diagnosis without a cure), and adding proactive background conflict scanning now (a separate, heavier feature that doesn't answer the specific out-of-sync question asked). Amendment 0051.1 (2026-09-17) corrected three scope claims found while drafting the plan: merge-base/unrelated-histories detection already exists (no new OID-returning method needed, only a small haveCommonAncestor extraction), the Reconcile-style guardrail dialog already exists in miniature (_dropOnCurrent/chooseAction<T>, drag-and-drop only), and the upstream fix is a validation gap, not a menu-visibility one. Executed through Phase 8 (789ef9f, 2cf1b0d, 89f7c28, 0888f72, 8e6f84f, 6e5d2af, 9ece179). Set upstream now validates its target and names Publish. The More menu is renamed Advanced and matches the context menu's row actions. Rows show "Not published"/"Diverged" chips, and the detail pane explains each state, including unrelated histories. Branches also gains: a confirmed allow-unrelated-histories merge; Reconcile… (Merge/Rebase/Reset) on a diverged current branch; a stale-branch cleanup after Fetch & Prune, with one force-delete follow-up for unmerged branches; and Status's interrupted-operation banner, full-width. Nine deviations were resolved with the maintainer mid-execution. Amendment 0051.2 records the four that contradict this record: rows show only the coarse state, since resolving unrelated histories per row broke the Browse command budget; the banner replaces a per-row indicator, since mid-rebase no row is current; menu parity is per row action; and the chooser is Rebase's confirmation. InlineActionButton labels now ellipsize, with all 48 goldens unchanged. Full suite 4159 passed (from 4122). Residual: three maintainer-only checks on real diverged clones (Reconcile's three paths, first Publish, Fetch & Prune cleanup) (the argv-level --allow-unrelated-histories test was added 2026-09-18, e90433b)
0052 Sidebar info card: Location row, plain Connections button, one repository name accepted plan in-progress The sidebar's connections-manager button carries state in its label (the SSH host, or Local), so it neither names what it opens nor matches the passive Repository row above it. Proposes a Location row in the info card under Repository (the SSH host, or This Mac for local; user@host:port tooltip) and a fixed Connections label on the button, over a two-line button, a tooltip only, or making the Location row itself the button. The tab title, window title, Repository row and status bar all show the same name, the tab alias when set, else the directory, from one repositoryDisplayNameProvider, replacing seven hand-written copies. Repository labels were added and withdrawn on review. Executed (f7d7b9d, 54508dc, 5124375, 10dc081): suite 4182 (+22), every new test seen failing first, and 12 mutations killed in a scratch worktree. Amendment 0052.1 (83c55ff): the tab, status bar and Location row share one location glyph via sessionLocationIcon, a globe for remote and a folder for local; Amendment 0052.2 (42d409e) extends it to the connections manager and the landing page's recent rows. Residual: the maintainer's manual check in the built app.
0053 In-app Help, README and build guide currency refresh accepted plan complete Help Book v2.0, the README and docs/guides/build-macos.md had drifted from the app: 21 verified falsehoods (W1–W21), 19 incomplete topics, 12 uncovered areas, 4 Help-machinery defects, and a build guide still describing pre-0042 entitlement stripping. Engineering phases 0–11 executed (2026-09-18). Help v3.0 has 34 topics in 7 categories, including a new Troubleshooting category. test/help_book_json_test.dart adds label anchors (quoted labels must exist in lib/), menu coverage, a W1–W21 falsehood ban and a renderer-schema guard; all 8 entries of tool/mutations/0053-help-book.json were KILLED. Help search now covers bullet items and code in both views, and HelpDataModelTests.swift joins RunnerTests and passes (7/7). The README and build guide were rewritten, and build_macos.sh's unknown-option crash was fixed. Four deviations were resolved with the maintainer: D1, the push-failure message is unreachable (Amendment 0053.1); D2, 0010's false Code fact; D3, unsigned Debug/Profile test entitlements via MG_DEBUG_ENTITLEMENTS (Amendment 0053.2); D4, two mutation rows retargeted. Full suite +4196 ~3. Maintainer checks done 2026-09-19: Help read on a built .app (closing 0010 Phase 7), and the build guide followed from a fresh clone and found accurate.
0054 Docs link checker and standard layout migration accepted plan complete The records sat flat in docs/, against the standard AGENTS.md already mandated, and nothing checked any of the ~600 relative links and docs/ path mentions between them. Checker first: tool/records.dart (six rules: links, anchors, docs/ path mentions, numbering, frontmatter, layout), run by test/docs_records_test.dart in flutter test, and dart run tool/records.dart next for numbering. It was seen to fail three ways: 23 fixture cases, 18 of 18 mutations killed (the first run's one survivor was redundant slug code, deleted), and the real move, which the checker failed with 269 findings before the links were repaired. One move commit (fea3dcf): 111 git mvs, with links repaired by resolution and 440 verified by target. The seven unnumbered documents became 0055–0061 (former-path: keeps the old names findable), and the build guide went to guides/. architecture.md written new from the code, with a claim-by-claim evidence table in the plan. 0056 (the old architecture plan) is kept as history, with three stale §0.1 statements annotated. This index is rebuilt as the table of contents, with an "I want to…" matrix, and the checker keeps it complete. One deviation (D1: a plan step expected a finding that the accepted blockquote exemption rules out). Deferred: the verified: backfill for 27 PLANs, by the maintainer's decision
0055 Post-review action plan — no MADR of its own; formerly ACTION_PLAN.md plan partial — the first review's P0–P3 backlog; 0007-MADR verified 46 of ~51 items
0056 Architecture and feature-parity plan — no MADR of its own; formerly ARCHITECTURE_PLAN.md plan partial — historical; superseded as a description by architecture.md, with three stale §0.1 statements annotated in place
0061 Remaining test coverage — no MADR of its own; formerly TEST_COVERAGE_PLAN.md plan partial — 12 of the 14 test files it names exist (checked by existence only)

0011 and 0012 each carry two unrelated records. CLAUDE.md forbids renumbering an existing file, so both keep the number and each carries a note naming its twin. Cite records by full filename, never by number alone.

0016 has a plan but no MADR of its own: it is the second tranche of 0015-MADR, and took the next free number because 0015-PLAN-* was taken.

0055, 0056 and 0061 are older than their numbers: they were unnumbered documents in docs/ until 0054 gave each the next free number. Each carries its old path in a former-path: key, so a search for the old name still finds it.

Reports

Audits and investigations that record what was found and decide nothing.

# Report Status
0005 UX baseline for the task-centered adaptive workspace — formerly 0005-UX-BASELINE-… partial
0057 File-view engine assessment — formerly viewer_engine_findings.md partial — residual L7 per 0004-MADR
0058 Window sizing assessment — formerly window_sizing_proposal.md partial — largely implemented
0059 Memory and performance audit — formerly memory_audit.md partial — Tier 1–2 fixes done
0060 Drag-and-drop engine feasibility — formerly DRAG_AND_DROP_ENGINE.md partial — A–E shipped

What "verified" means here

2026-09-19 (0054 follow-up): the 27 plans that had no verified: date at all (0022–0045, 0049, 0052, 0053) were each checked against the code at d67953c. Every one was confirmed, with shipped deliverables located and later supersessions traced, and each now carries verified: 2026-09-19. tool/records.dart now fails a record with no verified:, so the gap cannot reopen.

Statuses were last audited on 2026-09-03 against the tree, in the manner 0007 established: each claim checked against code rather than taken from the document. Three plans were found asserting the opposite of reality — 0006-PLAN ("ready to execute"), 0008-PLAN and 0009-PLAN ("proposed … no code written") — while their work was demonstrably shipped. Those lines are corrected in place and marked, not deleted.

The 2026-09-03 pass (0022) corrected one more: 0018 stood at proposed while its decision was live in the code, so the index was telling readers a fixed bug class was still open. Both its files are now accepted/executed, and the plan carries a late-recorded deviation — what shipped is an inline readiness gate, not the ReadinessGatedExecutor decorator its Phase 1c specified.

The 2026-09-04 pass (0024) corrected a second, wider one: 0022-PLAN and 0023-PLAN both record a baseline of 48 failing goldens and 2 analyzer warnings as a known pre-existing set. Neither was real — both were artifacts of running a Flutter that did not match build_macos.sh's pin, which also explains the pubspec.lock churn in bd93c18/21721ef. The pin is now 3.47.2, the suite is fully green, and both plans carry a correction block at the top rather than a rewrite.

Documents also record their own residuals. Where a status and the body disagree — a residual the status does not reflect — the body wins; it is more specific.