docs+test: gate fixes and handoff snapshot (compile-mode in flight)

cargo fmt over the new files; doc-markdown backticks; is_ok_and in
the recompile counter wait; m4_6's M-g n/p pin updated to the Q#CM5
takeover contract (error.next/error.previous with the diag commands
as the dispatchers' fallback — the test's no-attachment status
behavior is unchanged). Handoff §1: main @ 0efb5cd, compile-mode
branch in flight at framing revision 6, themes named as the
standing runner-up.

Gate results on this machine (laptop, basedpyright live): fmt,
clippy --workspace --all-targets, lib 1522, crdt lib 1696,
compile_mode_acceptance 34, compile_mode_crdt_acceptance 1,
m4_acceptance 101 (no skip), PMACS_REQUIRE_GPU gpu 59, workspace
sweep 2482/0, git diff --check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
Levi Neuwirth 2026-07-13 15:23:20 +01:00
parent 53854ed803
commit a2b12dc9d6
6 changed files with 168 additions and 61 deletions

View File

@ -1,7 +1,7 @@
# Agent handoff — cross-machine continuity
**Last updated: 2026-07-12, on the laptop, by the editops session
(post-#111 merge; scripts/bite micro-PR).** This file is the
**Last updated: 2026-07-13, on the laptop, by the compile-mode
session (on-branch snapshot).** This file is the
bridge between development machines. If you are an agent reading
this on a fresh clone: this document plus the `docs/*-framing.md`
files ARE your memory. Read this fully before taking on work, seed
@ -9,10 +9,26 @@ your persistent memory from it, and **update this file (and commit
it) whenever project state changes materially** — the next machine
reads it the way you just did.
## 1. Where the project stands (2026-07-12)
## 1. Where the project stands (2026-07-13)
- `main` @ `f0a05c5` (editops #111 merged), protocol **v15**
- `main` @ `0efb5cd` (scripts/bite #112 merged), protocol **v15**
(`SUPPORTED=[6..15]`).
- **IN FLIGHT: compile-mode (Arc 5 stage 1) on branch
`compile-mode`** — the user chose it over themes (Arc 4) in the
decision discussion. Framing `docs/compile-mode-framing.md` at
revision 6 (five pre-branch review rounds; approved for build).
Shape: `compile.run` streams `/bin/sh -c "exec 2>&1; <cmd>"`
(pipes, `stdin="null"`, `group=true`, TERM=dumb) into an
intercept-read-only `*compilation*` buffer via a Lua-side ANSI
parser; once-per-newline error rules; unified `error.next`/
`error.previous` dispatcher (M-g n/p taken over from diag with a
behavior-preserving fallback; `` C-x ` ``; M-! shell-command);
buffer-revision external-edit guard with desync marker + anchor
epochs; grep-mode upgrade of `project.search`. New substrate other
code can use: `ProcessSpec.stdin/group` (group lifecycle: reap
ledger, in-drain enforcement, cancellable poll readers),
`buf:revision()`, jump_back now fires `buffer.after-switch`,
`pmacs.errors.claim`. All gates green; PR next.
- **Editing-conveniences pack (editops, #111) landed** — the Lua
parallel lane. Framing `docs/editing-conveniences-framing.md` at
revision 6 (three pre-branch rounds, one adopted post-approval
@ -41,9 +57,11 @@ reads it the way you just did.
`buf:path()`, `pmacs.lsp.buffer_language(buf)`,
`PMACS_FAKE_LSP_CHANGE_SINK` (fake-LSP doc-sync replay),
`TestDaemon::spawn_with_config` (init.lua-carrying daemon fixture).
- **NEXT: the user wants a decision discussion — compile-mode (Arc 5
stage 1) vs themes (Arc 4). Do not pick unilaterally; frame the
tradeoff and ask.**
- **NEXT after compile-mode merges: themes (Arc 4) is the standing
runner-up from the decision discussion** — scout fresh before
framing (protocol bump v15→16 for a ThemeFacts channel, the
LineNumbers/Q#UX1 control-plane template, glyphon font reload is
the hard part).
- Auto-indent (#109) landed earlier: RET binds
`edit.newline-and-indent`; plain Enter round-trips on both
frontends; shared search invalidation (Q#AI8), empty-selection

View File

@ -12662,7 +12662,10 @@ mod tests {
)
.eval()
.unwrap();
assert!(ok, "revision must be strictly monotonic across edit/undo/redo");
assert!(
ok,
"revision must be strictly monotonic across edit/undo/redo"
);
}
#[test]

View File

@ -1447,7 +1447,12 @@ fn build_pipes_runtime(spec: &ProcessSpec, _id: ProcessId) -> Result<RuntimeHand
Arc::clone(&active_readers),
)
} else {
spawn_reader(byte_tx.clone(), Arc::clone(&cancel), out, ReaderKind::Stdout)
spawn_reader(
byte_tx.clone(),
Arc::clone(&cancel),
out,
ReaderKind::Stdout,
)
});
}
if let Some(err) = stderr {
@ -1871,10 +1876,7 @@ struct GroupDrainCtx {
deadline: Instant,
}
fn final_drain_runtime(
rt: &RuntimeHandles,
group: Option<GroupDrainCtx>,
) -> Vec<ProcessEventKind> {
fn final_drain_runtime(rt: &RuntimeHandles, group: Option<GroupDrainCtx>) -> Vec<ProcessEventKind> {
let deadline = Instant::now() + EXIT_OUTPUT_DRAIN_TIMEOUT;
let mut out = Vec::new();
// Group drains get tighter bounds than the plain byte-flush
@ -2813,7 +2815,10 @@ mod tests {
// Readiness echo: terminating before the trap is installed
// would let plain SIGTERM win and vacuously pass.
let id = sup
.spawn(sh_group_spec("trap-test", "trap '' TERM; echo ready; sleep 30"))
.spawn(sh_group_spec(
"trap-test",
"trap '' TERM; echo ready; sleep 30",
))
.expect("spawn");
let _ = drain_until(&mut sup, id, Duration::from_secs(2), |evs| {
stdout_contains(evs, b"ready")
@ -2849,7 +2854,9 @@ mod tests {
"( trap '' TERM; exec >/dev/null 2>&1; sleep 30 ) & echo $! > {}",
pidfile.display()
);
let id = sup.spawn(sh_group_spec("survivor", &script)).expect("spawn");
let id = sup
.spawn(sh_group_spec("survivor", &script))
.expect("spawn");
let events = drain_until(&mut sup, id, Duration::from_secs(5), has_exited);
assert!(
events
@ -2882,7 +2889,10 @@ mod tests {
let mut sup = ProcessSupervisor::new();
sup.set_group_term_grace(Duration::from_millis(500));
let id = sup
.spawn(sh_group_spec("re-term", "trap '' TERM; echo ready; sleep 30"))
.spawn(sh_group_spec(
"re-term",
"trap '' TERM; echo ready; sleep 30",
))
.expect("spawn");
let _ = drain_until(&mut sup, id, Duration::from_secs(2), |evs| {
stdout_contains(evs, b"ready")
@ -2923,7 +2933,9 @@ mod tests {
"( trap '' TERM; exec >/dev/null 2>&1; sleep 30 ) & echo $! > {}",
pidfile.display()
);
let id = sup.spawn(sh_group_spec("survivor", &script)).expect("spawn");
let id = sup
.spawn(sh_group_spec("survivor", &script))
.expect("spawn");
let _ = drain_until(&mut sup, id, Duration::from_secs(5), has_exited);
let survivor = wait_pidfile(&pidfile);
assert!(pid_alive(survivor), "survivor alive pre-shutdown");
@ -3075,13 +3087,23 @@ mod tests {
let mut spec = ProcessSpec::new("pty-null", "/bin/sh");
spec.mode = ProcessMode::default_pty();
spec.stdin = StdinMode::Null;
let err = sup.spawn(spec).expect_err("stdin=null must be rejected under pty");
assert!(err.contains("pipe mode"), "error points at pipe mode: {err}");
let err = sup
.spawn(spec)
.expect_err("stdin=null must be rejected under pty");
assert!(
err.contains("pipe mode"),
"error points at pipe mode: {err}"
);
let mut spec = ProcessSpec::new("pty-group", "/bin/sh");
spec.mode = ProcessMode::default_pty();
spec.group = true;
let err = sup.spawn(spec).expect_err("group=true must be rejected under pty");
assert!(err.contains("pipe mode"), "error points at pipe mode: {err}");
let err = sup
.spawn(spec)
.expect_err("group=true must be rejected under pty");
assert!(
err.contains("pipe mode"),
"error points at pipe mode: {err}"
);
}
}

View File

@ -1,7 +1,7 @@
//! Compile-mode acceptance (Arc 5 stage 1,
//! docs/compile-mode-framing.md, items 133; item 34 lives as unit
//! tests in src/process.rs, item 35 in
//! tests/compile_mode_crdt_acceptance.rs).
//! `tests/compile_mode_crdt_acceptance.rs`).
//!
//! Dispatch-driven: every keybinding claim is exercised through
//! `dispatch_key` (never `pmacs.command.invoke`), per the standing
@ -188,7 +188,7 @@ fn pid_alive(pid: i32) -> bool {
}
/// Errors getter marshalled to a comparable Rust shape (encoded as
/// one line per entry — mlua tuples don't implement FromLua).
/// one line per entry — mlua tuples don't implement `FromLua`).
fn compile_errors(s: &EditorState) -> Vec<(String, i64, i64, Option<String>)> {
let encoded: String = eval(
s,
@ -218,7 +218,7 @@ fn compile_errors(s: &EditorState) -> Vec<(String, i64, i64, Option<String>)> {
}
/// Rendered cells of the active window (copied from the
/// m4_acceptance grid helper — cross-crate test code can't import).
/// `m4_acceptance` grid helper — cross-crate test code can't import).
fn render_active_window_to_grid(
state: &mut EditorState,
rows: u32,
@ -617,7 +617,11 @@ fn acc14_malformed_rule_containers_fail_closed() {
1,
"built-in defaults still parse under a non-table container"
);
assert!(errors_buffer(&s).is_empty(), "no error spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no error spam: {}",
errors_buffer(&s)
);
// (b) an invalid-pattern entry is skipped; a later valid entry
// still matches; one status note counts the skip.
@ -644,7 +648,11 @@ fn acc14_malformed_rule_containers_fail_closed() {
vec![("b.c".to_owned(), 3, 0, Some("error".to_owned()))],
"the valid entry still matches after the malformed one"
);
assert!(errors_buffer(&s).is_empty(), "no error spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no error spam: {}",
errors_buffer(&s)
);
}
// ---------------------------------------------------------------------------
@ -738,7 +746,10 @@ fn acc17_chords_walk_compile_errors_across_files() {
// C-x ` is the classic chord for the same dispatcher.
ctrl(&mut s, 'x');
press(&mut s, KeyCode::Char('`'));
assert!(active_buffer_name(&s).ends_with("two.c"), "C-x ` = error.next");
assert!(
active_buffer_name(&s).ends_with("two.c"),
"C-x ` = error.next"
);
}
#[test]
@ -773,25 +784,24 @@ fn acc19_g_recompiles_and_clears_overlay_spans() {
),
dir.path(),
);
assert!(any_styled_cell(&render_active_window_to_grid(&mut s, 8, 60)));
assert!(any_styled_cell(&render_active_window_to_grid(
&mut s, 8, 60
)));
assert_eq!(
std::fs::read_to_string(&counter).unwrap().lines().count(),
1
);
// g re-runs the stored command. The rerun's output has no
// diagnostics, so any styling would be a stale span.
// g re-runs the stored command.
press(&mut s, KeyCode::Char('g'));
assert!(
pump_until(&mut s, 10_000, |_| {
std::fs::read_to_string(&counter)
.map(|c| c.lines().count() == 2)
.unwrap_or(false)
std::fs::read_to_string(&counter).is_ok_and(|c| c.lines().count() == 2)
}),
"recompile must actually re-execute the command"
);
// Wait, then override the fixture: the rerun emits the same
// diagnostic (stored command), so instead assert spans were
// cleared by checking the fresh run reached its own marker with
// The rerun executes the SAME stored command, so its output
// carries the same diagnostic; per-run reset is pinned by
// checking the fresh run reached its own marker with
// exactly one diagnostic parsed (not accumulated).
assert!(pump_until(&mut s, 10_000, |s| compilation_text(s)
.contains("[compile exited")));
@ -924,8 +934,11 @@ fn acc24_command_path_undo_after_completed_run_recovers_immediately() {
dir.path().display().to_string()
),
);
assert!(pump_until(&mut s, 10_000, |s| named_text(s, "*shell-command*")
.contains("[shell exited with code 0]")));
assert!(pump_until(&mut s, 10_000, |s| named_text(
s,
"*shell-command*"
)
.contains("[shell exited with code 0]")));
// M-x buffer.undo: the command path rebinding cannot reach. No
// pump event will ever arrive — recovery must come from the
// buffer.after-edit subscription, synchronously.
@ -938,7 +951,11 @@ fn acc24_command_path_undo_after_completed_run_recovers_immediately() {
"desync marker must appear immediately (bite: fails when \
recovery only runs at pump/anchor time); buffer:\n{text}"
);
assert!(errors_buffer(&s).is_empty(), "clean *errors*: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"clean *errors*: {}",
errors_buffer(&s)
);
}
#[test]
@ -986,7 +1003,11 @@ fn acc25a_no_hook_shrink_mid_stream_recovers_and_reanchors() {
text.find(DESYNC).unwrap() < text.find("two").unwrap(),
"streaming continued after the marker:\n{text}"
);
assert!(errors_buffer(&s).is_empty(), "no spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no spam: {}",
errors_buffer(&s)
);
// Pre-marker anchor dropped: RET on the old row reports.
exec(&s, "pmacs.editor.goto_byte(0)");
let target = first_row;
@ -1101,8 +1122,7 @@ fn acc27_killed_buffer_terminates_run_and_recreates() {
let dir = tempfile::tempdir().unwrap();
let mut s = editor();
compile_run(&s, "echo alive; sleep 30", dir.path());
assert!(pump_until(&mut s, 5_000, |s| compilation_text(s)
.contains("\nalive\n")));
assert!(pump_until(&mut s, 5_000, |s| compilation_text(s).contains("\nalive\n")));
exec(
&s,
r#"
@ -1117,7 +1137,11 @@ fn acc27_killed_buffer_terminates_run_and_recreates() {
pump_until(&mut s, 5_000, |s| process_count(s) == 0),
"run terminated and forgotten after buffer death"
);
assert!(errors_buffer(&s).is_empty(), "no spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no spam: {}",
errors_buffer(&s)
);
// The next run recreates the buffer and completes.
compile_and_finish(&mut s, "echo reborn", dir.path());
assert!(compilation_text(&s).contains("reborn"));
@ -1159,7 +1183,10 @@ fn acc28_grep_panel_is_a_locations_buffer() {
assert!(pump_until(&mut s, 10_000, search_done), "search completes");
assert_eq!(active_buffer_name(&s), "*search-results*");
let text = named_text(&s, "*search-results*");
assert!(text.contains("f.txt:2:0:"), "structured match line:\n{text}");
assert!(
text.contains("f.txt:2:0:"),
"structured match line:\n{text}"
);
// Read-only under dispatch.
type_str(&mut s, "x");
assert_eq!(named_text(&s, "*search-results*"), text, "read-only");
@ -1236,14 +1263,21 @@ fn acc29_grep_kill_mid_search_is_safe_and_masking_is_prevented() {
appending (not mask it); tail:\n{}",
&text[text.len().saturating_sub(400)..]
);
assert!(errors_buffer(&s).is_empty(), "no spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no spam: {}",
errors_buffer(&s)
);
// (b) killing the panel mid-search: no stale-handle writes, and
// the next search recreates the buffer.
let mut s = editor();
search(&s, "zqxvbn_needle_77", dir.path());
assert!(pump_until(&mut s, 10_000, |s| named_text(s, "*search-results*")
.contains(":1:0:")));
assert!(pump_until(&mut s, 10_000, |s| named_text(
s,
"*search-results*"
)
.contains(":1:0:")));
exec(
&s,
r#"
@ -1256,7 +1290,11 @@ fn acc29_grep_kill_mid_search_is_safe_and_masking_is_prevented() {
);
// Drain whatever the worker still delivers.
let _ = pump_until(&mut s, 1_000, |_| false);
assert!(errors_buffer(&s).is_empty(), "no spam: {}", errors_buffer(&s));
assert!(
errors_buffer(&s).is_empty(),
"no spam: {}",
errors_buffer(&s)
);
search(&s, "zqxvbn_needle_77", dir.path());
assert!(
pump_until(&mut s, 15_000, |s| named_text(s, "*search-results*")
@ -1280,8 +1318,11 @@ fn acc30_grep_root_retained_across_interactive_supersede() {
),
);
exec(&s, "pmacs.project.search('zqxvbn_needle_77')");
assert!(pump_until(&mut s, 10_000, |s| named_text(s, "*search-results*")
.contains("f.txt:2:0:")));
assert!(pump_until(&mut s, 10_000, |s| named_text(
s,
"*search-results*"
)
.contains("f.txt:2:0:")));
// Second search issued from inside the pathless panel, no
// opts.root: the panel's stored root must be reused (the "."
// fallback would search the test process's cwd and find
@ -1349,7 +1390,10 @@ fn acc32_q_restores_previous_buffer_from_all_three() {
// *compilation*
compile_and_finish(&mut s, "echo x", dir.path());
press(&mut s, KeyCode::Char('q'));
assert!(active_buffer_name(&s).ends_with("home.txt"), "q from compile");
assert!(
active_buffer_name(&s).ends_with("home.txt"),
"q from compile"
);
// *shell-command*
exec(
&s,
@ -1358,15 +1402,21 @@ fn acc32_q_restores_previous_buffer_from_all_three() {
dir.path().display().to_string()
),
);
assert!(pump_until(&mut s, 10_000, |s| named_text(s, "*shell-command*")
.contains("[shell exited")));
assert!(pump_until(&mut s, 10_000, |s| named_text(
s,
"*shell-command*"
)
.contains("[shell exited")));
press(&mut s, KeyCode::Char('q'));
assert!(active_buffer_name(&s).ends_with("home.txt"), "q from shell");
// *search-results*
search(&s, "zqxvbn_needle_77", dir.path());
assert!(pump_until(&mut s, 10_000, search_done));
press(&mut s, KeyCode::Char('q'));
assert!(active_buffer_name(&s).ends_with("home.txt"), "q from search");
assert!(
active_buffer_name(&s).ends_with("home.txt"),
"q from search"
);
}
#[test]
@ -1387,7 +1437,13 @@ fn acc33_round_trip_input_is_set_on_generated_buffers() {
dir.path().display().to_string()
),
);
assert!(s.core.borrow().active_buffer_round_trips(), "*shell-command*");
assert!(
s.core.borrow().active_buffer_round_trips(),
"*shell-command*"
);
search(&s, "zqxvbn_needle_77", dir.path());
assert!(s.core.borrow().active_buffer_round_trips(), "*search-results*");
assert!(
s.core.borrow().active_buffer_round_trips(),
"*search-results*"
);
}

View File

@ -220,7 +220,10 @@ fn compile_run_converges_and_replica_edit_triggers_recovery() {
let src_text = pump_until_text(&mut source, Duration::from_secs(15), "source run", done);
let obs_text = pump_until_text(&mut observer, Duration::from_secs(15), "observer run", done);
assert_eq!(src_text, obs_text, "byte-identical convergence");
assert!(src_text.contains("x.c:1:1: error: boom"), "output replicated");
assert!(
src_text.contains("x.c:1:1: error: boom"),
"output replicated"
);
assert!(src_text.starts_with("$ sh "), "header replicated");
// Synthetic accepted replica edit to the generated buffer: the

View File

@ -1892,13 +1892,18 @@ fn m4_6_diag_navigate_commands_and_bindings_are_registered() {
)
.eval()
.expect("keymap.list");
// Compile-mode (Q#CM5, docs/compile-mode-framing.md) took the
// M-g chords over for the unified dispatchers; the diag commands
// stay registered and remain the dispatchers' fallback when no
// compile/grep run has claimed the error source, so the
// no-LSP-attachment behavior asserted below is unchanged.
assert!(
bindings.iter().any(|b| b == "M-g n=>diag.next"),
"M-g n must bind to diag.next; got: {bindings:?}"
bindings.iter().any(|b| b == "M-g n=>error.next"),
"M-g n must bind to error.next; got: {bindings:?}"
);
assert!(
bindings.iter().any(|b| b == "M-g p=>diag.previous"),
"M-g p must bind to diag.previous; got: {bindings:?}"
bindings.iter().any(|b| b == "M-g p=>error.previous"),
"M-g p must bind to error.previous; got: {bindings:?}"
);
// Without an LSP attachment, the command should surface a status