docs: absorb #236 and #237 --- the GUI arc anchor moves to d038f71

Docs only. No `src/`, no `pmacs-gpu/`, no 1a implementation.

**The recovery procedure was RE-RUN, not SHA-swapped, and that is what
licenses the rest of this commit.** Every documented step, from an empty
directory, on git 2.55.0: clone the canonical URL (which names it
`origin`, and `git remote get-url githubsucks` errors with `No such
remote`, exactly as the section anticipates); add the alias; fetch;
`git log -1 --oneline githubsucks/main` -> `d038f71`; confirm BOTH the
old floor `9a26ac8` and the new `d038f71` are ancestors, so advancing
the floor is valid rather than merely plausible; recover with the
three-argument `git worktree add <path> -b <local> githubsucks/<branch>`
form and confirm the upstream is actually set. The documented TRAP was
reproduced too: the two-argument form succeeds and leaves
`## HEAD (no branch)`, so that correction still holds on 2.55.0.

**This discharges a debt rather than inheriting one.** The previous
absorption advanced the base to `e67ad07` WITHOUT re-running the
procedure and recorded that as an outstanding obligation. It is settled
here, by the run above.

Canonical base, recovery floor, the `git log` threshold and the
last-exercised record all move to `d038f71` together --- a floor that
accepts an older commit than the base it declares passes on a tree the
rest of the file does not describe.

**The GUI lane is REWRITTEN, not removed.** Rule 4 removes a lane when
its ARC is done; the arc is Stage 1 as a whole and **five slices
remain**. The lane now records 1-pre as merged, names 1a as next and
NOT STARTED, and keeps the fact that 1a (v24 `TextInput`) and 1e (v25
`OpenTarget`) are the two protocol-bearing slices and are serialized.
Open-PR and branch-recovery wording for 1-pre is gone --- a lane that
still says "recover with `git checkout`" for a merged branch is how a
reader ends up on a stale tip.

The handoff anchor absorbs both merges, and §1 gains the arc bullet the
anchor promises. Its durable facts are the ones that will bind future
slices rather than the diff: a route carries the DECISION and only the
body gives the EFFECT; a harness that re-implements what it tests
witnesses its own copy; winit's `KeyEvent` is non-constructible outside
winit and this will bind every GUI input slice; the sentinel is the
success condition and the timeout only an error ceiling; four test
defects that were all one shape --- witnessing an absence the harness
had manufactured; and `cargo metadata`, not intuition, decides which CI
job runs a member's unit tests.

**One judgement beyond the stated scope, flagged rather than slipped
in.** §1a's board had no GUI-arc row at all, which would have
contradicted the anchor two sections above it. A row is added at the
position `COHERENCE.md` §20 Q#GA5 gives it --- after Priority 1, running
alongside P2-P5, interacting only at the P2 start gate --- and the P2
row now names that gate. The section's own dateline is corrected in the
same breath: the table was written 2026-08-01 and claimed zero open PRs,
so it now says which row is current and which are as-of that date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-12 13:14:51 +02:00
parent d038f7144e
commit 502308d8e4
No known key found for this signature in database
2 changed files with 188 additions and 198 deletions

View File

