Skip to content

fix: close 27 audited gaps against Go upstream v3.1.0 - #2

Merged
quangdang46 merged 2 commits into
mainfrom
fix/gap-remediation
Oct 1, 2026
Merged

quangdang46 merged 2 commits into
mainfrom
fix/gap-remediation

Conversation

@quangdang46

Copy link
Copy Markdown
Owner

Closes the 29 verified gaps catalogued in MISSING.md, audited against a clone of kunchenguid/treehouse at v3.1.0 (cb7fb26).

The port was faithful in the pool core — pool layout, short_hash, next_name, owner_alive, ClassSet, the two-phase destroy reservation and the lock all match. But it failed open exactly where it is most destructive.

The headline

confirm() returned true on stdin EOF (main.rs:725, where the ? on read_line was dead code because EOF yields Ok(0)). So:

treehouse return <path> < /dev/null

confirmed the destructive reset, ran checkout --detach --force + reset --hard + clean -fd on a dirty worktree, erased uncommitted work, and exited 0. Every non-TTY caller — CI, Makefile, hook, agent script, cron — hit it. Go refuses (internal/ui/prompt.go:46-50, cmd/return_cmd.go:399-418).

Verified fixed end to end:

before after
return <p> < /dev/null, dirty tree wiped, exit 0 Aborted., exit 0, data intact
same, TREEHOUSE_EXIT_STRICT=1 — exit 3, data intact

