Review finding (P2), and it is this lane's own defect class one level
down. Round 1 added a `-p pmacs-protocol --features crdt` step so that
crate compiles both ways. That step EXECUTED
`InstanceCapabilities::default` in both configurations and asserted
NOTHING about it: the crate's only use of that value is a transport
round-trip, and a round-trip is invariant to the values. An all-false
default — or one whose three fields disagreed with each other — encodes,
decodes, and passes identically in both builds.
Running code is not testing it. That is the same sentence this whole
lane is about, and round 1 committed the smaller version of it while
fixing the larger one.
Three tests now pin the defaults, and the split is the point:
* under `crdt`: multi_frontend, crdt_replica and semantic_render all
default true. Advertising false on a CRDT build would strand every
frontend in single-frontend mode.
* without `crdt`: all three default false. Advertising true would be
wire-protocol false advertising — those code paths are
conditionally compiled out.
* FrontendCapabilities::default is all-false in BOTH builds, and this
test is DELIBERATELY NOT feature-gated.
That third one pins an asymmetry nothing else did.
FrontendCapabilities derives Default and is feature-INVARIANT, while
InstanceCapabilities is feature-DEPENDENT. It is load-bearing rather
than an oversight: an instance advertises what it can do, a frontend
OPTS IN through the negotiation handshake, and a v1 frontend has no
local CRDT state regardless of how the crate it links was compiled.
Making the frontend side track the feature would have frontends
claiming support they do not have. A future edit that "makes them
consistent" now fails a test that says why not to.
All three fields are asserted separately rather than by comparing whole
structs, because they track one `cfg!` and a change flipping only some
of them is exactly the regression worth catching.
Bite-verified rather than assumed: mutating `multi_frontend` to a
literal false gives `FAILED. 18 passed; 1 failed` with the expected
assertion message; restoring returns 19/19. Both configurations now
report 19 tests, up from 17, with the correct cfg-gated test running in
each.
Left untested and recorded instead: `InstanceCapabilities::crdt_replica`
carries `#[serde(default = "default_true")]`, a THIRD default mechanism
that is unconditional and therefore disagrees with the `Default` impl in
a non-CRDT build. Exercising it needs a self-describing format and this
crate's only serde dependency is postcard, which is not one. Adding
serde_json as a dev-dependency to test a divergence this lane did not
introduce is scope creep.
Also corrects a note that round 1 made stale: the ledger's "do not
subtract the two jobs' totals" figures (3,485 and 3,747) were measured
BEFORE round 1 added the protocol step, and round 2 adds two tests to
that crate. Expected totals are now 3,487 and 3,766. The root-package
census is untouched at 3,467 / 3,746 — the new tests live in a sibling
crate, which is precisely the region scripts/feature-census cannot see.
Verified: fmt, diff-check, clippy on pmacs-protocol in both feature
configurations, and workspace clippy --features crdt --keep-going.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding (P2): docs/active-work.md and docs/agent-handoff.md both
still said "PR not yet open". This file's own rule is that an open PR
gets a lane recording its state — "When a PR is opened, give it a lane",
written after #171 drifted 153 commits while invisible here. Both now
carry PR #209, its six commits, and the first CI run.
That run is worth recording rather than merely citing, because it
discharges the one risk the framing could not close locally. ALL 14
CHECKS GREEN, including both new jobs (Test (crdt) 12m20s, M10 Perf
Gates (crdt) 5m40s) and the macOS/luajit leg that is the documented
flake surface. This was the first time in the project's history that
any of these tests executed in CI.
ACCEPTANCE 8 HOLDS AGAINST THE REAL RUN. Test (crdt) reported 3,717
passed / 0 failed / 30 ignored: the 3,746 all-targets census (with
basedpyright NOT skipped, as it is locally) plus one doc test, less the
30 ignored. The job demonstrably compiled and ran the crdt corpus rather
than reporting green over nothing — which was the whole point of writing
that criterion while the local sweep was already green.
A trap for the next reader is recorded with it: DO NOT SUBTRACT THE TWO
JOBS' TOTALS. Test (ubuntu/luajit) reports 3,485 and Test (crdt) 3,747,
a difference of 262 rather than 279, because the jobs run different
SETS — the non-crdt job adds pmacs-protocol's 17 tests. The dark count
is the all-targets comparison, 3,746 - 3,467. The wrong number looks
entirely plausible.
Chasing that discrepancy found a real gap, and one this lane's own tool
could never have surfaced: PMACS-PROTOCOL HAS ITS OWN `crdt` FEATURE.
It gates no tests, so a per-test census reports 17 either way and scores
the crate as unaffected — but it changes `cfg!(feature = "crdt")`
EXPRESSIONS inside InstanceCapabilities::default and
FrontendCapabilities::default, so those same 17 tests exercise different
runtime values under it. CI had only ever run the non-crdt ones. Closed
with an explicit `-p pmacs-protocol --all-targets --features crdt` step,
verified 17/17.
The blind spot is structural, not an omission, so it is documented at
the tool rather than fixed in it: scripts/feature-census censuses the
workspace DEFAULT MEMBER, because that is what a bare
`cargo test --all-targets` builds. Sibling crates are invisible to it no
matter what configs are passed. The durable lesson: A FEATURE CAN MATTER
TO A CRATE A PER-TEST CENSUS SCORES AS UNAFFECTED. Check siblings by
hand.
Verified: ci.yml parses at 10 jobs, script syntax clean, fmt,
diff-check, and the new protocol step green locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Framing revision 3, plus the ledger and handoff updates the lane owes.
docs/active-work.md's "NEEDS A LANE" block is replaced rather than
annotated: the lane now has a branch, a framing, an implementation and
verification, so the old text's premise ("no branch, no framing yet")
was the part that needed removing. Its stale figures go with it — the
273-dark reading at 74301d1 and the seven-item clippy list, both
superseded and both explicitly labelled do-not-quote in the new block.
The corrections this lane produced, all now recorded where the next
reader will hit them:
* m10_10_perf is a CI-DEFAULT REGRESSION TRIPWIRE, not a bench. Its
bounds are deliberately generous "to catch catastrophic regressions,
not to verify a tight perf claim", so #[ignore]ing it to give it a
perf job would have reduced coverage inside a coverage lane. The
framing had this backwards through revision 1 and says so.
* gpu-render runs `cargo test -p pmacs-gpu` — a DIFFERENT PACKAGE from
the root-package GPU suites. The long-recorded fix-shape of "move
the GPU-requiring crdt suites onto gpu-render, it already has
lavapipe" does not work as written.
* PMACS_REQUIRE_GPU is not uniform: absent from
gpu_invocation_acceptance and gpu_initial_target_acceptance
entirely, so it cannot serve as blanket proof the GPU suites ran.
* The dark-test classification found THREE dispositions, not two:
benches awaiting a job, deliberately-manual operator tests, and
known-defect markers. Collapsing the second into the first gives a
CI job to tests whose #[ignore] reason says not to.
* §1.1's own target-column claim was wrong, and scripts/feature-census
caught it. Eleven targets run with zero tests under CI's flags;
eight of those gain tests under crdt and three are helper binaries
with no tests in either. The framing had merged two true statements.
Acceptance 9 is revised rather than quietly met at a lower bar. The
planned deliberately-broken-test bite is replaced by the --covers
coverage assertion plus the CI count reconciliation, and the doc states
what that trades: stronger on the claim specific to this lane (the old
job STRUCTURALLY cannot see these tests), and no longer proving that a
failing crdt test turns the job red — which is generic cargo and Actions
behavior, not anything this lane changes.
The handoff gains a census hazard beside the existing sweep note,
because every trap in it was hit while writing the script and one of
them survived two framing revisions: libtest prints `name: test` with no
space before the colon, a zero-test target prints only its Running line,
and both configurations need an --ignored pass or pre-existing ignores
get attributed to the feature.
The ledger also records what is NOT established, which is the lane's
whole remaining risk: the sweep is green SERIALIZED ON A DEVELOPER
MACHINE, and the failures this lane expects are hosted-runner timing and
concurrency. That green removes the "tests are wrong" explanation and
leaves the expected one untested. It must not be quoted as evidence the
CI leg will be green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.github/workflows/ci.yml` never enables the `crdt` feature anywhere, so
every `#[cfg(feature = "crdt")]` test is not compiled in CI — not
skipped, not filtered, not reported. 279 tests have never executed in
CI, and 186 of them are in the library, whose `cargo test --lib
--features crdt` invocation CLAUDE.md lists as a required pre-PR gate.
CI has never once run a required gate.
Named in docs/active-work.md as "NEEDS A LANE" since it was found while
gating #166, with no branch and no owner until now.
Revision 2, approved. What the scout established beyond the ledger:
- The census moved and was re-measured at 4223dd3: 279 dark, not the
ledger's 273. Plus a fact the old census did not carry — eight test
binaries contain zero tests under CI flags, so they build, run, and
report ok with nothing in them.
- "279 dark" overstates the fix. Eleven are #[ignore]d, so a plain crdt
leg recovers 268. All 279 are now assigned a disposition: 268 by the
leg, 7 by a new m10-perf-gates job, 3 deliberately manual, 1 an
ignored known-defect marker. The lane recovers 275 of 279 and the
other four are excluded for stated reasons rather than left as
residue.
- The classification docs/active-work.md called "the lane's first task"
is finished, and it found three dispositions rather than two:
benches awaiting a job, deliberately-manual operator tests, and
known-defect markers.
- The ledger's clippy inventory was stale in both directions. It warned
its own list was "a lower bound, not an inventory" because clippy
aborts on first failure; --keep-going is what converts it. The
complete set is eight findings across four files, none behavioral.
- A hole in the proposed fix shape: the gpu-render job runs
`cargo test -p pmacs-gpu`, a different package from the four root-package
suites the fix wants to move there. And PMACS_REQUIRE_GPU is absent
from two of those four, so it cannot serve as blanket proof they ran.
- The full serialized crdt sweep is green: 3,715 passed, 0 failed, 30
ignored, reconciling exactly to the 3,746 census. That green result
shaped the acceptance criteria more than a red one would have — a
green pre-measurement is the condition under which a vacuous CI job
ships unnoticed, so the criteria gained a count reconciliation and a
deliberately-broken-test bite.
Revision 1 -> 2 also corrects revision 1's classification of
m10_10_perf, which was wrong in a way that would have made the lane
worse: the suite is a deliberate CI-default regression tripwire with
generous bounds, not a bench, so adding #[ignore] would have shipped a
coverage reduction inside a coverage lane.
Framing only. No workflow change, no source change, no branch plan
executed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine PRs landed since the handoff's anchor (#199-#207) and it named
only four of them, so a fresh machine would not have learned that
ambient-root isolation exists, that the journey arc's 1b split
completed, or that the reap-ledger diagnostic landed. The ledger's
canonical base and recovery floor were five merges stale, and three
lanes described merged work as in flight.
Taken now because ZERO PRs are open. The ledger's own rule is never to
open a standalone refresh PR — because with several PRs open a lane
written on `main` re-conflicts at every merge — and this is the one
window where that cost is nil.
The handoff gains §1a, "Outstanding work — the whole board", which is
the point of the pass rather than a by-product: every arc against §20's
priority order with its next step, every open lane, every deferred item
attributed to the framing that parked it, and the standing hazards
someone running gates on a new machine needs — the three known flakes
by name, the basedpyright skip, the crdt sweep's build prerequisite,
and why a green a37 means nothing alone.
Two arcs completed, so per rule 4 their lanes are removed and their
facts are in the handoff: Journey Stage 1 and test ambient-root
isolation. Discovery and reap-ledger merged a stage each, so their
lanes are rewritten to the remaining plan rather than deleted — the
discovery lane now enumerates Stage 2 in dependency order and carries
the two Stage-1 facts a Stage-2 author would otherwise rediscover.
Base and floor advance together to `cfc1710`, per the file's own rule
that a floor accepting an older commit than the declared base passes on
a tree the document does not describe.
The recovery path was EXERCISED rather than asserted: from an empty
directory, clone, alias, fetch, floor check, and a lane worktree all
ran clean. The two-argument `git worktree add` still fails for a
remote-only branch, which is why every lane spells out the `-b` form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
§7 said the classification comes first and alone, "its answer belongs in
review before any mechanical edit rides on it." The classification came
back at 342 sites across 66 of 97 files, and the whole-corpus migration
rode this PR anyway. That was a decision, not an oversight, and revision
5 records it as one so a later reader does not have to reconstruct it
from the diff.
The reasoning, in short: splitting would either leave 65 suites still
writing the developer's real data root while the seam sat unused, or ship
acceptance 12's ratchet with a ~65-file allowlist — and a ratchet
exempting most of the corpus records rather than ratchets. §7's ORDERING
is honoured (the census is the first commit); its implied SCOPING is not.
Revision 5 also records what review round 1 established about the shape
acceptance 12 needs: "a narrow, named allowlist" is not sufficient by
itself, because narrowness constrains which files are exempt and says
nothing about how far each exemption stretches. Exemptions carry counts.
`docs/active-work.md` picks up the same two facts and the PR number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Per §25 the audited claim this stage falsifies is updated on the
landing PR. §5's scorecard row and ground truth both said the
registration metadata was best-in-class with almost no way for a user
to reach it; eleven `help.*` commands now reach it.
The row names what is still missing rather than implying the concern is
closed: packages and workers have no discovery surface, `Command` still
has no title/category/aliases/flags, M-x rows are still bare name
strings, the predicate is still never evaluated, settings value
provenance is still absent, and there is still no help prefix key. The
original audit grade is kept inline for reference.
`docs/keybindings.md` gains the family table and records that the
commands are reachable by name and bound to nothing — the help-prefix
decision belongs to a later stage, for the reason #205 recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`docs/active-work.md`: the lane moves from FRAMING OPEN to
IMPLEMENTATION OPEN, with the implementation branch and worktree and a
recovery command that names them. The framing-only worktree is spent —
its doc is on `main` (#201).
`docs/agent-handoff.md` §3: a local full-suite run needs all FIVE
storage variables controlled, not four. `PMACS_STATE_HOME` outranks
`XDG_STATE_HOME`, so naming only the XDG four leaves a
higher-precedence state override live; and a run isolating only
`XDG_CONFIG_HOME` stops the `init.lua` reads while still writing through
the real data root — every local gate run in this repo before today had
that hole. `HOME` is deliberately excluded: it is the fallback the XDG
roots already cover once set, and it separately drives `~`-expansion,
which `find_file_acceptance` pins on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Correct the M-x prompt census: six of the eleven canonical commands
need a second prompt, including describe-command. Make the apropos
substring negative discriminate against fuzzy matching with a concrete
non-contiguous fixture and a no-substring precondition. Update the help
index mutation to the twelfth canonical command, and carry all three
corrections into the active-work ledger.
No review findings at 1cc9d96; these are the two product choices the
framing deliberately left open.
Q#D2 resolves to `help.*` canonical with two forwarders. The family is
eleven commands under one prefix, so typing `help` at M-x surfaces all
of it — which is the discoverability win this arc exists for.
`editor.describe-command` and `editor.describe-setting` stay registered
as forwarders, so nothing in muscle memory or in
`docs/keybindings.md` breaks. Two forwarders are duplication of exactly
the kind §5 complains about; they are accepted as the bounded price of
not breaking documented names, and they carry a deprecation path. What
is not accepted is the split family revision 3 shipped.
Q#D3 resolves to substring. `fuzzy_score` is subsequence-based and
descriptions are long sentences, so a short query's letters almost
always appear in order — fuzzy would match nearly every command and
destroy the precision that makes apropos worth having. Acceptance 4 now
pins the decision rather than the intent: a subsequence that is not a
substring must find nothing.
Counts and pins follow the decision: the family is eleven throughout,
the `_show_help` counting stub expects eleven (the two renamed commands
are in it precisely because they were the pre-existing direct callers),
and the preservation pin now drives the forwarders through the real M-x
path — dropping them after the rename is the failure a user with muscle
memory would hit first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two factual corrections, both accepted and both verified in the code.
The custom completion source does not control display order. Revision 3
justified sorting the pool by claiming `Custom` candidates appear in
return order; `recompute_candidates` hands the pool straight to
`filter_and_sort`, which ranks by fuzzy score descending and breaks
ties lexically, so the source's order never reaches the user.
The sort is kept, for a reason that is actually true: `filter_and_sort`
applies `.take(CANDIDATE_LIMIT)` to the FILTERED iterator before
sorting, so when more settings match than the limit, pool order decides
which survive truncation. Registration order would make that vary with
an unrelated config edit; sorting makes it reproducible.
Read-only would not mitigate the foreign-`*help*` collision either.
Revision 3 implied it would. A buffer the user created and named
`*help*` carries no intercept of ours, so an intercept on the buffers
we create protects nothing — the renderer still matches on the name and
clears theirs. The missing guarantee is ownership identity: a private
table of buffers this module created, so found-by-name is not adoption.
`listview` carries it as `panels` and dired as its handle table; this
mechanism carries neither. Naming the wrong missing guarantee would
send a later fix at the wrong layer, which is why the correction is
worth its own paragraph rather than a word swap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted.
The ledger lane still said revision 1 and still carried the two claims
revision 2 refuted. The cause is worth recording: revision 2's ledger
edit was an assert-then-write block whose later assertion failed, so
nothing before it was written either — while the commit message
reported the whole change as done. This is the second time in this
project that shape has silently dropped edits. The lane is rewritten
from scratch and the result verified by re-reading the file rather than
inferred from an exit code.
`names_from` does not exist. Revision 2's completion source called a
helper nobody has written, over `pmacs.config.list()`'s descriptor
tables where `CompletionSource::Custom` consumes a sequence of strings.
Opening the prompt would have raised on an undefined global. The mapper
is now specified, and sorted — `Custom` presents candidates in the
order returned, and registration order is neither stable nor useful.
`*help*` has no read-only intercept. Revision 2 claimed one while
`show_help_text` writes with plain delete/insert, and #205 had already
recorded that this mechanism has not adopted the generated-buffer write
invariant. §3.4 now names the policies that really are shared —
reuse-by-name, wholesale replacement, the `q` binding, and the
foreign-buffer hazard — and notes that the last is precisely what a
read-only intercept would have mitigated and does not.
The naming was underspecified. With no `help.describe-command` in the
table, calling the existing `editor.describe-*` commands
"aliases-by-retention" was wrong on both halves: nothing forwards to
them and there was nothing for them to alias. They are now explicit
exceptions, the resulting split surface is named as a wart, and Q#D2 is
sharpened to the two ways out — forward the two under `help.*`, or keep
the family `editor.*` throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted and verified in the code
before the doc changed.
Completion does not close the free-text hole, and revision 1 said it
did. `resolve_accepted_value` returns the literal typed text whenever
no candidate is selected, so a non-matching typo still reaches
`on_accept` and the existing error path; separately, a fuzzy near-miss
can silently describe a DIFFERENT setting, which is a new failure mode
revision 1 did not name. Completion is reframed as assistance, the
acceptance pins both outcomes, and closed-set acceptance semantics are
named as Rust work rather than smuggled in as a side effect of adding
a source.
`invoke_interactive` is not the M-x path — the exact error #205
corrected one PR earlier. It rotates the interactive-command boundary
and calls the body; it opens no palette. The acceptance now states the
real path once and drives it: dispatch M-x, type the name, assert the
selected candidate BEFORE RET (accept does `session.take()`), accept,
and — for the five commands that take an argument — drive the second
prompt too. A pin that stops after the first RET has tested the
palette, not the command.
The `_show_help` seam is an output sink, not a migration seam.
`src/help.rs` has semantic renderers for command/key/buffer/mode/hook/
view and none for settings, lists or apropos, and the seam takes
already-flattened text, so a later migration still has to change each
command's subject-specific logic. The claim is narrowed to what is true
— one owner for Lua `*help*` writes — and paired with a per-subject
renderer function so the future Rust work is enumerated rather than
discovered.
Ground truth corrected: the missing-command list was eight and omitted
`list-settings` while §3.1 said nine; the site count said ten where
nine additions make eleven; `pmacs.keymap.lookup` does not return
`description` (it passes `cmd = None`); and the predicate sites cited
were `MenuItem` fields, not `Command.predicate`. The predicate
conclusion survives on the correct evidence — `src/help.rs:76` and one
assertion past `#[cfg(test)]`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §20 Priority 4, which §5 grades "substrate without
surface — the sharpest instance of §1.1" and calls the best
payoff-per-effort in the document. Nine describe/list commands over
introspection that already exists, picking up the family root `M-x
help` that #205 landed and documented for exactly this.
The stage adds no Rust. `pmacs.describe.*`, `pmacs.keymap.list()`,
`pmacs.command.list()` and `pmacs.config.list()` already return
everything the commands render, and `parse_completion_source` accepts a
Lua function as a `CompletionSource::Custom` — so `describe-setting`'s
free-text prompt, which today turns a typo into a status-line error
while `describe-command` has had completion all along, closes with no
new Rust either.
The consequential decision is a single rendering seam. `src/help.rs`'s
cross-referenced renderer is orphaned and the reachable Lua
`show_help_text` renders less, so nine new commands calling it directly
would turn a two-site migration into a ten-site one. Everything routes
through `pmacs.editor._show_help`, and an acceptance pin replaces that
seam with a counting stub — funnelling asserted, not merely intended.
Records that `Command.predicate` is stored and exposed but evaluated
nowhere, and deliberately does not start evaluating it: that makes
commands stop being invocable, which needs its own decision about what
"unavailable" means at each call site. A preservation pin registers a
raising predicate and asserts the command still runs, so a later stage
has to change that pin knowingly.
Adds no keybindings at all — the help-prefix question is taken once,
for the whole family, by the stage that can weigh it against the
constraint #205 recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 2 on #205, one finding, accepted. #204 is this PR's base,
so three places still describing it as open were simply false.
- `COHERENCE.md` §2's step-6 row: "(PR open) ends the silence" ->
"(#204) ended the silence", and the "flips only on merge" clause is
replaced by the reason the row is still Partial at all — a server
that starts and then crashes is unsurfaced, which that landing did
not touch.
- `docs/agent-handoff.md`: the 1b-2 bullet says LANDED (#204), with the
same substituted reason.
- `docs/active-work.md`: 1b-2's per-stage block is removed rather than
relabelled, per rule 4 — it has merged and its durable facts are in
the handoff, which is the precondition for removal.
That last one exposed a structural problem the merge had created: 1b-2's
old lane body had come in *underneath* the unified journey header, so
the file carried two journey sections and the first one's text
described the second's predecessor. There is now one arc lane with
1b-3 nested under it as the only open stage.
Also corrects the reap-ledger lane header, which still read
"IMPLEMENTED, PR OPEN" after #202 merged. Same class of staleness,
noticed while editing the same file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1 on #205, two findings, both accepted.
The greeting was written straight into the registry without calling
`notify_buffer_edit`. The window's `TextView` had been indexed while
`*scratch*` was empty, and newlines are zero-width to a painter working
from a stale line index — so the first TUI frame collapsed the whole
three-line greeting onto row 0. Every buffer-text assertion passed
because the buffer content was correct; only the rendering was wrong.
The edit is now captured, the registry borrow released, and the core
notified.
The pin that would have caught it paints a real frame and asserts the
second line occupies its own row AND that row 0 does not contain it —
both directions, because a one-direction check passes when everything
collapses upward. Bitten by dropping the notify call: row 1 comes back
empty with row 0 holding the lot, and it is the only pin that fails.
Second: the project docs still described the arc as it was two PRs ago.
`COHERENCE.md` §20 called 1b-2 in flight and the welcome buffer
unstarted; its arc list said 1b-3 remained; and the ledger's journey
lane header still read "1b-2 PR OPEN" while the 1b-3 block carried a
mangled "Framing only; no code" line left by an earlier edit. All now
describe the PR-head state per §25.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The journey suite conflicted additively — step 4 from this lane, step 6
from #204 — and both are kept: 44 pins now cover steps 2, 3, 4, 5, 6
and 9.
Per §25 the audited claims this stage falsifies are updated on the
landing PR rather than deferred: the scorecard's row 18 and §18's
ground truth both read "Missing" / "missing entirely", and a welcome
buffer plus a reachable cheat sheet makes both false. They move to
Partial. §2's step-4 row stays Partial, because `C-h` still deletes a
word and there is no tutorial.
§18's ground truth now records WHY `C-h` stays as it is, so the
help-prefix question reaches the discovery arc as a stated trade rather
than an oversight: non-kitty terminals cannot disambiguate
Ctrl+Backspace from Ctrl+H, so rebinding it would break Ctrl+Backspace
on every legacy terminal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two findings, both accepted.
The pinned API was crate-private while the pin was external.
`tests/journey_acceptance.rs` is a separate integration crate and
cannot call a `pub(crate)` function or match a `pub(crate)` enum, so
revision 3's acceptance 1 could not have compiled. `prepare_startup`
and `Startup` are now `pub`, which is consistent rather than expedient:
`run`, `EditorState::new`, `EditorState::open`, `install_state_dirs`
and `restore_desktop_if_armed` are already public, so the startup
sequence is public surface and this was the piece missing from it. The
alternative — keep it private and move the pin into `src/editor.rs`'s
unit tests — was rejected because §19 wants the journey row in the
journey suite.
The isolation that pin needs is now stated rather than left implicit.
`prepare_startup` deliberately calls `install_state_dirs`, which
resolves `PMACS_STATE_HOME` and XDG roots that tests cannot override
(`set_var` is unsafe and forbidden), so the pin inherits the standing
five-variable ambient-root requirement, asserts buffer content only,
and must assert that desktop restore was unarmed — a developer whose
real `init.lua` calls `desktop_mode(true)` would otherwise get a
restored scratch buffer and a silently different result.
The M-x acceptance still left its key assertion as an "or", and half of
it was impossible: `Minibuffer::accept` does `session.take()` and
resolves against the selected candidate, so after RET neither the
session nor the typed contents survive to be asserted. The framing now
specifies one observable, available before RET:
`pmacs.minibuffer.selected()` must equal exactly "help", then accept.
If the completion source selected something else the pin fails there,
naming what was actually selected, instead of passing on a help buffer
some other command produced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two acceptance holes and one doc correction, all accepted.
The real startup wiring was still unpinned, and revision 2 knew it: it
named the gap and then accepted it as residual, which is worse than
missing it. Deleting the sole `run()` call to `finalize_local_launch`
would have left every proposed pin green while shipping no welcome —
pin 1 called the seam by hand and pin 10 only proved constructors were
blank. That is the shape of a guard with no production caller passing
every direct-call test.
The fix is extraction, not disclaimer. Everything in `run()` from
`install_panic_hook` through the end of the attach-dispatch match is
terminal-free; `Frontend::new()` is where takeover begins. That prefix
becomes `prepare_startup`, which `run()` delegates to and which the
welcome pin now drives. Deleting the finalize call inside it turns the
pin red. What stays outside a test is `Frontend::new()` and the event
loop, which is where terminal takeover genuinely lives.
Acceptance 4 was not the M-x path. `pmacs.command.invoke` is the
programmatic API; M-x is `editor.execute-command`, which opens a
minibuffer with the `commands` completion source and calls
`invoke_interactive` only on accept. The pin now dispatches the chord,
enters the name, and accepts — and names the hazard that makes that
non-trivial: a selected candidate shadows typed text, so the pin has to
assert which command ran rather than trusting that some help buffer
appeared. dired refused a completion source on its own prompt for
exactly this reason.
Also corrects the #204 references: revision 2's history said it had
landed while the framing footer and ledger still called it open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Four findings, all accepted and verified in the tree first. The three
open questions are answered and folded into the design: three lines,
name `C-c c` and `C-c t` verified from structured entries, and the
command is `help`.
The startup seam was wrong, and every acceptance criterion rested on
it. `EditorState::new()` is not the no-argument entry point:
`EditorState::open` calls it before resolving the target, the daemon
constructs one too, user config runs inside it, and desktop restore
happens much later inside `run()`'s `RunLocal` arm. Greeting from
`new()` would greet a daemon, greet before a file argument replaced the
buffer, and precede anything config or a restored session put in
`*scratch*`. The stage now adds a launch-finalization seam called right
after `restore_desktop_if_armed`, using the `had_file` signal already
threaded to that point. Its untestable part — that `run()` calls it —
is stated rather than papered over, and bracketed by pins proving the
seam works and that no constructor greets on its own.
The step-2 pin amendment is withdrawn. Revision 1 analysed a
status-line welcome and then chose `*scratch*` but kept the amendment,
which was an internal contradiction. The status stays empty so the pin
stays true, and "no error text" has no defined predicate over an
unstructured status string — replacing an exact check with a fuzzy one
would weaken the ratchet to buy nothing.
The claim that this stage carried no §25 obligation was false. The
scorecard's row 18 and §18's ground truth both read Missing, and a
landed welcome plus reachable cheat sheet falsifies both: they move to
Partial on merge while §2's step-4 row stays Partial. A stage can be
too small to flip its journey step and still falsify a "missing
entirely" grade.
Acceptance 2 could not have been implemented as written. Scraping key
sequences out of prose is ambiguous — `M-x help` mixes a chord with a
command name, and `C-c c` is two chords whose boundary the text does
not mark. One structured entry list now both renders the text and
drives the `pmacs.keymap.lookup` checks, and `M-x help` is exercised
through the real palette path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The last of the 1b split. Journey step 4 and `COHERENCE.md` §18: a
fresh `pmacs` greets the user with an empty buffer, an empty status
line, and no indication that `M-x` exists — the sole discovery
affordance in the product is knowing to press it.
Three findings shape the design:
- The existing step-2 ratchet pin collides with any welcome. It asserts
`status.is_empty()` while its own message says "reports no error" —
the same predicate only while nothing writes a non-error status at
startup. The framing corrects the assertion to its message's claim
and names that as the one existing assertion the stage changes,
rather than burying it in a diff.
- `C-h` is not free. It is bound to word-delete because non-kitty
terminals cannot disambiguate Ctrl+Backspace from Ctrl+H — both
produce byte 0x08 — so rebinding it to a help prefix would break
Ctrl+Backspace on every legacy terminal. §2's step-4 row calls it an
oversight; it is a deliberate trade with a stated rationale. The
help-prefix decision is deferred to the discovery arc with the
constraint recorded so it is inherited rather than rediscovered.
- A `*help*` buffer mechanism already exists over `src/help.rs`, so the
welcome can point at something real. Its two gaps are recorded rather
than inherited silently: it writes with delete/insert instead of
`set_generated_contents`, and it is found by name.
The welcome deliberately does NOT use `set_generated_contents`: that
lifts read-only, discards history and marks the buffer generated, all
wrong for the buffer step 5 requires the user to type into immediately.
Step 4 stays Partial — the row names a welcome, a cheat sheet and
`C-h`, and this closes the first plus a minimal second — so this is the
first 1b stage with no landed-evidence obligation on merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Stage 1b-1 landed as #203, so the four places it deliberately left
saying "in flight" are flipped here rather than in a standalone docs
PR: this branch already touches all three files, and a separate PR
would re-conflict on every merge.
- `COHERENCE.md` §2's step-9 row: Partial -> **Works**.
- §2's keybinding-inversion paragraph: all three examples answered. The
quote itself is deliberately unchanged — it names a bias, and three
fixes do not retire a bias.
- §20 Priority 1 and the arc list: 1b-1 landed, 1b-2 in flight, 1b-3
remaining.
- `docs/agent-handoff.md` §1: IMPLEMENTED -> LANDED.
Conflicts were additive on both sides and are resolved keeping both:
the journey suite carries step 9 and step 6 (34 pins), and §24 keeps
both drift entries — the `ProjectKind::Cargo` naming error and §1.2's
wrong frequency note.
The two journey lanes are unified into one arc lane rather than one
being deleted. Rule 4 removes a lane when its ARC is done, and the
journey arc is not: 1b-2 is in flight and 1b-3 is unframed. Stage 1a
and 1b-1 are summarized there with their facts in the handoff, which is
rule 4's precondition satisfied rather than deferred.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Implements `docs/journey-stage1b2-lsp-guidance-framing.md` (approved at
revision 4, after three review rounds). Lua, tests and docs; no Rust
change and no protocol change.
`COHERENCE.md` §1.2's canonical silence: a preconfigured server that is
not installed failed with no status message, no record and no modeline
marker, while tree-sitter highlighting kept working and masked it. Now
the status line names the command, the language and the errno; the
modeline reads `LSP:!` instead of nothing; and `M-x lsp.status` renders
a durable `*lsp*` panel.
Half of this was already built. `status_buffer_text()` and
`last_error()` have existed since M4.8, exposed to Lua and tested, with
no production caller and no buffer to render into — several doc
comments already referred to "the `*lsp*` buffer" as though it existed.
The reporting shape was likewise already adopted twice inside
`lsp.lua`; the canonical case was silent because nobody had converted
it.
Three tables with three lifetimes, because one cannot do the job:
`reported` is never cleared and includes the command, so repointing at
another missing executable reports again; `failures` is cleared by a
successful spawn so the panel goes quiet on recovery; and a
buffer-keyed projection feeds the modeline, because that provider runs
for every window on every paint and deriving an affinity key inside it
would invoke root resolvers during painting.
The memo is on the report, not the failure: the spawn is still
attempted on every file open, so installing the binary mid-session
recovers with nothing to invalidate.
Adds `tests/lsp_spawn_guidance_acceptance.rs` (16 pins) and a step-6
row to the journey ratchet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One blocking lifecycle gap, accepted. The registry's dispatch order was
read before specifying the fix rather than assumed.
`failed_attachments` is keyed by `tostring(buf)` and revision 3 never
said who deletes an entry, so killing a failed buffer leaked its
projection for the session — and that also made revision 3's sweep
bound ("at most the number of open buffers") false, since the table
could exceed the number of buffers that exist.
Nothing existing would have cleaned it incidentally. The LSP resource
reconciliation finds its work through `attachments_under`, which
iterates `attachments`, and a failed buffer has no attachment by
construction — that is the whole reason the projection exists. The gap
needed its own registration.
Revision 4 specifies:
- `pmacs.buffer.on_removed` registered once per projection, never per
failed attempt, since `attach_buffer` is reachable more than once for
the same buffer and per-attempt registration would stack callbacks —
the same unbounded-registrar shape with the leak moved rather than
fixed.
- The handle released on the success sweep, where the buffer is still
alive, but not from inside the removal callback: dispatch does
`callbacks.take(id)` and then iterates a local vector, so the entry is
already gone.
- Rename and delete CLEAR the projection rather than re-keying it.
The projection asserts "this buffer's server failed for affinity K",
and after a rename that is no longer known to hold — the new path may
be in a different project or none. Re-keying would assert a failure at
a location where none was observed, which is the error shape this arc
has been correcting throughout.
- The sweep bound now follows from the cleanup instead of sitting beside
it as an independent claim.
Adds acceptance 14-16 for kill, rename and delete, each asserting the
chosen behaviour rather than merely the absence of the forbidden one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two cleanups. All four accepted, and both blockers
verified by running Lua rather than by reading it.
Recovery was inconsistent across buffers sharing an affinity. Revision
2 cleared `failures[K]` on a successful spawn but cleared only the
succeeding buffer's projection, so: buffer A fails, buffer B succeeds
for the same key, `M-x lsp.status` reports nothing wrong, and A's
modeline still reads `LSP:!`. Revision 2's claim that the two tables
are "written and cleared at the same moment" was false for exactly the
cross-buffer case, which is the normal case for a project with more
than one file. Each projection now carries its affinity key and a
success sweeps every projection holding it. The new pin asserts on A,
not B — a version checking B passes on the broken implementation.
The markerless key had no Lua representation. `key_uri` is deliberately
nil and `t[nil] = v` raises "table index is nil" — confirmed under both
LuaJIT and Lua 5.4 — so the central markerless criterion was literally
unimplementable as written, and leaving it to implementation would have
produced two ad-hoc encodings for the two tables. The framing now
prescribes one key function used by both, with a `u`/`n` discriminator
that cannot collide with any URI and a NUL separator (Lua strings are
8-bit clean, checked).
Acceptance 10 could not have observed what it claimed: making the
command resolvable changes no state by itself, since `failures` is
cleared by a successful spawn. The pin now reattaches before pressing
`g`, and says why that step is load-bearing.
Also corrects the ledger heading, which still said revision 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1 on #203, two blocking findings, both accepted.
The compile-directory assertions used the suite's lexical `canon()`,
but `pmacs.project.detect` canonicalizes before walking
(`canonicalize_or_passthrough`, `src/project.rs:509-511`), so the
compile cwd is filesystem-canonical. On macOS `/var` is a symlink to
`/private/var` and the two spellings disagree — both macOS legs failed
while Ubuntu, where `/tmp` is not a symlink, stayed green.
Fixed with a `detected_root()` expectation, and pinned by a fixture
that launches through an explicit **symlink** so lexical and canonical
paths disagree on every platform. That matters more than the fix: the
original bite ran only on Linux, where nothing could make the two
differ, so no amount of local mutation testing would have caught this.
The new pin is the only one that goes red when the lexical expectation
is restored.
Second: `COHERENCE.md` §2's step-9 row was flipped to **Works** and the
handoff said Stage 1b-1 was **LANDED**, while this PR is open. §25 is
explicit that grades change only with landed evidence, never
aspirationally. Both now describe the real state — the row stays
Partial and names #203 as the open PR that closes it, §20 and the arc
list say "in flight", and the handoff says IMPLEMENTED with the PR
number.
The flip is not dropped, it is owned: the active-work lane records the
four places to change on merge, because an unowned doc flip is exactly
how this ledger's drift starts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, three major, one minor. All six accepted and verified in
the code before the doc changed.
The affinity key was misstated. `ensure_server` sets `key_uri` only
when the root came from config or a marker walk, so a markerless file
keys on nil and loose files across unrelated directories deliberately
share one server per language. Revision 1's "(root, language)" would
have split what the runtime shares and re-reported one failure once per
directory. The memo now uses the real key and the stage changes no
reuse behaviour.
Dedupe and current-failure state were conflated into one record, and
revision 1 said nothing about recovery. One record cannot do both jobs:
keep it and `*lsp*` shows a failure the user has already fixed; clear
it and the message returns on the next file open. They are now two
records with different lifetimes, the reported identity includes the
command so repointing config at a different missing executable reports
again, and recovery is pinned in both surfaces.
The modeline provider is a pure per-buffer projection by design — it
runs for every window on every paint. Revision 1's "read the failure
table" would have made it derive an affinity key, invoking user root
resolvers and project detection inside painting. The failure is now
projected per buffer at attach time and the segment stays one map
lookup, with a preservation pin that counts resolver invocations.
"Adopt listview's idiom" is replaced by requiring `pmacs.listview.open`
and naming what it guarantees, including `on_refresh` — without which
`listview.refresh` early-returns and `g` is a bound key that silently
does nothing.
The acceptance had no journey ratchet row despite the stage making step
6 real; `tests/journey_acceptance.rs` states that steps 6-12 join as
later stages make them real, and 1b-1 added step 9 the same way. The
end-to-end row is now acceptance 1 and the M4 pins stay beneath it.
Also refreshes the ledger's canonical-base anchor and recovery floor
from `7586905` to `fbcf235`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §1.2's canonical silence, and journey step 6: a
preconfigured-but-missing language server fails with no status message,
no record, and no modeline marker, while tree-sitter highlighting keeps
working and masks it.
Three findings shape the design, all verified in the tree:
- The reporting pattern is already adopted twice inside `lsp.lua`
itself — root-resolver failures and subscriber failures both report
through `pmacs.editor.set_status` with the `pmacs.error` arm riding
along. The canonical spawn failure at `:658-674` was simply never
converted, so this stage finishes an adoption rather than starting
one.
- Half the stage is already built and unwired.
`LspManager::status_buffer_text()` renders "the `*lsp*` status
buffer", `last_error(sid)` exists, and both are exposed to Lua and
tested — with no production caller, no `*lsp*` buffer and no command.
Several doc comments already refer to that buffer as though it
exists.
- COHERENCE's frequency note is wrong, and the correction decides the
hardest question. It records the failure as firing once per project
root; `LspManager::spawn` returns early *before* both
`status_tracker.ensure` and `clients.insert`, so a failed spawn
leaves no record, the affinity loop cannot see it, and the real rate
is once per file open. Hence the rule the stage adopts: memoize the
report, not the failure — the spawn is still retried, so installing
the binary mid-session recovers with nothing to invalidate.
Records one limitation rather than hiding it: `status_buffer_text`
renders from `self.clients`, which a failed spawn never enters, so the
durable surface cannot yet show the failure natively. The record stays
in Lua for this stage.
Also logs four stale `COHERENCE.md` §1.2 citations for correction when
the stage lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Implements `docs/journey-stage1b1-compile-defaults-framing.md`
(approved at revision 2). Lua, tests and docs; no Rust change and no
protocol change.
`C-c c` now runs `compile.run`, and the first prompt is prefilled from
the detected project kind through `pmacs.compile.defaults` — seeded
`rust = "cargo build"` and extensible from `init.lua`. `_last` still
wins, so a session that has compiled keeps its own command.
The prompt CAPTURES its directory rather than re-resolving it. Sharing
one resolver between the prompt and the run is necessary and not
sufficient: `pmacs.minibuffer.read` is asynchronous and nothing freezes
the active window while a prompt is open, so two calls to the same
resolver at two different moments are still two different answers — the
user could be offered `cargo build` for A and handed a run in B by
clicking away mid-prompt. This is Journey Stage 1a's `commit_to`
discipline on a smaller seam.
`pmacs.compile.defaults` is public and assignable, so the lookup is
guarded: a throwing `__index`, a non-string entry and a non-table
replacement all degrade to the pre-stage empty prompt and never
prevent compiling.
Only `rust` ships seeded. Rust has one answer; npm/yarn/pnpm,
make/cmake, and `go build` versus `go test` do not, and a wrong prefill
costs more than an empty one.
Adds eight step-9 rows to the journey ratchet and five module pins to
the compile suite. Corrects `COHERENCE.md`, which named a
`ProjectKind::Cargo` that does not exist — the variant is `Rust`, line
77 is its doc comment, and Lua only ever sees the tag string "rust".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1, one finding, accepted.
"A failed escalation is never retried by anything" was false.
`shutdown()`'s force-kill loop iterates the reap ledger with **no**
`!entry.killed` guard, so it does re-kill an entry the escalation arm
gave up on. The accurate claim is that no later *tick* retries it —
`tick_reap_ledger`'s escalation is guarded by `!entry.killed` and never
fires again for that group.
The overclaim collapsed two failure modes that this lane exists to keep
distinct: a failed escalation leaks the group until editor exit, where
one more attempt is made, while a failed `shutdown()` force-kill leaks
it past exit with nothing left to try. Narrowed in the framing, the
handoff, the active-work ledger and the test commentary.
The corrected claim was asserted in three documents and pinned by
nothing, so it gets a pin: a failed escalation marks the entry, the
survivor stays alive across ticks, and `shutdown()` — with no fault
planned, so its force-kill really lands — still reaps it. Bitten by
adding the missing `!entry.killed` guard to that loop: the new pin
fails and the other five stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Not a new design round. Every bet resolved, and one acceptance turned
out to be satisfiable vacuously.
Acceptance 2's in-drain clause said "the live descendant's named late
output absent" without saying how the descendant stays live. It does
not, by default: `poll_one` TERMs the whole group on leader exit. The
first fixture's marker was therefore absent on both paths, and the pin
would have stayed green with the collapse fixed.
Bets 1 and 2 hold — every site took a directed outcome without
restructuring, and every consequence is reachable, so the lane does not
shrink. Bet 3 resolves the shutdown coupling as real and measured. Bet
4 is falsified exactly as its own clause anticipated: `ProcessEvent` is
keyed by `ProcessId` while the ledger is keyed by pgid and is
deliberately independent of managed records, every production consumer
polls per known id, and `take_all_events` has no production consumer at
all. Q#RL3 is answered accordingly and reporting is parked as its own
lane.
Also records the four sites and the two reusable lessons in
`docs/agent-handoff.md` — that a seam for a background loop has to be
directed, and that an absence assertion is only as good as its
fixture's ability to produce the thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted and verified in the code
before the doc changed.
Sharing one cwd resolver between the prompt and the run does not
prevent drift. `pmacs.minibuffer.read` is asynchronous, nothing freezes
the active window while a prompt is open, and `run` re-resolved at
accept time — so the prompt could offer `cargo build` for A and execute
in B. Two calls to the same resolver at two different moments are still
two different answers. The interactive command now captures
`context()` and passes its `cwd` through to `run`. This is Journey
Stage 1a's `commit_to` discipline on a smaller seam: capture at request
time, never re-derive from whatever is ambient when the async work
lands.
No pin crossed the accept boundary. N1-N5 compared values the prompt
and the resolver had already agreed on, so a wrong directory inside
`on_accept` — exactly the defect above — passed every one of them. Two
pins now accept: N3 switches windows between prompt and RET and reads
the directory back two ways, and N4 accepts `cargo build` unedited in a
real Cargo fixture and asserts a clean exit.
N3's old falsifier was not discriminating: `project_root_of_active()`
already returns the innermost root, so re-detecting from it yields the
same kind and the pin would have stayed green. The mixed-fixture pin is
now N5 with a falsifier it actually catches, and says what it does not.
And §6.1 contradicted §3.1 — a Cargo workspace subdirectory holds no
`Cargo.toml` and correctly receives `cargo build`. The predicate is a
detected project, not a file in that one directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §20 Priority 1 names Journey Stage 1b as the golden
journey's remainder and it had no branch, no framing, and no lane —
the only §20 priority with nothing in flight. This frames the first
third of it: journey step 9, build or test the project.
Scope is a global `C-c c` for `compile.run` plus a first prompt
prefilled from the detected project kind instead of empty. Lua, tests
and docs; no Rust change, no protocol change. LSP spawn guidance and
the welcome buffer are separate stages (§7).
Three ground-truth findings the framing rests on:
- `ProjectKind::Cargo` does not exist. COHERENCE names it twice; the
variant is `ProjectKind::Rust` and line 77 is its doc comment. Lua
matches the tag string `pmacs.project.detect` already returns, so
the stage needs no new Rust primitive.
- The compile cwd is resolved inside `run`, after the prompt closes,
so a suggestion built in the command's `fn` would obey a different
rule than the run. The stage extracts one resolution and has both
consume it.
- The last-resort cwd is `std::env::current_dir()` at call time, which
in tests is the pmacs repo root — itself a Cargo project. Negative
pins are built so that fallback is never reached.
Records one limitation rather than fixing it: after `pmacs <dir>` the
active buffer is dired's and pathless, so the cwd falls through to the
process cwd. That needs COHERENCE §8's execution-location model, not a
reach into dired's private handle table.
Adds the journey lane to the active-work ledger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Refine the framing's in-drain test seam: a one-shot probe error cannot
survive the 50 ms quiescence interval, so the test-only override lasts
for one drain context and remains per-supervisor. Name the in-drain
SIGKILL as a non-observable local fact rather than adding an untestable
seam, and synchronize the revision-three ledger record.
The revision-2 commit shipped the framing alone. Its ledger edit was
generated by a script that aborted on a line-wrap mismatch before
writing, so `b881bd4` left the lane entry describing revision 1 — three
silent failures instead of four, no in-drain twin, no site-directed
seam, and the withdrawn "Bet 1 ships alone" staging.
Synchronized now: four sites including `final_drain_runtime` and its
distinct reader-cancellation consequence; journey step 9 rather than 8;
the `any_running()` precondition on the shutdown coupling; and the
first PR scoped as seam plus tests.
The failure mode is worth naming, because a partial write is exactly the
shape that leaves a document confidently wrong: the commit succeeded and
reported one file changed, which is easy to read as success when two
were intended.
Three blocking and two major findings, all accepted, all verified in the
code first.
- **§0 named the wrong journey step**, and contradicted Q#RL5 in the same
document. The ledger arms only for `spec.group`, which spawn rejects
for PTY mode, so no terminal reaches it. The only production
`group = true` caller is compile mode, making this journey step 9
(build/test) plus general pipe-process cleanup.
- **A fourth site was missed.** `final_drain_runtime` collapses every
probe errno to "dead" while no tick runs, discards its SIGKILL result,
and sets its own flag. It is not identical to the persistent ledger —
a later tick can retry that, this decision is terminal for the drain —
and its consequence differs: a false "dead" cancels the readers, so
the failure mode is truncated output rather than a leaked process. It
also constrains the seam, being a free function with no `&mut self`.
- **The staging contradicted itself**: "Bet 1 ships alone" against an
acceptance requiring failure-path tests, under one-branch/one-PR. It
was also wrong on its own terms — a seam with no tests does not show
it reaches the intended production calls. The first PR is now seam
plus behaviour-preserving tests, still diagnosis-only.
- **A generic seam is the wrong shape.** `shutdown()` calls
`self.signal(*id, SIGKILL)` before its ledger force-kill, so one
shared "next kill errno" would be consumed by the wrong call and the
test would pass while proving nothing. The seam is now site-directed
with four independently addressable outcomes, expressing more than one
pending result because the coupling test needs two at once, and with
fixture cleanup stated rather than assumed.
- **§1.3 overstated the loop coupling.** Early exit needs the ledger
empty AND `any_running()` already false. The precondition is now in
the ground truth and in Bet 3's fixture requirement.
Framing only. No code.
Unparked from PR #200's §5, which retired the premise justifying the
ledger's leniency and deliberately changed no disposition.
Scouting found three silent failures where #200 named two. A probe error
of any errno drops the entry and cancels escalation; a failed escalating
SIGKILL is recorded as a success and never retried; and `shutdown()`
discards its own force-kill result identically — on the path written
specifically to stop a leak at editor exit.
Two facts shape the lane. `shutdown()`'s final loop terminates when the
ledger empties, which happens through the same silent drop, so the probe
error that hides a leak can also end the cleanup loop early — the probe
cannot be made strict in isolation. And all three paths are untestable
today: `tick_reap_ledger` and `shutdown()` call nix directly and consult
no injection seam, unlike `signal()`'s `forced_kill_errno`, so every one
of the five existing ledger tests exercises only the success path.
Diagnosis first; no disposition change is proposed. Stage A of the signal
lane had three tolerance rules rejected across three revisions, each for
concluding something about one entity from something about another, on
this same data structure.
The framing states plainly that none of the three has been observed:
#200 saw an explicit SIGTERM fail in `signal()`, not a ledger call.
Framing only. No code.
`docs/active-work.md` auto-merged without conflict — this branch's lane
entry sits above the folding lane, clear of the six blocks #199 removed.
Verified rather than assumed: all lane headers checked afterwards.
The merge surfaced a stale statement on `main`. The process-signal lane
still read "PR #200 open, four review rounds closed, held for review",
which stopped being true when #200 merged as `a2a92bb`. Its arc is
complete — Stage A #176 and Stage B #200 both landed, and the items its
§5 parked become their own lanes — so under the rule #199 established
(a lane retires when its ARC is done, not when a PR merges) it is
removed and recorded under "Closed since the last snapshot".
Doing that here rather than deferring it: this PR already edits the
file, and leaving a known-false statement on `main` to keep a PR
single-purpose is the wrong trade. The alternative was letting it stand
until the reap-ledger lane, which is scoped next and inherits from the
very §5 list being retired.
Review round 5: one blocking, one major. Both documentation-only; the
runtime fixes from round 4 are unchanged.
**The pre-kill sample was overclaimed.** Acceptance 4 said the measured
group "describes the target that was attempted". It does not. `getpgid`
and `kill` remain separated by the same read-then-act window §1.5 is
built on, so the sample can be stale by the time the signal is
delivered. Moving it earlier removes a POST-HOC reading; it does not
make the reading contemporaneous. The criterion now says it records
pre-kill evidence about the attempted target, and adds an explicit
sentence forbidding any acceptance from claiming otherwise.
This is worth naming rather than quietly editing: the framing's whole
spine is that this telemetry establishes less than it appears to, and
round 4 fixed a real ordering defect and then inflated the fix's meaning
inside the document that governs it.
**`95897f7`'s commit message carries the same overclaim.** It is not
amended — force-pushing a branch under review to rewrite history would
hide the error rather than correct it. This message is the correction,
and the two read in sequence.
**A dead doc comment was documenting a constant.** The original
pre-Stage-B acceptance-1 block survived the rewrite and came to rest
above `const BASH`, so the file explained a string constant with the
sentence this entire lane exists to remove — "here they are asserted to
agree only because nothing has moved the terminal", describing a test
that no longer exists. Removed; only the BASH explanation remains.
Verified the overclaim is gone from the whole tree, not only the cited
line: no hits in `docs/` or `src/`.
Gates: 11 gates, 4,471 tests, zero failures.
Review round 4: three blocking, one major. All accepted.
**`measured_group` was sampled after the failure.** It ran inside
`signal_failure_report`, after the `kill` and after `observe_leader`,
while the framing and the function's own doc both said before. A
concurrent group change would have made the diagnostic report
post-failure state as evidence about the attempted target. It is now
sampled in `signal` before the kill and passed into the report, so the
field describes the target that was attempted rather than the state the
failure left behind.
**The Linux corroboration did not exercise the production lookup.** Its
helper read `portable_pty::process_group_leader` — the accessor this
lane stopped using on the signal path — so `pty_foreground_group` could
have fallen back on every call with every test still green. Forcing it
to always fall back demonstrates the hole exactly: the corroboration
fails, and the injected pin PASSES, because the injected tests supply
the group themselves and structurally cannot detect a broken lookup.
The helper now calls the production lookup, and the corroboration forces
only the kill so the report is built from a real terminal read. The
residual limitation is recorded rather than left to the green: on macOS
`pty_foreground_group` has no end-to-end coverage, because the platform
cannot produce the precondition.
**The framing did not update its own acceptance contract.** Revision 5
recorded Bet 1's falsification in the revision history and in the bet,
and left the normative criterion demanding the real-shell rewrite — the
"implementation quietly diverges from the contract" shape this project
already recorded as a lesson on #191/#188. Acceptance 1 now describes
the injected pin, 1a adds the corroboration and its macOS limitation,
and 4 states the sampling order. The ledger is synchronized: revision 6,
four commits, 4,471 tests, bash armed on Linux only.
**`TargetSource`'s doc had the wrong classification.** Two of the four
variants target the leader pid, not one, and the pid-versus-group split
does not line up with PTY-versus-pipe — which is why the fallback needed
its own variant rather than reusing `LeaderPid`.
Gates: 11 gates, 4,471 tests, zero failures.
CI falsified framing Bet 1. Both macOS legs reported
job control never moved the terminal off the leader
(leader=8542, foreground groups observed: [8542])
with the terminal staying on the leader for the entire 10s bounded wait.
Linux diverges reliably — 20/20 locally and green on both ubuntu legs —
so this is a platform difference rather than a flake, and rerunning past
it would have been wrong.
The framing named this outcome and prescribed the response, so that is
what ships rather than an improvised fix:
- The divergent case is pinned by INJECTING the foreground group at the
`signal_target` seam. Deterministic, runs on every platform. The
injection seam widens from failure-only to either outcome; the branch,
target choice, leader observation against the real child, and report
construction all remain production code.
- Verified still discriminating: the `leader_pid`-substitution mutation
fails it, `target=-1707909` against an expected `-1707910`. That was
the whole point of the original rewrite and it survives the fallback.
- The real shell is retained as corroboration in
`job_control_really_diverges_the_foreground_group`, Linux-only. It
skips on macOS by PLATFORM CHECK rather than by arming: the
precondition genuinely does not hold there, so running it would assert
a false claim about macOS instead of finding a bug.
- Framing revision 5 records the falsification and states exactly how
the injected pin is weaker — it proves the target is read from the
lookup rather than substituted from the leader; it does not by itself
prove any real shell produces that divergence.
`PMACS_REQUIRE_BASH` moves to Linux-only. The earlier reasoning for
arming both platforms — macOS is where the failures happen, so Linux-only
leaves it dark where it matters — was right about the diagnostic and
wrong about this test, which cannot produce its precondition on macOS at
all. Arming it there made a missing binary fatal for a test that can
never run. The measurement is recorded in ci.yml and the README so it is
not re-derived.
Gates on this tree: 11 gates, 4471 tests, zero failures.
One blocking and two major findings, all accepted.
- The isolated gate still inherited `PMACS_STATE_HOME`, which wins over
`XDG_STATE_HOME` in `user_state_dir` (`src/state.rs:47-68`), so
controlling the four XDG storage variables did not isolate state on a
machine that exports it. The contract now names the exact five
variables rather than counting roots.
- Q#TI5 still called self-spawn the durable regression guard after
revision 3 had accepted that it proves behaviour, not continued
adoption. It now names both guards and their different jobs: self-spawn
is the behavioural proof, the checked source inventory is the adoption
proof.
- The active-work lane still said revision 2 and still prescribed an
isolated `XDG_CONFIG_HOME` alone; both are synchronized, and the stale
config-only gating note elsewhere in the ledger is corrected to say it
isolates the observed symptom rather than the gate.
Framing only. No code.
`docs/active-work.md`'s snapshot header was the only conflict; main's
absorption version supersedes this branch's. `docs/agent-handoff.md`
auto-merged, and both sides were verified present afterwards rather than
assumed: the absorption's `4cd4a7b` anchor and its new landed-lane
entries, and this branch's macOS occurrence with its run link.
This lane's ledger entry is updated from "revision 4 awaiting review" to
its actual state — implemented, gated, PR open — since the framing was
approved and both implementation commits have landed on the branch.
The full gate suite is re-run on the merged tree rather than inherited
from the pre-merge head. The absorption is docs-only, so a clean merge
is very likely correct — which is exactly when inheriting a green would
be most tempting and least justified.
Three blocking and two major findings, all accepted. Two were verified
by running the thing rather than reading it.
- **Journey isolation had no executable mechanism.** Cargo launches each
integration-test binary with the caller's environment, and a binary
cannot re-point its own roots before its tests run — the `set_var`
prohibition applies to itself. Revision 2's "isolated by its launched
environment" therefore assumed the external wrapper this lane exists
to delete. Now named and pinned: each journey test re-execs
`current_exe()` with `--exact`, a marker, and controlled roots; the
child runs the real body against the ambient `EditorState::open`, so
the production-entry-point ratchet is untouched while the roots are
contained. The same helper serves the hostile-environment check.
- **One self-spawning test is not a ratchet.** It proves the seam works
and cannot notice a raw `EditorState::new()` added to another binary
later. A checked source inventory, falsifiable by adding an ambient
constructor, is now acceptance 12.
- **The root list and the gates disagreed with the audit.** The scope is
now explicit — bootstrap STORAGE roots only (config, data, state,
cache). `HOME`'s non-storage semantics are excluded by decision, not
omission: `expand_tilde` resolves user-entered `~` and
`find_file_acceptance` pins it deliberately, so redirecting `HOME`
would retarget a user-facing feature. `XDG_RUNTIME_DIR` addresses
sockets, not stored data.
- **The gate instruction itself was insufficient**, and this is the
finding with immediate consequences: isolating only `XDG_CONFIG_HOME`
stops the reads and leaves the write path open. Every local gate run
in this repository today had that hole.
- **The count was one high and the ledger overstated it further.** 65
files call the constructor; 66 mention it. The 66th, `m5_6_acceptance`,
mentions it only to say it deliberately does not use it — making it
the third place in the suite documenting the `cfg(test)` gap. The
ledger's "all 96 test files load the real config" was false.
- **The recovery command did not work**, verified by running it:
`git worktree add <path> <remote-only-branch>` fails with `fatal:
invalid reference` after a bare fetch. Replaced with the explicit
tracking-branch form.
Framing only. No code.
Four blocking and two major findings, all accepted, all verified in the
code before acceptance. The lane's scope changes: it is about ambient
roots, not about init.lua.
- The read-only assumption was already false. `EditorState::new`
materializes bundled packages unconditionally and before config
loading, into `XDG_DATA_HOME` or `$HOME/.local/share`, and
`materialize_all` creates directories. Confirmed on the development
machine: `~/.local/share/pmacs/builtin-packages/` exists with v0.1.0
and v1.0.0. This upgrades the lane from "local gates lie" to "tests
write into real user data".
- The population count was wrong and its stated method did not match the
command that produced it: 18 from a grep for `Editor::new`, which does
not match the real constructor `EditorState::new`. 66 of 96 files
construct an editor directly.
- File-level classification cannot work: 5 files are both in-process and
spawned. Classification moves to construction sites.
- The seam must cover `EditorState::open`, which calls `Self::new()`
directly, while `journey_acceptance` requires that exact public entry
point to avoid a dead-production-path test. Resolved by isolating
journey through its launched environment rather than a different call.
- Isolated construction must still return `is_init_complete() == true`.
Config loading and `set_init_complete()` share one block, and
`m8_2_acceptance.rs:75` documents its dependence on integration-test
construction being init-complete — the `cfg(test)` gap is load-bearing
in that one respect.
- Revision 1 both proposed and parked a hostile-config CI leg. Resolved
in favour of a test-binary self-spawn, which travels with the test
rather than the workflow file, and which now also asserts the hostile
root is unmodified afterwards.
- The lane is recorded in `docs/active-work.md`, which revision 1 omitted
despite the volatile-work protocol requiring it.
Also documents six ambient roots where the shared daemon harness sets
two, and why setting HOME only isolates a root whose XDG variable is
unset — the harness's apparent adequacy is a property of one developer's
environment.
Framing only. No code.
Eight PRs merged on 2026-07-29/30 (#188, #190, #191, #194, #195, #196,
#197, #198) and `docs/active-work.md` had drifted to 1,854 lines, most of
it describing work already on `main`. Rule 4 permits removal once a
lane's facts reach `docs/agent-handoff.md`; this does both halves in one
commit so the precondition is satisfied rather than deferred.
Handoff §1: the head-of-`main` anchor moves from `6c9e765` to `4cd4a7b`
and its date from 2026-07-28 to 2026-07-30. New entries for the three
implementation PRs that had no record — #190's pre-filesystem delete
refusal, #191's Stage 1 adoption of the generated-buffer contract, and
#196's dired Stage 2a — plus #188 recorded as landed rather than
proposed.
Ledger: six merged lanes removed (781 lines). Two lanes are NOT removed
and are rewritten to their remaining plan instead, because their arcs are
unfinished: generated-buffer immutability (Stage 1 merged, Stage 2 not
started) and bottom-panel (Stage 2 complete, Stage 3 ahead). The
bottom-panel block said so in its own text — "this lane is not removed at
2B-3's merge" — so a removal keyed on "the PR merged" would have
discarded live planning. The header now states the rule as ARC-done
rather than PR-merged.
Two method notes are recorded in the ledger because they nearly cost
real work:
- A census by PR number is a proxy, not a measurement. Counting `#NNN`
in the handoff reported that five of these lanes had no record at all.
Counting by content found most already documented, with the real gap
being the implementation PRs while their framings were recorded. The
absorption written from the first count would have duplicated existing
entries.
- #194 and #195 keep their lessons in §3 and §5 rather than §1, which is
why a number search finds them once each. Sufficient under rule 3:
durable knowledge needs a home, not a required section.
Docs only. No code, no protocol change.
Integration tests read the developer's real `~/.config/pmacs/init.lua`.
The suite is green in CI and deterministically red on any machine with a
real config, attributed to whatever branch is checked out — 11 of 67 in
`compile_mode_acceptance` on 2026-07-30, 67/67 with an isolated
`XDG_CONFIG_HOME`.
The mechanism is that `src/editor.rs:770` guards config loading with
`#[cfg(not(test))]`, which is set only when compiling the crate's own
unit tests. An integration test links pmacs as an ordinary dependency, so
the guard is inactive for all 96 of them. The hazard was identified and a
mitigation was written; its scope does not match the threat.
The obvious fix is unavailable: `std::env::set_var` is unsafe and the
crate forbids unsafe, which the repo already knows —
`Installer::root_override` exists for exactly this reason.
Framing only, awaiting review round 1. No code changes.