@ -136,7 +136,11 @@ lesson, §1 for the two framings).
are identical on every machine. Remote names are otherwise
machine-local: `origin` may name this canonical URL, a release mirror,
or something else, and therefore has no authority by name alone.
- Canonical base at this snapshot: **`githubsucks/main` @ `e67ad07`** —
- Canonical base at this snapshot: **`githubsucks/main` @ `d038f71`** —
**GUI Stage 1-pre #237**, the input seam (`App::window_event` from 655
lines to four; no behaviour change, no wire change), atop `f8ad3e7`
**GUI arc Stage 0 #236**, the arc framing and its documentation sweep.
Beneath them `e67ad07`,
the file-watcher arc absorption, atop `122b8e8`
the file-watcher D3 **#235** (closes issue #233), atop `b867f64`
git integration Stage 1 **#227**, atop `ae84d58` the LSP file-watcher
@ -173,7 +177,7 @@ lesson, §1 for the two framings).
where they describe a stage as it landed** — only this
current-state paragraph tracks the live range.
**The recovery floor advances with the base**, so the check below
now requires **`e67ad07`** or newer; a tree at `db1bbe9` no longer
now requires **`d038f71`** or newer; a tree at `db1bbe9` no longer
passes — it would lack the entire QoL arc, which this file and the
handoff both describe as complete. That is deliberate — a check accepting an older commit than
the declared base passes on a tree the rest of this file does not
@ -213,28 +217,40 @@ git worktree list
git status --short --branch
```
The `git log` command must expose `e67ad07` — the base named above — or a
The `git log` command must expose `d038f71` — the base named above — or a
newer intentional main. Keep this threshold and the canonical-base line in
step: a recovery check that accepts an older commit than the base it
declares canonical will pass on a tree the rest of this file does not
describe.
If it does not, stop and repair the remote/fetch configuration.
**LAST EXERCISED AT `9a26ac8`, 2026-08-08** — re-run from an empty
directory rather than having its SHA swapped. **The advance to
`e67ad07` (2026-08-11) has NOT been re-exercised**, and by this
paragraph's own argument that is exactly when the exercise matters, so
it is recorded as an outstanding obligation rather than left to read as
though it had been done. The floor above and the check below are in
step at `e67ad07`; what is unverified is the *procedure* against that
base, not the SHA. That distinction is the
whole point of this paragraph: advancing the base is exactly when the
recovery commands are most likely to have rotted, and a swapped SHA
reads identically to a verified one. `git clone` the canonical URL, add
the `githubsucks` alias, `git fetch githubsucks --prune`, confirm
`9a26ac8` is an ancestor of `githubsucks/main`, and recover with the
three-argument `git worktree add <path> -b <local> githubsucks/<branch>`
form. All four steps ran clean.
**LAST EXERCISED AT `d038f71`, 2026-08-12**, from an empty directory
rather than by having its SHA swapped — **which also discharges the
outstanding obligation this paragraph carried at `e67ad07`**, where the
base advanced without the procedure being re-run. That debt is settled,
not inherited: the advance recorded here was exercised as part of making
it.
Every documented step ran, in order, on git **2.55.0**:
1. `git clone` the canonical URL into an empty directory. The clone
names it `origin`; **`git remote get-url githubsucks` errors with
`No such remote`**, exactly as the section above anticipates.
2. Add the alias, confirm the URL, `git fetch githubsucks --prune`.
3. `git log -1 --oneline githubsucks/main` → `d038f71`.
4. **The floor check, run against both floors**: `9a26ac8` (the old one)
and `d038f71` are each an ancestor of the tip, so advancing the floor
is valid rather than merely plausible.
5. The three-argument `git worktree add <path> -b <local>
githubsucks/<branch>` form → a real branch with its upstream set
(`## recovered-local...githubsucks/gui-stage1-pre`).
6. **The documented trap, reproduced rather than assumed**: the
two-argument form on a remote-only branch **succeeds** and leaves
`## HEAD (no branch)`. The correction below still holds on 2.55.0.
That distinction is the whole point of this paragraph: advancing the
base is exactly when the recovery commands are most likely to have
rotted, and a swapped SHA reads identically to a verified one.
**Correction, found by re-running it.** This file claimed the
two-argument form fails for a remote-only branch with `fatal: invalid
@ -250,186 +266,88 @@ hazard in a shape that looks committed. **A documented error message
that never appears is worse than no documentation**, because the reader
waits for a signal that is not coming.
## GUI arc Stage 1 — 1-pre OPEN as PR #237
## GUI arc Stage 1 — 1-pre MERGED as #237 (`d038f71`); 1a is next, NOT STARTED
**Written at the branch's first commit**, with the framing, as the arc's
§5 requires of every PR in it.
**The lane is rewritten, not removed.** Rule 4 removes a lane when its
ARC is done, and the arc is Stage 1 as a whole: **five slices remain**.
- **PR #237** — https://github.com/levineuwirth/pmacs/pull/237.
- **Branch `gui-stage1-pre`**, base `githubsucks/main` @ `f8ad3e7` (the
Stage 0 merge, #236). **`githubsucks/gui-stage1-pre` is the
authoritative tip** — the ref, not a SHA. Recover with
`git fetch githubsucks && git checkout gui-stage1-pre`.
- **Framing `docs/gui-stage1-input-framing.md`, revision 11, APPROVED**
after **eight rejected revisions**. Revision 9 is the approved design;
revision 10 recorded a scope correction found against this
implementation and **also argued P2 was satisfied by classification
alone, which review overturned — revision 11 retracts it**. It is
Stage 1's framing for **all** slices and governs the later branches;
only Stage 0 was framed by the arc document itself.
- **Scope of THIS branch: 1-pre only — the input seam. No behaviour
change.** `App::window_event` was **655 lines** and is now **four** —
call `dispatch_window_event`, exit if it asks. The dispatch it calls
is one `route_event` call and one arm per route. Nothing below it
could be witnessed without a display, which is why the seam precedes
every other slice.
- **IMPLEMENTED in four commits, one per event family**, so each lands
with its own witnesses and mutations rather than as one 600-line
diff: `014110f` lifecycle (close / modifiers / resize), `7f0f9db`
redraw, `0705564` keyboard, `e955645` the four pointer arms. Review
round 1 added `f976dc1`, the P2 effect harness.
- **P2 HAS TWO HARNESSES, and the second was review round 1's blocker.**
The routing harness answers *where did this event go*; `EffectHarness`
answers *what did it do*. Classification alone could not satisfy P2 —
**a wheel route carries a delta, and whether that becomes a viewport
update, a panel event, a terminal event or nothing depends on
`State`.** The effect harness drives a real `AttachClient` over a
`socketpair` (real handshake, outbox, writer thread and encoder, so
the transcript is the wire), a real windowless `State`, and
`App::dispatch_window_event`.
- **`App::dispatch_window_event` is what made P2 reachable.** Left inside
`window_event`, the dispatch would force the harness to re-implement
it, and a harness that re-implements what it tests witnesses its own
copy. `window_event` is now **four lines**, so **P3 shrinks from a
33-line match to a single `if`**.
- **Steps are delimited by a non-coalesceable sentinel key, not a
sleep** — otherwise "this step sent nothing" is undecidable without
waiting, and a fixed-duration wait against a writer thread is exactly
the core-count assumption behind PR #235's CI red.
- **The effect rows never skip**: a missing wgpu adapter is an assertion
failure. M21 confirms all **nine** effect rows fail loudly while the
**thirteen** GPU-free routing rows stay green. The assert is
**unconditional**, not `PMACS_REQUIRE_GPU`-gated, so no invocation
anywhere can turn a missing adapter into a quiet `ok`.
- **They execute in exactly ONE CI job, and that is checked rather than
assumed.** `cargo metadata` reports `workspace_default_members` as the
root `pmacs` package alone, so the `test` matrix and `crdt-test` —
both bare `cargo test --all-targets` — never compile `pmacs-gpu`'s
unit tests at all. Only **`gpu-render`** runs them, and it installs
lavapipe, proves the adapter with `vulkaninfo`, and sets
`PMACS_REQUIRE_GPU=1`. This is the handoff's "`gpu-render` runs a
DIFFERENT PACKAGE" fact showing up as a dependency: these rows live or
die with that one job.
- **Three manufactured absences, all found by running the rows**, and
each the same shape: the harness withheld something production
supplies, then witnessed its own omission. `resumed` sets the frontend
id and session version before any geometry flush (the resize row);
the fixture document was two lines and could not scroll; a headless
`State` has no attached buffer, so `scroll_by_lines` returned `None`.
A fourth was a vacuous assertion — `.all(|e| matches!(..))` over an
empty transcript is true — caught by the outbound-blind mutation.
- **The shape.** Deciding is `route_event(&WindowEvent) -> Route`, a
free function composing one decision function per family
(`route_lifecycle`, `route_keyboard` + `route_key_action`,
`route_pointer`). Performing stays on `App` in seven `apply_*`
methods. A route carries the **decision**, not the effect: a wheel
route holds a delta, and whether that becomes a viewport update, a
panel event, a terminal event or nothing depends on `State`. Effects
are witnessed separately — see the P2 bullet below.
- **Every moved body verified as the original, mechanically.** The
keyboard body is byte-identical modulo two named conversions; the four
pointer bodies were checked by re-running rustfmt on the pre-move text
at the new indent level and diffing, since de-indenting by 8 columns
lets rustfmt rejoin lines. **22 witnesses — 13 routing, 9 effect — and
24 mutations, M1–M24.** Twenty-three fail their own rows; **M6 is the
P3 exception check and must stay green**.
- **P3 IS NOW MEASURED, NOT ASSUMED — and re-measured after the effect
harness landed.** Replacing `window_event`'s whole body with `let _ =
(event_loop, event);` — a GUI that responds to no input at all —
leaves **all 265 `pmacs-gpu` tests green** under `PMACS_REQUIRE_GPU=1`
at the current shape (it was 256 before the effect rows; the number is
re-run, not carried forward). That is the exception's true extent:
`ActiveEventLoop` cannot exist outside a live event loop, so **no**
headless test in the crate observes the delegation. What it covers is
now **one `if`**, not a 33-line match.
- **A SECOND ACCEPTED STRUCTURAL EXCEPTION, found here and winit's
rather than ours.** `KeyEvent` carries a `pub(crate)
platform_specific` field, so **no `WindowEvent::KeyboardInput` can be
constructed outside winit** and the keyboard family's routing arm
cannot be fed by a test. Bounded three ways: it does **not** extend to
the pointer families (`DeviceId::dummy()` exists for exactly this, and
all three pointer events are constructible — checked before writing
the exception down); the family's only real decision is factored into
`route_key_action(ElementState)` and witnessed directly; and what
stays unwitnessed is one pattern arm containing a match and a call.
- **The crate now has exactly ONE executable `event_loop.exit()`**, in
`window_event`. Bodies return an `EventOutcome` rather than taking an
`&ActiveEventLoop`, which is what keeps them reachable in principle.
**`EventOutcome` has TWO producers and they differ in kind**: a native
close (`LifecycleRoute::Exit`), which must always exit, and
`apply_keyboard`'s idle Escape, which is a local quit. **Stage 1a's A4
removes the keyboard producer only**, leaving **one** `Exit`
producer — the native close. **One producer is not one variant**:
`EventOutcome` survives because `dispatch_window_event` must still
distinguish `Continue` from `Exit` on every event, and only the close
exits. A4 changes `apply_keyboard`'s signature, not the type. An
earlier revision of this bullet claimed the type would collapse and
should go with the Escape branch; that was wrong.
- **Slice order (each its own branch and PR):** `1-pre` → `1a`\* → `1b`
→ `1c` → `1d` → `1e`\*. **`1a` and `1e` are protocol-bearing (v24
`TextInput`, v25 `OpenTarget`/`OpenTargetResult`) and are
SERIALIZED.** 1c is **not** protocol-bearing under Q#S1-8.
- **Q#S1-7 obligation carried by THIS PR:** Meta/Super policy moves to
Stage 2, so the arc framing's §2.5 and the standing backlog are
amended here. Stage 1 keeps the deliberate OS reservation and adds no
island.
- **Gates:** `./scripts/gate --acceptance gpu_invocation_acceptance`.
That one invocation already runs `PMACS_REQUIRE_GPU=1 cargo test -p
pmacs-gpu` (step `gpu`) and the full `--workspace --no-fail-fast`
sweep in both feature configurations, so the framing §11 phrase "plus
touched input suites" is satisfied by the sweep rather than by a
hand-picked list. **No `--protocol`** — 1-pre changes no wire.
- **THE FIRST GATE RUN WENT RED ON A STRAY `/tmp/.git`, NOT ON THIS
BRANCH.** `m4` and the sweep failed
`m4_24_bare_string_glob_stays_relative` and
`m4_24_d3_fallback_base_is_the_smallest_attachment_dir` — two LSP
file-watcher tests — with every other target green. Established as
environmental three ways, in increasing strength:
- **Structurally impossible for this branch to cause.** The branch
changes seven files, five of them under `docs/`; **the whole
executable diff is inside the `pmacs-gpu` crate**
(`src/main.rs` and `src/attach.rs`), and `pmacs-gpu` is a workspace
**member but not a dependency** of the root package, so the
`m4_acceptance` binary never links it.
- **The marker is older than the session.** `/tmp/.git` is empty and
was created at 20:14 CEST, **3.5 h before** the gate run at 23:45;
`/tmp` held 8,920 entries. That is handoff §1's recorded hazard
exactly.
- **A discriminating pair compared on SIGNATURE, not test name**, same
binary and commit, one variable — run as **four literal `--exact`
invocations, one test each**, since `m4_24_` is a prefix matching
**18** tests. Contaminated: `0 passed; 1 failed` each, panicking at
`m4_acceptance.rs:5668:5` and `:6615:5` with `.received = ""`,
**matching the gate red's own signature**. Clean: `1 passed; 0
failed` each, **zero panics**. The exact commands are in handoff
§1's hazard bullet. A rerun would have established only
intermittence; the pair establishes the cause.
after eight rejected revisions. It is Stage 1's framing for **all**
slices and governs every later branch; only Stage 0 was framed by the
arc document itself. Revision 9 is the approved design; 10 recorded a
scope correction found against the 1-pre implementation; **11 retracts
10's claim that P2 was satisfied by route classification alone**, and
corrects the Stage 1a consequence 10 got wrong.
- **Slice order, each its own branch and PR:** ~~`1-pre`~~ → **`1a`**\* →
`1b` → `1c` → `1d` → `1e`\*. **`1a` and `1e` are the two
protocol-bearing slices — v24 `TextInput` and v25
`OpenTarget`/`OpenTargetResult` — and they are SERIALIZED** against
each other and against every other wire change in the project. 1c is
**not** protocol-bearing, under Q#S1-8.
- **NEXT: 1a, and nothing of it exists yet** — no branch, no code. It
carries the **v24** `TextInput` variant, so it must run alone.
`PROTOCOL_VERSION` is **23** at this base, and
`ADVERTISED_PROTOCOL_VERSION` stays pinned at 20 and must not be
edited to chase it. Its nine contracts (A1–A9) and the Q#S1-9
precedence rules are in the framing; **A4 deletes the idle-Escape
local quit**, which is the one 1-pre change 1a is expected to undo.
**`scripts/gate` isolates the target dir and five ambient roots but
NOT `TMPDIR`** — recorded in handoff §1, where the standing fix is
assigned to the gate lane rather than to this PR.
### What 1-pre landed, and the facts worth not re-deriving
- **`App::window_event` went from 655 lines to four**: call
`dispatch_window_event`, exit if it asks. Deciding is
`route_event(&WindowEvent) -> Route`, composing one decision function
per family; performing is seven `apply_*` methods. **No behaviour
change, no wire change.**
- **A route carries the DECISION, not the effect.** A wheel route holds
a delta; whether that becomes a viewport update, a panel event, a
terminal event or nothing depends on `State`. This is why P2 needs a
second harness, and why revision 10's contrary argument was retracted.
- **Two harnesses.** `RoutingHarness` answers *where did this event go*
(13 rows, no GPU). `EffectHarness` answers *what did it do* (9 rows) —
a real `AttachClient` over a `socketpair` through the real handshake,
outbox, writer thread and encoder, a real windowless `State`, and the
production dispatch. **22 witnesses, 24 mutations M1–M24**;
twenty-three fail their own rows and **M6 is the P3 exception check,
which must stay green**.
- **`dispatch_window_event` is why P2 was reachable at all.** Left inside
`window_event`, the dispatch would force a harness to re-implement it,
and a harness that re-implements what it tests witnesses its own copy.
- **The sentinel is the success condition; the 30 s read ceiling is only
an error ceiling.** Steps are delimited by a non-coalesceable sentinel
key rather than a sleep, so absence is never inferred from duration —
but an unbounded blocking read would wedge the gate instead of
reddening it, and a hang looks like slowness until the job is killed.
- **TWO ACCEPTED STRUCTURAL EXCEPTIONS, both measured rather than
asserted.** **P3**: deleting `window_event`'s whole body leaves all
**265** `pmacs-gpu` tests green, so no headless test in the crate
observes the delegation — what it covers is now one `if`. **P1,
keyboard only, and winit's rather than ours**: `KeyEvent` carries a
`pub(crate) platform_specific` field, so **no
`WindowEvent::KeyboardInput` can be constructed outside winit**. It
does **not** reach the pointer families — `DeviceId::dummy()` exists
for exactly this and all three pointer events are constructible,
checked before the exception was written down.
- **`EventOutcome` survives A4, and one producer is not one variant.**
The crate has **exactly one** executable `event_loop.exit()`, in
`window_event`. A4 removes the keyboard `Exit` producer, leaving the
native close; the type stays because `dispatch_window_event` must
still distinguish `Continue` from `Exit` on every event. What A4
changes is `apply_keyboard`'s signature.
- **The effect rows execute in exactly ONE CI job**, checked with `cargo
metadata`: `workspace_default_members` is the root `pmacs` package
alone, so the `test` matrix and `crdt-test` — both bare `cargo test
--all-targets` — never compile `pmacs-gpu`'s unit tests. Only
**`gpu-render`** runs them, with lavapipe, `vulkaninfo` and
`PMACS_REQUIRE_GPU=1`. The harness's adapter assert is
**unconditional**, so a missing adapter can never become a quiet `ok`.
- **A stray `/tmp/.git` reddened the first gate run and had nothing to do
with this branch.** Established on signature, not test name, by four
literal `--exact` invocations; the marker was left in place and an
isolated `TMPDIR` used instead. **`scripts/gate` does not isolate
`TMPDIR`** — recorded in `docs/agent-handoff.md` §1, where the
standing fix is assigned to the gate lane.
**The marker was left in place**: it is foreign, deleting it is
unnecessary, and the isolated `TMPDIR` is the correct remedy. It must
be **outside `/tmp` and outside every git worktree** — a child of
`/tmp` is not isolated, because `/tmp/.git` remains its ancestor.
- **GREEN under an isolated `TMPDIR`: all nine gates pass on the final
EXECUTABLE tree** (log `20260812T090034Z-2989598`, review round 2) —
executable, not final, because prose and doc comments changed after
it, as the sentence below records. fmt, clippy,
lib, lib-crdt, `gpu_invocation_acceptance`, **m4 168/0/3**,
`PMACS_REQUIRE_GPU=1 -p pmacs-gpu` (**265 tests, none filtered and
none skipped**, all nine effect rows included), the **117-target
`--workspace --no-fail-fast` sweep with zero failures anywhere**
(`m4_acceptance` running all 171 in it), and `diff-check`. Earlier
full-green runs at `20260811T215605Z-2664352` (round 1) and
`20260812T083735Z-2869707` (the P2 harness) are superseded by this
one. **What changed after it is doc comments and prose only** — the
`EventOutcome` correction below and this paragraph; `cargo fmt
--check`, `clippy -D warnings` and the 22 routing/effect rows were
re-run on the result. Stated rather than glossed, because "the gate
was green" and "the gate was green on exactly this tree" are
different claims.
## The GUI arc — Stage 0 MERGED as #236 (`f8ad3e7`)

View File

@ -1,6 +1,15 @@
# Agent handoff — cross-machine continuity
**Last updated: 2026-08-11 (second pass, same day).** `main` is
**Last updated: 2026-08-12.** `main` is **`d038f71`** — **#237**, GUI
arc **Stage 1-pre**, the input seam: `App::window_event` goes from 655
lines to four, deciding moves to a pure `route_event` and performing to
seven `apply_*` methods. **No behaviour change and no wire change** —
it is the seam the five behavioural slices land on, and **1a is next
and not started**. Beneath it `f8ad3e7` — **#236**, GUI arc **Stage
0**, the arc framing and its documentation sweep. **Both are absorbed
into §1 at this anchor.**
Previously **2026-08-11 (second pass)**: `main` was
**`122b8e8`** — **#235**, D3 of the LSP file watcher, which **closes
issue #233 and completes that arc**: the watcher stops sleeping and
walks once per scan, zero jobs at idle. Beneath it `b867f64` — git
@ -102,8 +111,66 @@ reads it the way you just did.
For volatile branches, checkpoints, verification, and recovery
commands, read `docs/active-work.md` immediately after this file.
## 1. Where the project stands (2026-08-11)
## 1. Where the project stands (2026-08-12)
- **GUI arc — Stage 0 MERGED as #236, Stage 1-pre MERGED as #237
(2026-08-12). The arc is OPEN: five Stage 1 slices remain and 1a has
not started.** Stage 0 framed the arc
(`docs/gui-arc-framing.md`); Stage 1 is framed for **all** its slices
by `docs/gui-stage1-input-framing.md` (revision 11). 1-pre is the
input seam and changes **no behaviour and no wire**:
`App::window_event` went from 655 lines to four. Durable facts:
- **A route carries the DECISION; only running the body gives the
EFFECT.** A wheel route holds a delta, and whether that becomes a
viewport update, a panel event, a terminal event or nothing at all
depends on `State`. An approved evidence contract asked for
"outbound events **and** local effects", and a first implementation
argued route-classification covered both. It does not, and framing
revision 11 retracts the argument. **When a contract names two
things, satisfying one and arguing it implies the other is a
narrowing wearing the costume of a mechanism.**
- **A harness that re-implements the thing it tests witnesses its own
copy.** The dispatch had to leave `window_event` — which cannot be
driven, since `ActiveEventLoop` does not exist outside a live event
loop — and become `App::dispatch_window_event` before any effect
could be observed. That single extraction is what made the evidence
possible, and it shrank the untestable residue to one `if`.
- **Winit's `KeyEvent` cannot be constructed outside winit** — a
`pub(crate) platform_specific` field — so **no
`WindowEvent::KeyboardInput` can be built by a test**. This will
bind every future GUI input slice. It does **not** extend to the
pointer families: `DeviceId::dummy()` exists for exactly this
purpose and `CursorMoved` / `MouseInput` / `MouseWheel` are
constructible. Bound an exception by checking its edges before
writing it down.
- **The sentinel is the success condition; the timeout is only an
error ceiling**, and conflating them undoes the design. Absence is
decided by a non-coalesceable sentinel rather than a duration, so
the harness is insensitive to core count — but an unbounded blocking
read turns a downstream regression into a **wedged gate** instead of
a red one, and a hang reads as slowness until the job is killed.
- **Four defects in the tests themselves, all one shape: witnessing an
absence the harness had manufactured.** It skipped the post-connect
wiring `resumed` performs, so geometry was silently withheld; the
fixture was too short to scroll; a windowless `State` has no
attached buffer, so viewport sends were withheld; and
`.all(|e| matches!(..))` over an **empty** transcript is vacuously
true. **Before believing a green row, ask what the harness withheld
that production supplies.**
- **`cargo metadata` decides which CI job runs a member's unit
tests, not intuition.** `workspace_default_members` is the root
package alone, so every bare `cargo test --all-targets` skips
`pmacs-gpu` entirely; only `gpu-render` runs it. A GPU-dependent
assert is therefore **unconditional** rather than
`PMACS_REQUIRE_GPU`-gated, so a missing adapter can never become a
quiet `ok`.
- **Deliberately unbuilt here**: every behavioural change. 1a
(`TextInput`, **v24**, protocol-bearing) is next and serialized
against 1e (`OpenTarget`, **v25**); **A4 deletes 1-pre's
idle-Escape local quit**, which is the one thing 1-pre added that a
later slice removes. `EventOutcome` nevertheless **survives** A4:
the native close still returns `Exit`, and **one producer is not one
variant**.
- **Git integration Stage 1 — MERGED as #227 (2026-08-11).**
`*git-status*` (a `listview` panel over `git --no-optional-locks -C
<dir> status --porcelain=v2 --branch -z`) and `*git-diff*`
@ -620,15 +687,20 @@ commands, read `docs/active-work.md` immediately after this file.
**Read this before picking anything up.** It is the only place the
remaining work is enumerated in one view; the per-arc bullets below give
the detail. Nothing here is in flight: **zero PRs are open** at this
anchor, so every item is startable.
the detail. **The table is current at `d038f71` (2026-08-12); the rows
beneath it that carry no GUI-arc mention date from 2026-08-01** and
were written before that arc existed. Zero PRs were open when this
section was first written; the GUI row is the only one refreshed since,
so treat the others as of that date and check `git log` before believing
any of them is still startable.
#### Arc state, against `COHERENCE.md` §20's priority order
| P | Arc | State | What is next |
|---|---|---|---|
| 1 | **Journey** | **Stage 1 COMPLETE** (1a #182/#183; 1b-1 #203, 1b-2 #204, 1b-3 #205) | Journey runs to step 10. The thin end is now steps **1** (install — that is P8), **11** (background work: visible but no ownership model, §9) and **12** (session restore: desktop-save is opt-in *and* a documented no-op under a daemon) |
| 2 | Workspace + location | Missing; model gap | The long-lead arc. Start before a fifth subsystem grows its own root convention — four have already diverged (§7) |
| 1.5 | **GUI arc (half A)** | **Stage 0 MERGED (#236); Stage 1-pre MERGED (#237)** | **`COHERENCE.md` §20 Q#GA5 slots half A immediately after Priority 1** and it runs alongside P2–P5, interacting only at P2 — *reaching Stage 4b is a P2 START GATE*. It is the product half of §16 and what that section's product subgrade is graded against. **Next: Stage 1a**, not started, protocol-bearing at **v24** and serialized against 1e's v25 |
| 2 | Workspace + location | Missing; model gap | The long-lead arc. Start before a fifth subsystem grows its own root convention — four have already diverged (§7). **Gated by the GUI arc reaching Stage 4b** (Q#GA5) |
| 3 | Extension ownership | Missing; prerequisite-shaped | **`pmacs.hook.remove` does not exist.** That one bug-sized gap blocks §13's disable/uninstall, §10's trust classes, and package-scoped cancellation |
| 4 | **Discovery** | **Stage 1 MERGED (#207)** | Stage 2 candidates, in rough dependency order: richer M-x rows (**protocol change** — `MinibufferPrompt.candidates` is `Vec<String>`; `CompletionPopupRow` already proves the pattern), `Command` gaining title/category/aliases/flags/arg-schema (~147 definition sites), predicate evaluation, help-layer unification, and the help-prefix decision |
| 5 | Workbench convergence | Partial; **Arc 7 COMPLETE** (#213) and **the tree primitive is implemented** (PR held) | The bottom panel is finished on both frontends and the adopter default is flipped. The tree primitive has landed on a held PR: §14's Tree moves ✗ → ◐ with the LSP outline as its one adopter. **Next: adoption** — dired's `i`, then DAP's variables view, which is why it was built first |