From e26ddd5ec366550c43eb4fddf9b0a33430ea5752 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 30 Jul 2026 18:15:58 -0400 Subject: [PATCH] =?UTF-8?q?docs(journey):=20frame=20Stage=201b-1=20?= =?UTF-8?q?=E2=80=94=20make=20building=20discoverable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 ` 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 Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T --- docs/active-work.md | 60 ++ ...urney-stage1b1-compile-defaults-framing.md | 563 ++++++++++++++++++ 2 files changed, 623 insertions(+) create mode 100644 docs/journey-stage1b1-compile-defaults-framing.md diff --git a/docs/active-work.md b/docs/active-work.md index 09c5c9d..9b26399 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -253,6 +253,66 @@ If it does not, stop and repair the remote/fetch configuration. never been enforced. Any CI job that compiles the `crdt` targets has to fix them first or it will be red on arrival. +## Journey lane (P1) — STAGE 1a MERGED; STAGE 1b-1 FRAMING OPEN, revision 1 + +- **Branch `journey-stage1b1-compile-defaults`**, worktree + `../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`. + **Framing only; no code, no PR yet.** + `docs/journey-stage1b1-compile-defaults-framing.md` revision 1, no + review rounds yet. +- **Why this lane exists.** `COHERENCE.md` §20 Priority 1 names Journey + Stage 1b as the golden journey's remainder — "the compile binding + + Cargo defaults, LSP spawn guidance, and the welcome buffer" — and it + had no branch, no framing, and no lane. It is the only §20 priority + with nothing in flight. +- **What 1b-1 is.** Journey step 9 only: a global `C-c c` for + `compile.run`, and a first prompt prefilled from the detected project + kind instead of empty. Lua, tests, and docs; **no Rust change and no + protocol change**. 1b-2 (LSP spawn guidance, step 6) and 1b-3 (welcome + buffer, step 4) are separate stages — see the framing §7 for why the + three are not one PR. +- **`ProjectKind::Cargo` does not exist.** `COHERENCE.md` names it twice + (§2's step-9 row and §20 Priority 1); the variant is + `ProjectKind::Rust` (`src/project.rs:78`) and line 77 is its doc + comment. The audit read the comment. Lua matches on the **tag string** + `"rust"` that `pmacs.project.detect` returns, so no Rust primitive is + needed. The PR corrects both COHERENCE sites. +- **The suggestion and the run must share one cwd resolution.** Today + the cwd is computed *inside* `pmacs.compile.run` + (`builtin/runtime/compile.lua:765`), after the prompt has closed, so a + suggestion computed in the command's `fn` would obey a different rule + than the run. The stage extracts it and exposes + `pmacs.compile.context()`, consumed by both. +- **The trap the acceptance is designed around.** The last-resort cwd is + `std::env::current_dir()` evaluated at call time + (`pmacs-protocol/src/message.rs:1703-1706`), so in tests it is the + **test runner's cwd — the pmacs repo root, itself a Cargo project**. + `compile_mode_acceptance.rs:1721` already pins this. A pin asserting + "no Cargo suggestion" that reaches the fallback would report pmacs's + own `Cargo.toml`. Negative pins therefore use a fixture carrying a + *different* marker so the fallback is never consulted; + `set_search_boundary` does **not** help here, because it only clamps a + walk that starts below the boundary. +- **Known limitation, deliberately not fixed.** After `pmacs ` the + active buffer is dired's and **pathless** (`pmacs.buffer.create` never + assigns a path; dired compensates through its own module-local + `handle_for_buffer`, `dired.lua:205-217`), so the cwd falls through to + the process cwd. Launched from elsewhere that is the wrong directory. + The fix is `COHERENCE.md` §8 (First-Class Execution Locations), a + model gap; reaching into dired's private table for one string would + add an interaction island. +- Recovery from a clean checkout — **the two-argument form does not + work** (`git worktree add ` fails with + `fatal: invalid reference`, because after a bare fetch no local branch + exists): + + ```sh + git fetch githubsucks + git worktree add ../pmacs-journey-1b1 \ + -b journey-stage1b1-compile-defaults \ + githubsucks/journey-stage1b1-compile-defaults + ``` + ## Generated-buffer immutability lane (Arc: workbench primitives) — STAGE 1 MERGED; STAGE 2 IS NEXT **Framing #188 (revision 7) and Stage 1 #191 are both on `main` @ diff --git a/docs/journey-stage1b1-compile-defaults-framing.md b/docs/journey-stage1b1-compile-defaults-framing.md new file mode 100644 index 0000000..b79ac64 --- /dev/null +++ b/docs/journey-stage1b1-compile-defaults-framing.md @@ -0,0 +1,563 @@ +# Journey Stage 1b-1 — make building discoverable + +**Status: framing, rev 1 — awaiting approval.** +**Serves `COHERENCE.md` §2 (the golden product journey, step 9), §19 +(coherence acceptance tests), §20 Priority 1.** + +## 0. Revision history + +- rev 1 (2026-07-30) — first framing. Scouted against `githubsucks/main` + @ `22df6ab` (test-ambient isolation framing #201). + +## 1. What this stage is, and what it is not + +`COHERENCE.md` §20 Priority 1 names the remainder of the journey arc: + +> Journey Stage 1b is the named remainder: the compile binding + Cargo +> defaults, LSP spawn guidance, and the welcome buffer. + +Those three are unrelated in mechanism, in failure mode, and in cost. +This stage takes **only the first**: journey step 9, *build or test the +project*. LSP spawn guidance (step 6) and the welcome buffer (step 4) +get their own stages; §7 states the split and why. + +Nothing here is a new Rust primitive. The stage is Lua, tests, and docs. + +## 2. Ground truth + +Every claim below was read in the tree at `22df6ab`, not inferred from a +name. + +### 2.1 `compile.run` has no binding, and the docs say so + +`compile.lua` binds four global sequences (`:1183-1188`): `M-g n`, +`M-g p`, `C-x \``, `M-!`. `bind_slot_keys` (`:220-240`) binds +`RET`/`n`/`p`/`q`/`C-c C-k`/`g` at **buffer** scope inside a generated +buffer. `builtin/keymaps/default.lua` never mentions compile. There is +no third keymap file — `builtin/keymaps/` contains `default.lua` alone. + +`docs/keybindings.md:191-192` states it outright: + +> `compile.run` and `compile.recompile` are available through `M-x`; no +> global key is assigned to them. + +### 2.2 The first prompt is empty + +```lua +-- builtin/runtime/compile.lua:1131-1144 +name = "compile.run", +fn = function() + local last = pmacs.compile._last + pmacs.minibuffer.read { + prompt = "Compile command: ", + history = "compile", + initial = last and last.cmdline or "", +``` + +`_last` is set only by a completed `pmacs.compile.run` (`:1094`), so on a +fresh session `initial` is `""`. The user is asked what to build and +given nothing to build with. + +`initial` does reach the user: `Minibuffer::begin` calls +`replace_contents(&session.initial)` (`src/minibuffer.rs:106-107`), and +`contents()` (`:129`) reads it back. The prefill mechanism works; it is +handed an empty string. + +### 2.3 `ProjectKind::Cargo` does not exist — the variant is `Rust` + +**`COHERENCE.md` is wrong here, in two places.** Its §2 step-9 row and +its §20 Priority 1 paragraph both say "`ProjectKind::Cargo` existing +(`src/project.rs:77`)". Line 77 is a doc comment; the variant on line 78 +is `Rust`: + +```rust +// src/project.rs:77-78 +/// A Cargo workspace (`Cargo.toml`). +Rust, +``` + +The marker that produces it is `{ name: "Cargo.toml", kind: +ProjectKind::Rust, is_directory: false }` (`:148-150`). The audit read +the comment and named the comment. This framing corrects both COHERENCE +sites (§8). + +The correction is not cosmetic: it decides what the Lua side matches on. + +### 2.4 Lua already sees the project kind — as a tag string + +`pmacs.project.detect(path)` returns a table (`src/lua_bindings/mod.rs:11683-11694`): + +```rust +t.set("root", root.display().to_string())?; +t.set("kind", kind.tag())?; +t.set("language_id", kind.default_language_id())?; +``` + +`ProjectKind::Rust.tag()` is `"rust"` (`src/project.rs:101`). So the Lua +key is the string `"rust"`, and **no Rust change is needed to learn the +project kind**. + +`detect` accepts a directory as well as a file: `walk_for_marker` uses +`start` unchanged when it is not a file (`src/project.rs:225-229`). + +### 2.5 The cwd is resolved *inside* `run`, after the prompt has closed + +```lua +-- builtin/runtime/compile.lua:765 +local cwd = opts.cwd or project_root_of_active() or daemon_working_directory() +``` + +The interactive `fn` that builds the prompt cannot see this. Any +suggestion computed in the `fn` today would be computed from a different +rule than the one the run obeys — which is the failure this stage must +not ship. §3.1 is about closing that gap before adding the suggestion, +not after. + +### 2.6 The active buffer at the journey moment is **pathless** + +`project_root_of_active()` (`:600-609`) needs `buf:path()`. Directly +after `pmacs .` the active buffer is dired's, created by +`pmacs.buffer.create(name)` (`dired.lua:506`) — a name, never a path. +`buf:path()` returns `file_path()` mapped to a string +(`src/lua_bindings/mod.rs:1242-1248`), so it is `nil`. + +This is not a guess. dired itself compensates, and its own helper is the +evidence: + +```lua +-- builtin/runtime/dired.lua:205-217 +local function current_directory() + local buf = pmacs.window.buffer() + if buf ~= nil then + local ok, path = pcall(function() return buf:path() end) + if ok and path then ... end + local h = handle_for_buffer(buf) -- <-- the pathless case + if h then return h.path end + end + return canonicalize(".") +end +``` + +`handle_for_buffer` is a **module-local** table. Compile cannot reach it, +and reaching for it would make compile depend on dired — a new +interaction island for one directory string. §5 decides against it and +§6 states the residual gap. + +### 2.7 The last-resort cwd is the *process* cwd, evaluated at call time + +`daemon_working_directory()` reads +`pmacs.instance.identity().working_directory`, which is: + +```rust +// pmacs-protocol/src/message.rs:1703-1706 +working_directory: std::env::current_dir() + .ok() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default(), +``` + +Two consequences, both load-bearing: + +- **In production**, `pmacs .` is launched from the project directory, so + the fallback happens to be right. `pmacs ~/code/proj` launched from + `~` resolves to `~`, and the fallback is wrong. That is a pre-existing + step-9 defect this stage does not fix (§6.1). +- **In tests**, it is the *test runner's* cwd — the pmacs repo root, + **which is a Cargo project**. `compile_mode_acceptance.rs:1721-1733` + already pins exactly this (`r1f8_inherited_cwd_resolves_to_the_daemon_working_directory`), + and `:1382` already warns that "fallback would search the test + process's cwd". Any pin that asserts *no* Cargo suggestion is at the + mercy of where `cargo test` was invoked unless it is designed to never + reach the fallback. §5.2 designs for that; it is the single largest + trap in this stage. + +### 2.8 `C-c c` is free, and `C-c` is not CUA copy + +Global `C-c` sequences in the tree: `C-c a`/`f`/`h`/`H`/`i`/`o`/`r`/`s`/`y` +(`lsp.lua`), `C-c t` (`terminal.lua:203`), `C-c @ …` (folding). +`C-c c` is unbound. Copy is `M-w` (`builtin/keymaps/default.lua:114`) — +the CUA trio was deliberately not taken — so binding under the `C-c` +prefix collides with nothing in the default map. + +`terminal.lua` binds its own `C-c t`; `lsp.lua` binds its own `C-c o`. +A runtime module owning its global binding is the established pattern, +and this stage follows it rather than editing `default.lua`. + +### 2.9 dired already decided the prompt shape + +`C-x d` prefills its prompt and **deliberately refuses a completion +source** (`dired.lua:749-756`): a candidate list makes RET-on-empty open +whatever sorts first, and a selected candidate shadows typed text. The +same reasoning applies to a compile command, so this stage prefills and +adds no completion source. + +## 3. Design + +### 3.1 One resolution, consumed twice + +Extract the `:765` expression into a helper and expose a read-only view +of it: + +```lua +local function resolve_cwd(explicit) + return explicit or project_root_of_active() or daemon_working_directory() +end + +--- Where the next compile would run, and what kind of project is +--- detected *from that directory*. Public getter (API conventions): +--- `{ cwd = string|nil, kind = string|nil }`. +function pmacs.compile.context(explicit_cwd) +``` + +`run` calls `resolve_cwd(opts.cwd)`; the interactive `fn` calls +`pmacs.compile.context()`. **The suggestion is therefore a function of +the directory the command will execute in**, and the two cannot drift. + +`kind` is `pmacs.project.detect(cwd).kind` — detection *from* the cwd, +which is not the same as "the cwd is the root": `opts.cwd = /proj/src` +in a Cargo workspace yields kind `rust` with root `/proj`. That is +correct (cargo works from a subdirectory) and is stated so a reader does +not read `kind` as "this directory is a project root". + +`project_root_of_active()` already calls `detect` once to get a root; +`context` then calls it again on the result. The second call is +redundant in that branch and is kept anyway, because the alternative is +two different rules for where `kind` comes from depending on which +branch produced the cwd. One rule, stated as: **`kind` is always +`detect(cwd)`.** + +### 3.2 The default table + +```lua +--- Default compile command per detected project kind, keyed by the +--- tag `pmacs.project.detect` returns. Assign into this table from +--- `init.lua` to add or override one. +pmacs.compile.defaults = { rust = "cargo build" } +``` + +**Only `rust` is seeded, and that is a decision rather than an +omission.** Rust has one answer. Node does not (`npm` / `yarn` / `pnpm` +/ a `scripts.build` that may not exist); Python does not; Go's build and +test are different commands with equal claim. A wrong prefill is worse +than an empty one — the user must first delete it, then type. The table +exists so a user or a package can add the answer *they* know. + +`cargo test` is reachable by editing the prefill. The prompt prefills +one string; offering both would need a candidate list, which §2.9 +already ruled out for this prompt. + +### 3.3 Precedence at the prompt + +``` +last.cmdline -- unchanged; a session that has compiled keeps its command + or defaults[kind] -- new + or "" -- unchanged +``` + +`last` winning is deliberate: a user who ran `cargo test` once should get +`cargo test` back, not be reset to `cargo build`. This is a preservation +pin (§5.4, P1), not an accident of ordering. + +### 3.4 The table is user-writable, so reading it is guarded + +`pmacs.compile.defaults` is public and assignable, which means a +metatable with a throwing `__index`, a non-string value, or a +non-table replacement all have to be survivable. The module already +holds this discipline for a hostile rule container (`validated_rules`, +round-2 finding 3: shell-command "must neither surface compile-rule +warnings nor fail on a hostile rule container"). + +The lookup therefore runs under `pcall` and accepts a value only when it +is a non-empty string. Anything else yields `""` — the pre-stage +behavior. **A broken `defaults` degrades to today's prompt; it never +prevents compiling.** + +### 3.5 The binding + +```lua +pmacs.keymap.bind { scope = "global", sequence = "C-c c", command = "compile.run" } +``` + +In `compile.lua`, beside the four existing global binds (§2.8). + +Two reachability limits it inherits, both pre-existing and both stated +rather than discovered later: + +- Inside a **terminal** window `C-c` is consumed as the escape key, so + `C-c c` does not arrive. `COHERENCE.md` §2 already records this for + `C-c t`. `M-x compile.run` still works there. +- The repl package binds `C-c` at **buffer** scope + (`builtin/packages/repl/init.lua:300`), which shadows the global + prefix in a repl buffer. Same escape hatch. + +`compile.recompile` gets no global binding: `g` in `*compilation*` +already covers rerun, and adding a second global chord for it is scope +this stage has no journey argument for. + +## 4. What this changes for the journey + +Walking `COHERENCE.md` §2 on a Rust project, unconfigured: + +| | before | after | +|---|---|---| +| launch | `pmacs .` lists the directory (Stage 1a) | unchanged | +| open a file | `RET` visits it (Stage 1a) | unchanged | +| build | *no key exists*; `M-x compile.run` → empty prompt | `C-c c` → `Compile command: cargo build` | +| accept | — | RET runs it in the detected root | +| errors | `M-g n` walks them | unchanged | + +Step 9's verdict row moves from **Partial** to **Works**; step 10 stops +being gated on the user already knowing `M-x compile.run`. + +## 5. Acceptance + +### 5.0 Two labels, as Stage 1a established + +**N** — new behavior, must fail on full revert. **P** — preservation, +legitimately green on the pre-image, falsified only by a named targeted +mutation. Stage 1a's §6.0 is the reason the distinction is kept: an +equivalence assertion between two implementations that already agree +proves nothing. + +Every pin below names the mutation that falsifies it. `scripts/bite` is +run against the suite before the PR opens. + +### 5.1 Where the pins live + +- **`tests/journey_acceptance.rs`** gains a step-9 section. This file is + the ratchet — *stages add rows, none removes them*. Its pins go + through the **real** entry points: `EditorState::open` on a directory, + a dispatched `RET`, a dispatched `C-c c`. Nothing calls + `pmacs.compile.context()` directly in this file. +- **`tests/compile_mode_acceptance.rs`** gains the module-contract pins: + `context()`'s shape, and the hostile-table guards. + +### 5.2 The fixture problem, and its only safe shape + +Per §2.7, the last-resort cwd is the test runner's cwd, and the test +runner's cwd is a Cargo project. **A pin that asserts "no `cargo build` +suggestion" and reaches the fallback will report the pmacs repo's own +`Cargo.toml` as the fixture's answer.** It would pass or fail on where +`cargo test` was invoked from. + +The negative pins are therefore built so the fallback is **never +consulted**: the fixture carries a *different* project marker, so +`project_root_of_active()` resolves inside the fixture and returns +before `daemon_working_directory()` is reached. + +``` +rust_fixture/ Cargo.toml, main.rs +mixed_fixture/ Cargo.toml, main.rs + sub/ package.json, index.js +``` + +`mixed_fixture` is what makes N4 discriminating: the file opened is +`sub/index.js`, the nearest marker is `package.json` (kind `node`, no +default), and the *outer* marker is Cargo. A suggestion computed from +anything other than the resolved cwd — the outermost marker, the launch +directory, the process cwd — produces `cargo build` here. The correct +implementation produces `""`. + +`pmacs.project.set_search_boundary` is set to the fixture root in each +test that detects, so a stray marker above the tempdir (a developer's +`/tmp/.git`) cannot leak in. Note what it does **not** do: it clamps the +upward walk from a start *below* the boundary, so it is no protection at +all for the fallback path, whose start is the repo root. That is why the +fixture shape above, not the boundary call, is the actual defense. + +### 5.3 New-behavior pins + +**N1 — the chord reaches the command.** +Launch on `rust_fixture`, `RET` on `main.rs`, dispatch `C-c c`; assert +`pmacs.minibuffer.is_active()`. +*Falsifier:* remove the `keymap.bind` line — the chord is unbound, no +session opens. +*Why it is separate from N2:* a prefill assertion alone would stay green +if the binding were removed and the prompt were opened some other way. +The binding is the thing COHERENCE says is missing; it gets its own pin. + +**N2 — the prompt is prefilled from the project kind.** +Same walk; assert `pmacs.minibuffer.contents() == "cargo build"`. +*Falsifier:* drop the `defaults[kind]` term from the precedence chain — +contents become `""`. + +**N3 — the prefill is executable as offered.** +Same walk; assert the prompt contents are exactly +`pmacs.compile.defaults[pmacs.compile.context().kind]`, and that +`context().cwd` is the fixture root. +*Falsifier:* compute the prefill from `project_root_of_active()` instead +of from `context()`. In the plain fixture the two agree, so this pin +alone is **not** sufficient — which is exactly why N4 exists. + +**N4 — the suggestion follows the directory the run will use.** +Launch on `mixed_fixture`, `RET` into `sub/index.js`, dispatch `C-c c`; +assert contents are `""` and `context().kind == "node"`. +*Falsifier:* any rule that is not "detect from the resolved cwd" — take +the outermost marker, or the launch directory, or the process cwd — all +yield `cargo build`. +This is the pin that carries §3.1's whole claim. + +**N5 — `context()` is total in a launched session.** +Property, not a constant, because the value is environment-dependent +(§2.7): after a launch, `context().cwd` is non-nil, and `context().kind` +equals `pmacs.project.detect(context().cwd)`'s kind (both nil, or both +the same string). Asserted with a dired buffer active — the pathless +case — so the fallback branch is the one under test. +*Falsifier:* make `resolve_cwd` return `nil` when the active buffer has +no path, i.e. drop the `daemon_working_directory()` term. +*What it deliberately does not assert:* which directory. Pinning that +would pin the test runner's cwd. + +### 5.4 Preservation pins + +**P1 — `_last` still outranks the kind default.** +Run a compile with an explicit cmdline that is not `cargo build`, then +open the prompt in the Rust fixture; assert the contents are the last +cmdline. +*Targeted mutation:* reorder the precedence chain to put `defaults[kind]` +first. Green on the pre-image (there was no default), red under the +mutation. + +**P2 — a hostile `defaults` cannot break compiling.** +Three cases in `compile_mode_acceptance`: `defaults` replaced by a +non-table; a `__index` metatable that raises; a non-string entry for +`rust`. In all three the prompt opens with `""` and `compile.run` still +executes a typed command. +*Targeted mutation:* remove the `pcall` / type guard — the raising case +propagates out of the command and no prompt opens. + +**P3 — the existing compile bindings are unchanged.** +`M-g n`, `M-g p`, `C-x \``, `M-!` still dispatch to their commands, and +`compile.run` is still reachable through `M-x`. +*Targeted mutation:* the new `bind` call written as an `unbind`+`bind` +pair over the wrong sequence. + +### 5.5 Gates + +The full suite from `CLAUDE.md`, plus `compile_mode_acceptance`, +`journey_acceptance`, `dired_acceptance`, and `find_file_acceptance` as +the touched suites. Local runs must control all five bootstrap-storage +variables (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, +`XDG_CACHE_HOME`, `PMACS_STATE_HOME`) — the ambient-root isolation lane +(#201) is **framing only**, so the workaround is still required and +`compile_mode_acceptance` is one of the suites that goes red without it. + +## 6. Named limitations — stated, not discovered later + +### 6.1 `pmacs ` from elsewhere still resolves the wrong cwd + +Launched as `pmacs ~/code/proj` from `~`, the active buffer is dired and +pathless (§2.6), so the cwd falls through to the process cwd `~` +(§2.7). Compile would then run in `~`, and — consistently, since §3.1 +ties them — suggest nothing. + +**This stage does not fix it.** The fix needs a notion of "the directory +this session is working in" that is not any one module's private table, +which is `COHERENCE.md` §8 (First-Class Execution Locations) — a model +gap, not wiring. Reaching into dired's `handle_for_buffer` would make +compile depend on dired for one string and add exactly the kind of +interaction island §6 of COHERENCE is about. + +What this stage does guarantee is that the failure is **coherent**: the +suggestion describes the directory the command will run in, whatever +that directory turns out to be. The user is never offered `cargo build` +for a directory with no `Cargo.toml`. + +N5 pins the property; it deliberately does not pin the value. + +### 6.2 The default is per-kind, not configurable through the registry + +`pmacs.compile.defaults` is a plain Lua table, not a registered setting. +It cannot be one: `ConfigValue` is four scalars, so a kind→command map +is not expressible. A scalar `compile.default-command` that overrides +the table *is* expressible and is **deferred, not skipped** — it is one +more precedence step and a registry entry, and it belongs with the +config-adoption work (§20 Priority 6) rather than bolted on here. + +### 6.3 One binding does not retire the inversion + +`COHERENCE.md` §2 keeps a standing observation verbatim: keybinding +coverage is inverted relative to frequency, with `C-c @ C-M-s` bound +while opening a file, opening a terminal, and running a build were not. +Two of the three have been answered (#162, #173); this stage answers the +third, and §8 updates the paragraph accordingly. The *quote* stays as +written, because it names a bias in how new work gets bound rather than +three omissions. + +## 7. Staging — why 1b is split + +`COHERENCE.md` §20 bundles three items under "Stage 1b". They share a +priority and nothing else: + +| | subsystem | shape | risk | +|---|---|---|---| +| **1b-1** (this) | compile + project | wiring, Lua only | low | +| 1b-2 | LSP lifecycle | a failure that is currently *silent* (§1.2) must become visible without becoming noise | medium | +| 1b-3 | startup buffer | new content, plus §18's `C-h`-deletes-a-word problem | low, but touches the default keymap | + +One feature, one branch, one PR. 1b-2 is the hard one — the silence +asymmetry is a design question about *when* to speak, not a wiring +question — and bundling it with a keybinding would hold the cheapest +journey fix in the tree behind the most contested one. + +## 8. Coherence impact + +Per `CLAUDE.md` and `COHERENCE.md` §20's standing process change. + +- **Journey steps touched:** 9 directly (Partial → Works); 10 indirectly + — it was "gated entirely on step 6 or 9 succeeding first". +- **Interaction islands:** none added. The prompt is the existing + minibuffer; the binding joins the existing `C-c` prefix; the kind + comes from the existing detector. `pmacs.compile.defaults` is an + extensible table, not a new modal surface. +- **Config registry adoption:** none, deliberately — §6.2 gives the + mechanism reason and names the deferred scalar. +- **Background-work attribution:** unchanged. Compile already spawns + through the process-group machinery; this stage changes what is typed + into the prompt, not what is spawned or how it is tracked. +- **Doc updates riding this PR** (§25 requires it): + - `COHERENCE.md` §2 step-9 verdict row → Works, and the + `ProjectKind::Cargo` → `ProjectKind::Rust` correction **in both + places** (§2 row and §20 Priority 1). §24 gains the drift entry. + - `COHERENCE.md` §2's post-table paragraph: "Running a build still has + no binding" → answered, with the quote itself left intact (§6.3). + - `docs/keybindings.md`: the `C-c c` row, and the removal of the + "no global key is assigned to them" sentence at `:191`. + - `docs/agent-handoff.md` §1: the journey arc bullet gains Stage 1b-1. + +## 9. Open questions for review + +- **Q#J1 — is `C-c c` the right chord?** It is free, it is under the + established `C-c` prefix, and it matches what most Emacs distributions + bind compile to. The alternative worth naming is `C-c C-c`, which is + more finger-friendly but is the chord many major modes claim + buffer-locally, so a global one would be shadowed unpredictably later. +- **Q#J2 — should `rust` be the only seeded default?** §3.2 argues yes + on the grounds that a wrong prefill costs more than an empty one. The + counter-argument is that `go build` and `make` are about as + unambiguous as `cargo build`, and seeding them would make the table + read as a real registry rather than a Rust special case. +- **Q#J3 — should the prefill be selected, so typing replaces it?** + Emacs leaves the prefill unselected and the point at the end. dired's + prefill does the same. Matching them means "accept" is RET and + "replace" is a kill-line first. Changing it is a minibuffer-wide + behavior change and out of scope, but it is the ergonomic difference a + user will notice first. + +## 10. Ledger + +Branch `journey-stage1b1-compile-defaults`, worktree +`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`. +Framing only; no code, no PR yet. + +Recovery from a clean checkout — the two-argument form of +`git worktree add` does not work for a remote-only branch (it fails with +`fatal: invalid reference`, because after a bare fetch no local branch +exists): + +```sh +git fetch githubsucks +git worktree add ../pmacs-journey-1b1 \ + -b journey-stage1b1-compile-defaults \ + githubsucks/journey-stage1b1-compile-defaults +```