Safety invariants

  • M-002 — marker precondition on every destructive git call. In-project pools are supported, so reset --hard previously walked up and hit the repository enclosing the pool.
  • M-003 / M-004 — ProcessTable was enumerated once at Pool::open and never refreshed; refresh() had zero call sites. Now re-read per scan, and the post-kill survivor re-scan fails closed instead of unwrap_or_default().
  • M-012 — clone-identity scoping. Two clones of the same origin share a pool by default; clone A's get would reset clone B's live slot.
  • M-013 — unlanded-work guard before reset --hard.
  • M-009 — prune re-classifies under the lock on fresh state, and uses the non-forced worktree remove (the forced variant was losing git's own dirty-tree guard).
  • M-010 — parent-directory fsync after rename. Without it a crash reverts to a valid older state file, so corruption recovery never fires.

A second cause the audit missed

Fixing M-004 needed more than the stale snapshot. protected_chain treated parent_pid() == None as fatal and failed closed — but sysinfo returns None for any process with ppid == 0, including pid 1, which every ancestry walk ends at. So terminate_with_grace always returned Err on unix and destroy --include-in-use was permanently a no-op. Go handles this at terminate.go:88-90.

Two pre-existing tests used if let Ok(...) and so passed whether the walk returned Ok or Err; they are replaced with real assertions rather than deleted.

Durability and CLI

  • M-027 — treehouse update now really downloads, verifies sha256 against the sidecar and atomically replaces. It previously printed Successfully updated while downloading nothing, and returned Ok(0) on network failure. Adding flate2/tar/zip, all with default features off.
  • M-007 — a treehouse.toml without max_trees was a hard parse error that broke every command in that repo. (A test had worked around it with a comment; the loader is now fixed.)
  • M-008 / M-023 — --root and TREEHOUSE_ROOT, precedence flag > env > config > default.
  • M-016 / M-017 / M-026 — return <name>, return --all, lease — shipped together with the release preconditions they need. Adding the commands without the guards would let one caller release a slot another owns.
  • M-024 — -b (branch creation, distinct from the existing --base which is the reset target), --unique-leaf, --worktree-path.
  • M-018 — --format json|toon is now honored on destroy/prune/return/gc.

⚠️ Behaviour change to be aware of

Exit code 3 for an unreturned dirty worktree ships behind TREEHOUSE_EXIT_STRICT=1, default off. Go made this a v3.0.0 BREAKING CHANGE; our own docs/rust-port-plan.md Appendix B records exit 0 as the contract, so flipping the default would break existing set -e scripts. The opt-in is deliberate — flip it with a release note when you want Go parity.

--env-path is now a visible alias of --root and its meaning changed: --env-path /tmp/pools resolves to /tmp/pools/.treehouse (Go --root semantics) rather than the old no-op ~/.treehouse. README updated.

Deliberately still open

  • M-028 — the VCS seam. Upstream split internal/vcs/ + gitvcs/ + jjvcs/ + fileclone/; this port is still a flat git-only GitBackend. That is a multi-week rewrite (jj backend, APFS clonefile sharing, .worktreeinclude seeding). M-002 and M-021 are implemented directly on the flat backend so Tier 0/1 were not blocked.
  • --apfs-sharing (#153) and --include-file (#131) — depend on the seam above.

Roughly one rewrite, not six separate gaps: six of the sixteen drift items share that one blocker.

Tests

136 → 317, all passing. Build clean, clippy --all-targets warning-free. No test weakened, ignored, or re-baselined; the two #[cfg_attr(not(windows), allow(dead_code))] additions are on Windows-only updater functions so the linux/macOS build still type-checks that path.

Every fix is mutation-verified: the wiring is reverted in a scratch check and a test is confirmed to fail — removing the apply_into call, disabling return --all JSON, removing the HeldOnlyByCwd exclusion, restoring the gc --all early return, restoring the no-op nonzero-shell-exit branch, and routing lease mode back to the flag-dropping call.

Review notes

MISSING.md is attached — every finding carries the symptom, file:line citations on both sides, a fix sketch, and the verifier's reasoning. Two things a reviewer should know:

  • One claim was refuted and is documented as such: a missing top-level state version does not make Go quarantine everything (there is a carve-out at state.go:143). The residual is a wire-format nit, not CRITICAL.
  • The three P0_PARITY vs UPSTREAM_DRIFT bucket splits that carry decision weight were each re-checked against the v2.1.1 release commit 939cb59.

Audit ran as a 67-agent workflow with two adversarial lenses per finding; implementation as two further rounds (17 agents) with an adversarial review pass that caught an unwired updater round 1 had left looking complete.

🤖 Generated with Claude Code

E2E and others added 2 commits October 1, 2026 11:07
Audit run as a 67-agent workflow: 6 domain finders compared the Rust
port against a clone of kunchenguid/treehouse at v3.1.0, then every
finding passed two adversarial lenses (existence, impact).

29 findings confirmed (4 CRITICAL / 11 HIGH / 14 MEDIUM), 1 refuted,
54 lower-severity leads recorded unverified.

Headline: the port is faithful in the pool core but fails open exactly
where it is most destructive. confirm() returns true on stdin EOF, so
`treehouse return <p> < /dev/null` hard-resets and erases uncommitted
work while exiting 0 — Go refuses. The port also lags six upstream
releases, and 12 of the 16 drift items are in v3.0.0 alone, 6 of them
gated on the same missing VCS seam (one rewrite, not six gaps).

Doc-only; no code changes.
MISSING.md documented 29 verified gaps between this port and the Go
baseline (kunchenguid/treehouse v3.1.0). This closes them.

The port was faithful in the pool core but failed open exactly where it
is most destructive. The headline: confirm() returned true on stdin EOF,
so `treehouse return <p> < /dev/null` hard-reset a dirty worktree, erased
uncommitted work and exited 0. Go refuses. Verified fixed end to end --
both the uncommitted edit and the untracked file now survive.

Safety invariants
- M-002 marker precondition on every destructive git call, so an in-project
  pool can no longer walk up and reset the repository enclosing it
- M-003/M-004 process table is re-read per scan instead of snapshotted at
  Pool::open; post-kill re-scan now fails closed on a scan error
- M-012 clone-identity scoping (two clones of one origin no longer share a
  slot), M-013 unlanded-work guard before reset --hard
- M-009 prune re-classifies under the lock on fresh state and uses the
  non-forced remove; M-010 parent-dir fsync after rename

Fixing M-004 also required a second cause the audit missed: protected_chain
treated parent_pid() == None as fatal, and sysinfo returns None for any
ppid == 0 -- including pid 1, which every ancestry walk ends at. So
terminate_with_grace always failed on unix and destroy --include-in-use was
permanently a no-op. Go handles this at terminate.go:88-90.

Durability and CLI
- M-027 `treehouse update` now really downloads, verifies sha256 and
  atomically replaces. It previously printed success without doing anything;
  round 1 wrote the pipeline but left it uncalled, which the review caught.
- M-007 treehouse.toml without max_trees no longer breaks every command
- M-008/M-023 --root and TREEHOUSE_ROOT with flag > env > config > default
- M-016/M-017/M-026 return <name>, return --all, lease -- all shipped
  together with the release preconditions they require
- M-024 -b (branch creation, distinct from --base), --unique-leaf,
  --worktree-path now reach the core
- M-018 --format json|toon honored on destroy/prune/return/gc

Exit code 3 for an unreturned dirty worktree ships BEHIND
TREEHOUSE_EXIT_STRICT=1, default off. Go shipped it as a v3.0.0 BREAKING
CHANGE and our own plan Appendix B records exit 0, so flipping the default
would break existing scripts. Opt in deliberately, with a release note.

Still open, deliberately: the VCS seam (M-028) and --apfs-sharing /
--include-file, which depend on it. That is a multi-week rewrite (jj
backend, APFS fileclone, .worktreeinclude seeding). M-002 and M-021 are
implemented directly on the flat GitBackend so they were not blocked.

Tests: 136 -> 317, all passing. Build and clippy warning-free. No test
weakened, ignored or re-baselined. Every fix is mutation-verified -- the
wiring is reverted in a scratch check and a test is confirmed to fail.
MISSING.md carries the per-finding citations on both sides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@quangdang46
quangdang46 merged commit 128b9ce into main Oct 1, 2026
5 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant