Merge canonical main into bottom-panel Stage 2B-1

Integrate main through the Journey/GPU directory-target ratchet (#183).
The public managed-GPU probe additions compose with the v21 wire layer.

Resolve the volatile ledgers to the landed state: advance the canonical
anchor to 7fd646d, remove the completed ratchet lane, preserve the
approved Stage 2 revision-5 recovery facts, and mark the integrated
2B-1 tree as awaiting its from-start full gate rerun.
This commit is contained in:
Levi Neuwirth 2026-07-28 11:38:19 -04:00
commit c8895a85d3
4 changed files with 222 additions and 61 deletions

View File

@ -1,6 +1,6 @@
# Active work — cross-machine resume ledger # 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 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 entries when their PR merges; do not let this become a second permanent
backlog. backlog.
@ -27,12 +27,11 @@ landed regardless of what a lane says.
machine-local: `origin` may name this canonical URL, a release mirror, machine-local: `origin` may name this canonical URL, a release mirror,
or something else, and therefore has no authority by name alone. or something else, and therefore has no authority by name alone.
- Canonical base at this snapshot: - Canonical base at this snapshot:
`githubsucks/main` @ `c2d56ff` (Journey Stage 1a #182, which `githubsucks/main` @ `7fd646d` (Journey/GPU directory-target ratchet
incorporated terminal configuration + copy mode #180, atop Lean 4 #183, atop Journey Stage 1a #182 and the previously recorded landed
Stage 4b #181 and the previously recorded landed work; protocol v20). work; protocol v20). The previous snapshot named `c2d56ff`, and **the
The previous snapshot named `42025e4`, and **the recovery floor recovery floor advances with it**: the check below now requires
advances with it**: the check below now requires `c2d56ff` or newer, `7fd646d` or newer, so a tree at `c2d56ff` no longer passes. That is
so a tree at `42025e4` no longer passes. That is
deliberate — the floor moves with the base, because a check that deliberate — the floor moves with the base, because a check that
accepts an older commit than the declared base passes on a tree the accepts an older commit than the declared base passes on a tree the
rest of this file does not describe. rest of this file does not describe.
@ -71,7 +70,7 @@ git worktree list
git status --short --branch 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 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 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 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 never been enforced. Any CI job that compiles the `crdt` targets has to
fix them first or it will be red on arrival. 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 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 revision 5's three-way split of 2B was explicitly approved on
2026-07-27. **Stage 2B-1 is implemented, integrated with canonical 2026-07-27. **Stage 2B-1 is implemented, integrated with canonical
`main`, and its full matrix has run. No PR is open: one deterministic `main` @ `7fd646d`, and has no remaining dependency. Its full matrix is
touched-suite assertion is stale on canonical `main` after #182 and being rerun on the integrated tree before a PR opens.**
must be corrected separately before this branch can claim a green
gate.**
- **Stage 2B-1 branch:** `bottom-panel-stage2b`, based on - **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 Recovery: `git fetch githubsucks && git checkout
bottom-panel-stage2b`. Everything described through the integration bottom-panel-stage2b`. Everything described through the integration
checkpoint is committed and pushed; nothing depends on a worktree or 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 statusline version ladder still pinned v20/rejected v21, and Vterm
Stage 3 pinned v20 both structurally and in its real headless probe. Stage 3 pinned v20 both structurally and in its real headless probe.
Those ratchets now expect v21 and, where applicable, reject v22. 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 workspace Clippy; library **1,849 passed + 3 ignored default** and
**2,034 passed + 4 ignored CRDT**; bottom-panel Stage 1 / 2A / 2B-1 **2,034 passed + 4 ignored CRDT**; bottom-panel Stage 1 / 2A / 2B-1
**46 / 17 / 15**; folding Stage 2 **48**; GPU font **11**; statusline **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`; `m8_1_acceptance::read_dir_supersede_cancels_in_flight_predecessor`;
the exact pin, its full 10-test target, and the complete workspace the exact pin, its full 10-test target, and the complete workspace
rerun all passed. rerun all passed.
- **Sole deterministic red — reproduced unchanged on canonical - **The former deterministic red is resolved on `main`.** PR #183
`main`:** `gpu_initial_target_acceptance` is **13/14**, failing corrected `gpu_initial_target_acceptance` through the public
`malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon`. `pmacs --gpu .` path, consumed the asynchronous dired snapshot, and
Its invalid-target table still includes `"."` and demands only a retained the managed daemon before the wait so failure cleanup remains
failure result, while #182 deliberately made a directory target valid effective. The code integration auto-composed; the full matrix still
and therefore sends the result plus snapshot. The identical failure has to prove the combined tree.
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.
- **Next ordering is fixed:** 2B-2 branches from `main` only after 2B-1 - **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 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 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. `docs/agent-handoff.md` §1; the two round lessons are in §5.
- Landed-docs follow-up merged as **#156** (`main` @ `d152120`, - Landed-docs follow-up merged as **#156** (`main` @ `d152120`,
2026-07-25). 2026-07-25).
- **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5**, - **Stage 2 framing: `docs/bottom-panel-stage2-framing.md` revision 5**
on branch `githubsucks/bottom-panel-stage2-framing` (three commits, is commit `56301ed` on branch `githubsucks/bottom-panel-stage2b`,
one per pre-implementation revision), worktree `../pmacs-bp-stage2`, based on worktree `../pmacs-bp-stage2b`. Revisions 14 remain on
`githubsucks/main` @ `ccf29e3`. Round 1 closed 2 blocking + 3 high; `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 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 items; round 3 closed 1 blocking + 1 high + 1 medium. No open items
remain. Revision 5 adds no decision; it records the approved remain. Revision 5 adds no decision; it records the approved

View File

@ -1,8 +1,10 @@
# Agent handoff — cross-machine continuity # Agent handoff — cross-machine continuity
**Last updated: 2026-07-27, after Journey Stage 1a (#182) — the **Last updated: 2026-07-28, after the Journey/GPU directory-target
directory-open seam and golden-journey ratchet, incorporating terminal ratchet (#183), following Journey Stage 1a (#182), which made directory
configuration + copy mode (#180) — and after terminal copy mode (#178) — `C-c C-t` 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, materializes a terminal's whole retained range into an ordinary buffer,
plus `Buffer::set_generated_contents`, the first genuinely immutable plus `Buffer::set_generated_contents`, the first genuinely immutable
generated-buffer write path — and its landed-doc pair (#168); following 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 For volatile branches, checkpoints, verification, and recovery
commands, read `docs/active-work.md` immediately after this file. 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 - `main` @ `7fd646d` (Journey/GPU directory-target ratchet #183, atop
configuration + copy mode #180, atop Lean 4 Stage 4b #181, the dired Journey Stage 1a #182, incorporating terminal configuration + copy
Stage 1 landed docs #169 and the PTY-terminate diagnostic #176, mode landed docs #180, Lean 4 Stage 4b #181, the dired Stage 1 landed
terminal copy mode #178, the GPU-terminal-input landed docs #168, Lean 4 Stage 4a 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 #179, bottom-panel Stage 2A
#177, the bottom-panel Stage 2 framing #175, terminal configuration #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 Stage 1 #173, Lean 4 Stage 3b #170, Stage 3a #167, the CRDT undo repro

View File

@ -936,10 +936,20 @@ fn run_headless_managed_probe(
} }
}; };
let mut client = managed.client; let mut client = managed.client;
let initial_message = client.take_initial_message();
let initial_target_ready = matches!( let initial_target_ready = matches!(
client.take_initial_message(), initial_message.as_ref(),
Some(InstanceMessage::BufferSnapshot { .. }) 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 daemon = managed.daemon;
let protocol = client.server_protocol_version(); let protocol = client.server_protocol_version();
@ -961,8 +971,14 @@ fn run_headless_managed_probe(
let mut last_wait_result = None; let mut last_wait_result = None;
let mut last_disconnect = String::new(); let mut last_disconnect = String::new();
if ready if ready
&& let Err(error) = && let Err(error) = write_managed_probe_report(
write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) report,
"ready",
protocol,
&daemon,
&buffer_facts,
&disconnect,
)
{ {
eprintln!( eprintln!(
"pmacs-gpu managed probe: writing {} failed: {error}", "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)) { match event_rx.recv_timeout(Duration::from_millis(50)) {
Ok(AttachEvent::Message(message)) => { 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; ready = true;
if let Err(error) = if let Err(error) = write_managed_probe_report(
write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) report,
{ "ready",
protocol,
&daemon,
&buffer_facts,
&disconnect,
) {
eprintln!( eprintln!(
"pmacs-gpu managed probe: writing {} failed: {error}", "pmacs-gpu managed probe: writing {} failed: {error}",
report.display() report.display()
@ -1006,9 +1034,14 @@ fn run_headless_managed_probe(
|| wait_result != last_wait_result || wait_result != last_wait_result
|| disconnect != last_disconnect) || disconnect != last_disconnect)
{ {
if let Err(error) = if let Err(error) = write_managed_probe_report(
write_managed_probe_report(report, "ready", protocol, &daemon, &disconnect) report,
{ "ready",
protocol,
&daemon,
&buffer_facts,
&disconnect,
) {
eprintln!( eprintln!(
"pmacs-gpu managed probe: writing {} failed: {error}", "pmacs-gpu managed probe: writing {} failed: {error}",
report.display() report.display()
@ -1021,9 +1054,14 @@ fn run_headless_managed_probe(
} }
if ready && stdin_closed { if ready && stdin_closed {
if let Err(error) = if let Err(error) = write_managed_probe_report(
write_managed_probe_report(report, "complete", protocol, &daemon, &disconnect) report,
{ "complete",
protocol,
&daemon,
&buffer_facts,
&disconnect,
) {
eprintln!( eprintln!(
"pmacs-gpu managed probe: writing {} failed: {error}", "pmacs-gpu managed probe: writing {} failed: {error}",
report.display() 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( fn write_managed_probe_report(
report: &Path, report: &Path,
phase: &str, phase: &str,
protocol: u32, protocol: u32,
daemon: &attach::ManagedDaemonFacts, daemon: &attach::ManagedDaemonFacts,
buffer_facts: &ManagedProbeBufferFacts,
disconnect: &str, disconnect: &str,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
use std::fmt::Write as _; use std::fmt::Write as _;
@ -1056,6 +1125,12 @@ fn write_managed_probe_report(
let _ = writeln!(out, "phase={phase}"); let _ = writeln!(out, "phase={phase}");
let _ = writeln!(out, "server_protocol_version={protocol}"); let _ = writeln!(out, "server_protocol_version={protocol}");
let _ = writeln!(out, "buffer_snapshot=true"); 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, "spawned_daemon={}", daemon.spawned_daemon());
let _ = writeln!( let _ = writeln!(
out, out,

View File

@ -121,6 +121,19 @@ mod crdt {
.collect() .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::<Vec<_>>();
String::from_utf8(bytes).expect("snapshot text is UTF-8")
}
fn wait_for_fact( fn wait_for_fact(
report: &Path, report: &Path,
key: &str, key: &str,
@ -356,6 +369,16 @@ mod crdt {
} }
impl ManagedProbe { 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 { fn spawn(socket: &Path, report: &Path, daemon_executable: &Path, home: &Path) -> Self {
Self::spawn_with_env(socket, report, daemon_executable, home, &[]) Self::spawn_with_env(socket, report, daemon_executable, home, &[])
} }
@ -418,18 +441,11 @@ mod crdt {
for (key, value) in envs { for (key, value) in envs {
command.env(key, value); command.env(key, value);
} }
let mut child = command.spawn().expect("spawn managed probe"); Self::from_child(command.spawn().expect("spawn managed probe"), report)
let stdin = child.stdin.take().expect("probe stdin");
Self {
child,
stdin: Some(stdin),
report: report.to_owned(),
daemon_pid: None,
}
} }
fn wait_ready(&mut self) -> HashMap<String, String> { fn wait_for(&mut self, key: &str, expected: &str) -> HashMap<String, String> {
let facts = wait_for_fact(&self.report, "phase", "ready", Duration::from_secs(10)); let facts = wait_for_fact(&self.report, key, expected, Duration::from_secs(10));
if facts if facts
.get("spawned_daemon") .get("spawned_daemon")
.is_some_and(|value| value == "true") .is_some_and(|value| value == "true")
@ -439,6 +455,10 @@ mod crdt {
facts facts
} }
fn wait_ready(&mut self) -> HashMap<String, String> {
self.wait_for("phase", "ready")
}
fn close(mut self) -> std::process::ExitStatus { fn close(mut self) -> std::process::ExitStatus {
self.stdin.take(); self.stdin.take();
wait_for_fact(&self.report, "phase", "complete", Duration::from_secs(5)); wait_for_fact(&self.report, "phase", "complete", Duration::from_secs(5));
@ -662,6 +682,75 @@ mod crdt {
assert!(probe.close().success()); 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] #[test]
fn malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon() { fn malformed_or_unloadable_targets_fail_closed_without_poisoning_the_daemon() {
let temp = secure_tempdir(); let temp = secure_tempdir();
@ -673,7 +762,6 @@ mod crdt {
(cwd.clone(), Vec::new()), (cwd.clone(), Vec::new()),
(cwd.clone(), b"bad\0name".to_vec()), (cwd.clone(), b"bad\0name".to_vec()),
(cwd.clone(), vec![b'x'; 32 * 1024 + 1]), (cwd.clone(), vec![b'x'; 32 * 1024 + 1]),
(cwd.clone(), b".".to_vec()),
]; ];
for (index, (bad_cwd, bad_path)) in invalid.into_iter().enumerate() { 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); let (frontend_id, mut stream, messages) = open_raw_target(&socket, bad_cwd, bad_path);