diff --git a/docs/active-work.md b/docs/active-work.md index fb6090a..871bbc1 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -1,6 +1,6 @@ # Active work — cross-machine resume ledger -**Snapshot: 2026-07-27.** This file records volatile work that has not +**Snapshot: 2026-07-28.** This file records volatile work that has not landed on `main`. Read it after `docs/agent-handoff.md`. Remove completed entries when their PR merges; do not let this become a second permanent backlog. @@ -27,12 +27,11 @@ landed regardless of what a lane says. machine-local: `origin` may name this canonical URL, a release mirror, or something else, and therefore has no authority by name alone. - Canonical base at this snapshot: - `githubsucks/main` @ `c2d56ff` (Journey Stage 1a #182, which - incorporated terminal configuration + copy mode #180, atop Lean 4 - Stage 4b #181 and the previously recorded landed work; protocol v20). - The previous snapshot named `42025e4`, and **the recovery floor - advances with it**: the check below now requires `c2d56ff` or newer, - so a tree at `42025e4` no longer passes. That is + `githubsucks/main` @ `7fd646d` (Journey/GPU directory-target ratchet + #183, atop Journey Stage 1a #182 and the previously recorded landed + work; protocol v20). The previous snapshot named `c2d56ff`, and **the + recovery floor advances with it**: the check below now requires + `7fd646d` or newer, so a tree at `c2d56ff` no longer passes. That is deliberate — the floor moves with the base, because a check that accepts an older commit than the declared base passes on a tree the rest of this file does not describe. @@ -71,7 +70,7 @@ git worktree list git status --short --branch ``` -The `git log` command must expose `c2d56ff` — the base named above — or a +The `git log` command must expose `7fd646d` — the base named above — or a newer intentional main. Keep this threshold and the canonical-base line in step: a recovery check that accepts an older commit than the base it declares canonical will pass on a tree the rest of this file does not @@ -306,18 +305,16 @@ 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. -## Bottom-panel lane (Arc 7) — 2B-1 GATED; PR HELD ON MAIN RATCHET +## Bottom-panel lane (Arc 7) — 2B-1 INTEGRATED; FULL GATES PENDING Stage 1, the Stage 2 framing, and Stage 2A are on `main`. Framing revision 5's three-way split of 2B was explicitly approved on 2026-07-27. **Stage 2B-1 is implemented, integrated with canonical -`main`, and its full matrix has run. No PR is open: one deterministic -touched-suite assertion is stale on canonical `main` after #182 and -must be corrected separately before this branch can claim a green -gate.** +`main` @ `7fd646d`, and has no remaining dependency. Its full matrix is +being rerun on the integrated tree before a PR opens.** - **Stage 2B-1 branch:** `bottom-panel-stage2b`, based on - `githubsucks/main` @ `c2d56ff` by merge because review had begun. + `githubsucks/main` @ `7fd646d` by merge because review had begun. Recovery: `git fetch githubsucks && git checkout bottom-panel-stage2b`. Everything described through the integration checkpoint is committed and pushed; nothing depends on a worktree or @@ -334,7 +331,7 @@ gate.** statusline version ladder still pinned v20/rejected v21, and Vterm Stage 3 pinned v20 both structurally and in its real headless probe. Those ratchets now expect v21 and, where applicable, reject v22. -- **Green evidence on the corrected tree:** formatting and strict +- **Pre-integration green evidence at `b9123c2`:** formatting and strict workspace Clippy; library **1,849 passed + 3 ignored default** and **2,034 passed + 4 ignored CRDT**; bottom-panel Stage 1 / 2A / 2B-1 **46 / 17 / 15**; folding Stage 2 **48**; GPU font **11**; statusline @@ -347,16 +344,12 @@ gate.** `m8_1_acceptance::read_dir_supersede_cancels_in_flight_predecessor`; the exact pin, its full 10-test target, and the complete workspace rerun all passed. -- **Sole deterministic red — reproduced unchanged on canonical - `main`:** `gpu_initial_target_acceptance` is **13/14**, failing - `malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon`. - Its invalid-target table still includes `"."` and demands only a - failure result, while #182 deliberately made a directory target valid - and therefore sends the result plus snapshot. The identical failure - reproduces at the tree-identical #182 head `7a3a55d`; 2B-1 changes no - initial-target behavior. Correct this as a Journey/GPU-initial-target - ratchet side quest on `main`, then integrate it here and rerun that - touched gate before opening the 2B-1 PR. +- **The former deterministic red is resolved on `main`.** PR #183 + corrected `gpu_initial_target_acceptance` through the public + `pmacs --gpu .` path, consumed the asynchronous dired snapshot, and + retained the managed daemon before the wait so failure cleanup remains + effective. The code integration auto-composed; the full matrix still + has to prove the combined tree. - **Next ordering is fixed:** 2B-2 branches from `main` only after 2B-1 lands; 2B-3 branches only after 2B-2 lands. The daemon epoch machine belongs to 2B-2; the GPU band and negotiated capability flip belong @@ -429,10 +422,12 @@ gate.** `docs/agent-handoff.md` §1; the two round lessons are in §5. - Landed-docs follow-up merged as **#156** (`main` @ `d152120`, 2026-07-25). -- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5**, - on branch `githubsucks/bottom-panel-stage2-framing` (three commits, - one per pre-implementation revision), worktree `../pmacs-bp-stage2`, based on - `githubsucks/main` @ `ccf29e3`. Round 1 closed 2 blocking + 3 high; +- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5** + is commit `56301ed` on branch `githubsucks/bottom-panel-stage2b`, + worktree `../pmacs-bp-stage2b`. Revisions 1–4 remain on + `githubsucks/bottom-panel-stage2-framing` (head `4fbd47f`, four + framing commits, revision 4 at `49757e5`). Round 1 closed 2 blocking + + 3 high; round 2 closed 1 blocking + 2 high + 1 medium and decided both open items; round 3 closed 1 blocking + 1 high + 1 medium. No open items remain. Revision 5 adds no decision; it records the approved diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 9ccb325..d2e8ee2 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -1,8 +1,10 @@ # Agent handoff — cross-machine continuity -**Last updated: 2026-07-27, after Journey Stage 1a (#182) — the -directory-open seam and golden-journey ratchet, incorporating terminal -configuration + copy mode (#180) — and after terminal copy mode (#178) — `C-c C-t` +**Last updated: 2026-07-28, after the Journey/GPU directory-target +ratchet (#183), following Journey Stage 1a (#182), which made directory +startup one coherent local/daemon/GPU path and incorporated the terminal +configuration + copy mode landed-doc work (#180); following terminal +copy mode (#178) — `C-c C-t` materializes a terminal's whole retained range into an ordinary buffer, plus `Buffer::set_generated_contents`, the first genuinely immutable generated-buffer write path — and its landed-doc pair (#168); following @@ -41,12 +43,13 @@ reads it the way you just did. For volatile branches, checkpoints, verification, and recovery commands, read `docs/active-work.md` immediately after this file. -## 1. Where the project stands (2026-07-27) +## 1. Where the project stands (2026-07-28) -- `main` @ `c2d56ff` (Journey Stage 1a #182, incorporating terminal - configuration + copy mode #180, atop Lean 4 Stage 4b #181, the dired - Stage 1 landed docs #169 and the PTY-terminate diagnostic #176, - terminal copy mode #178, the GPU-terminal-input landed docs #168, Lean 4 Stage 4a +- `main` @ `7fd646d` (Journey/GPU directory-target ratchet #183, atop + Journey Stage 1a #182, incorporating terminal configuration + copy + mode landed docs #180, Lean 4 Stage 4b #181, the dired Stage 1 landed + docs #169 and the PTY-terminate diagnostic #176, terminal copy mode + #178, the GPU-terminal-input landed docs #168, Lean 4 Stage 4a #179, bottom-panel Stage 2A #177, the bottom-panel Stage 2 framing #175, terminal configuration Stage 1 #173, Lean 4 Stage 3b #170, Stage 3a #167, the CRDT undo repro diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 32a9fba..bd961e1 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -936,10 +936,20 @@ fn run_headless_managed_probe( } }; let mut client = managed.client; + let initial_message = client.take_initial_message(); let initial_target_ready = matches!( - client.take_initial_message(), + initial_message.as_ref(), Some(InstanceMessage::BufferSnapshot { .. }) ); + let mut buffer_facts = ManagedProbeBufferFacts::default(); + if let Some(message) = initial_message.as_ref() + && let Err(error) = buffer_facts.observe(message) + { + let contents = format!("phase=error\nerror={error}\n"); + let _ = write_probe_report(report, &contents); + eprintln!("pmacs-gpu managed probe: {error}"); + return 7; + } let daemon = managed.daemon; let protocol = client.server_protocol_version(); @@ -961,8 +971,14 @@ fn run_headless_managed_probe( let mut last_wait_result = None; let mut last_disconnect = String::new(); if ready - && let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) + && let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", @@ -976,11 +992,23 @@ fn run_headless_managed_probe( } match event_rx.recv_timeout(Duration::from_millis(50)) { Ok(AttachEvent::Message(message)) => { - if matches!(*message, InstanceMessage::BufferSnapshot { .. }) && !ready { + let is_snapshot = matches!(*message, InstanceMessage::BufferSnapshot { .. }); + if let Err(error) = buffer_facts.observe(&message) { + let contents = format!("phase=error\nerror={error}\n"); + let _ = write_probe_report(report, &contents); + eprintln!("pmacs-gpu managed probe: {error}"); + return 7; + } + if is_snapshot { ready = true; - if let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1006,9 +1034,14 @@ fn run_headless_managed_probe( || wait_result != last_wait_result || disconnect != last_disconnect) { - if let Err(error) = - write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "ready", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1021,9 +1054,14 @@ fn run_headless_managed_probe( } if ready && stdin_closed { - if let Err(error) = - write_managed_probe_report(report, "complete", protocol, &daemon, &disconnect) - { + if let Err(error) = write_managed_probe_report( + report, + "complete", + protocol, + &daemon, + &buffer_facts, + &disconnect, + ) { eprintln!( "pmacs-gpu managed probe: writing {} failed: {error}", report.display() @@ -1043,11 +1081,42 @@ fn run_headless_managed_probe( } } +#[derive(Default)] +struct ManagedProbeBufferFacts { + snapshots: u32, + last_snapshot_text: String, +} + +impl ManagedProbeBufferFacts { + fn observe(&mut self, message: &InstanceMessage) -> Result<(), String> { + let InstanceMessage::BufferSnapshot { crdt_snapshot, .. } = message else { + return Ok(()); + }; + let doc = loro::LoroDoc::new(); + doc.import(crdt_snapshot) + .map_err(|error| format!("BufferSnapshot import failed: {error:?}"))?; + self.snapshots += 1; + self.last_snapshot_text = doc.get_text(LORO_TEXT_CONTAINER).to_string(); + Ok(()) + } +} + +fn hex_bytes(bytes: &[u8]) -> String { + use std::fmt::Write as _; + + let mut encoded = String::with_capacity(bytes.len() * 2); + for byte in bytes { + let _ = write!(encoded, "{byte:02x}"); + } + encoded +} + fn write_managed_probe_report( report: &Path, phase: &str, protocol: u32, daemon: &attach::ManagedDaemonFacts, + buffer_facts: &ManagedProbeBufferFacts, disconnect: &str, ) -> std::io::Result<()> { use std::fmt::Write as _; @@ -1056,6 +1125,12 @@ fn write_managed_probe_report( let _ = writeln!(out, "phase={phase}"); let _ = writeln!(out, "server_protocol_version={protocol}"); let _ = writeln!(out, "buffer_snapshot=true"); + let _ = writeln!(out, "buffer_snapshots={}", buffer_facts.snapshots); + let _ = writeln!( + out, + "last_snapshot_hex={}", + hex_bytes(buffer_facts.last_snapshot_text.as_bytes()) + ); let _ = writeln!(out, "spawned_daemon={}", daemon.spawned_daemon()); let _ = writeln!( out, diff --git a/tests/gpu_invocation_acceptance.rs b/tests/gpu_invocation_acceptance.rs index 5371438..b4e3102 100644 --- a/tests/gpu_invocation_acceptance.rs +++ b/tests/gpu_invocation_acceptance.rs @@ -121,6 +121,19 @@ mod crdt { .collect() } + fn decode_hex(encoded: &str) -> String { + assert_eq!(encoded.len() % 2, 0, "hex payload must have even length"); + let bytes = encoded + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let pair = std::str::from_utf8(pair).expect("hex pair is UTF-8"); + u8::from_str_radix(pair, 16).expect("decode hex pair") + }) + .collect::>(); + String::from_utf8(bytes).expect("snapshot text is UTF-8") + } + fn wait_for_fact( report: &Path, key: &str, @@ -356,6 +369,16 @@ mod crdt { } impl ManagedProbe { + fn from_child(mut child: Child, report: &Path) -> Self { + let stdin = child.stdin.take().expect("probe stdin"); + Self { + child, + stdin: Some(stdin), + report: report.to_owned(), + daemon_pid: None, + } + } + fn spawn(socket: &Path, report: &Path, daemon_executable: &Path, home: &Path) -> Self { Self::spawn_with_env(socket, report, daemon_executable, home, &[]) } @@ -418,18 +441,11 @@ mod crdt { for (key, value) in envs { command.env(key, value); } - let mut child = command.spawn().expect("spawn managed probe"); - let stdin = child.stdin.take().expect("probe stdin"); - Self { - child, - stdin: Some(stdin), - report: report.to_owned(), - daemon_pid: None, - } + Self::from_child(command.spawn().expect("spawn managed probe"), report) } - fn wait_ready(&mut self) -> HashMap { - let facts = wait_for_fact(&self.report, "phase", "ready", Duration::from_secs(10)); + fn wait_for(&mut self, key: &str, expected: &str) -> HashMap { + let facts = wait_for_fact(&self.report, key, expected, Duration::from_secs(10)); if facts .get("spawned_daemon") .is_some_and(|value| value == "true") @@ -439,6 +455,10 @@ mod crdt { facts } + fn wait_ready(&mut self) -> HashMap { + self.wait_for("phase", "ready") + } + fn close(mut self) -> std::process::ExitStatus { self.stdin.take(); wait_for_fact(&self.report, "phase", "complete", Duration::from_secs(5)); @@ -662,6 +682,75 @@ mod crdt { assert!(probe.close().success()); } + #[test] + fn public_gpu_directory_target_reaches_dired_and_leaves_the_daemon_usable() { + let temp = secure_tempdir(); + let socket = temp.path().join("directory-target.sock"); + let report = temp.path().join("directory-target-report"); + let wrapper = temp.path().join("headless-gpu"); + let listed_name = "listed-before-bootstrap.txt"; + fs::write(temp.path().join(listed_name), "listed\n").expect("write listed file"); + write_script( + &wrapper, + "test \"$1\" = \"--managed-attach\"\n\ + socket=$2\n\ + daemon=$3\n\ + shift 3\n\ + exec \"$PMACS_REAL_GPU\" --headless-managed-probe \ + \"$socket\" \"$PMACS_TEST_REPORT\" \"$daemon\" \"$@\"", + ); + + let mut command = Command::new(pmacs_binary()); + command + .args(["--gpu", "--socket"]) + .arg(&socket) + .arg(".") + .current_dir(temp.path()) + .env(TEST_GPU_OVERRIDE, &wrapper) + .env("PMACS_REAL_GPU", gpu_binary()) + .env("PMACS_TEST_REPORT", &report) + .env("HOME", temp.path()) + .env("XDG_CONFIG_HOME", temp.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let mut directory = + ManagedProbe::from_child(command.spawn().expect("spawn public GPU command"), &report); + + // The public root broker and real managed GPU connector must stay + // alive through Journey N2's asynchronous dired commit. Snapshot + // one is the deliberately pre-existing bootstrap document; snapshot + // two is the post-quiescence directory surface. + // Capture the spawned daemon's PID from the ready report first so + // ManagedProbe::drop can terminate it if snapshot two never arrives. + let ready = directory.wait_ready(); + assert_eq!( + ready.get("spawned_daemon").map(String::as_str), + Some("true") + ); + let facts = directory.wait_for("buffer_snapshots", "2"); + let listing = decode_hex(&facts["last_snapshot_hex"]); + let canonical = fs::canonicalize(temp.path()).expect("canonical directory"); + let mut lines = listing.lines(); + let expected_header = format!("{}:", canonical.display()); + assert_eq!( + lines.next(), + Some(expected_header.as_str()), + "the replacement snapshot must be dired's directory surface:\n{listing}" + ); + assert!( + lines.any(|line| line.trim_end().ends_with(listed_name)), + "dired must list the file that existed before bootstrap:\n{listing}" + ); + + fs::write(temp.path().join("still-alive.txt"), "alive\n").expect("write survivor"); + let survivor = attach_target(&socket, temp.path(), Path::new("still-alive.txt")); + assert_eq!(survivor.replica.materialize_string(), "alive\n"); + + drop(survivor); + assert!(directory.close().success()); + } + #[test] fn malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon() { let temp = secure_tempdir(); @@ -673,7 +762,6 @@ mod crdt { (cwd.clone(), Vec::new()), (cwd.clone(), b"bad\0name".to_vec()), (cwd.clone(), vec![b'x'; 32 * 1024 + 1]), - (cwd.clone(), b".".to_vec()), ]; for (index, (bad_cwd, bad_path)) in invalid.into_iter().enumerate() { let (frontend_id, mut stream, messages) = open_raw_target(&socket, bad_cwd, bad_path);