Skip to content

fix: bug-hunt round 8 — guided-apply domain consent, truthful capture summary, local-overlay terminal compare, deps error diagnostics, snapshot containment guard, exact release version gate - #37

Merged
REPPL merged 2 commits into
mainfrom
bughunt/round-8
Aug 14, 2026

Conversation

@REPPL

@REPPL REPPL commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

Round 8 of the autonomous bug-hunt loop (state: #29). Fresh hunt across the four dimensions with four hunters; 22 unique candidates, each handed to an independent adversarial refuter before any fix — 12 confirmed (8 substantive, 4 nitpick), 12 refuted, 2 hunter-self-refuted.

Confirmed and fixed — substantive

  1. Guided apply's bulk consent named the wrong domain — cmd/guided_apply.go:269-291. groupRisky switched on a two-name literal list (agents, terminals), so risky keybindings/emacs/iterm2-profiles changes fell into the default dotfiles bucket: the group header and the "Apply all N change(s) in dotfiles?" prompt named a domain the user was not reviewing, and one "yes" covered the mixed bucket (consequential on first apply over pre-existing files, where StateRepoAhead adopt/overwrite items pass the conflict refusal). Now grouped per FileDomain in registry order — the same hand-maintained-list class fileDomainIsRepoAuthoritative (cmd/registry.go:121-134) was introduced to end.
  2. Capture claimed changes it never made — cmd/capture.go:218-241, internal/deps/dump.go, internal/deps/npm.go. reDumpDeps/reDumpNpmGlobals returned true on any successful dump (brew bundle dump --force and the npm list write rewrite unconditionally), so on any machine managing either domain a clean re-run printed "wrote 1 change(s) into the repo. Review with git status…" over an empty git status. Both re-dumps now byte-compare and count only real changes (TestReDump_Brew_ChangeDetection, extended TestReDumpNpmGlobals_WritesSortedList).
  3. A captured-to-local terminal domain was re-offered forever — cmd/capture.go:1060-1078 compared the live iTerm2/Apple Terminal export only against the shared plist, while apply (cmd/apply.go:1680), status, and diff resolve the per-machine local overlay first. After routing a capture to [l]ocal, every later capture re-offered the identical domain as drifted while status reported it clean — and re-accepting [s]hared would promote machine-divergent settings into the shared repo. Capture now compares against the same terminalRepoStatusSource local-wins source (new cmd/terminal_status_source_test.go pins the seam; the darwin-only defaults export has no injectable runner, so the harness cannot drive this end-to-end — stated plainly rather than papered over).
  4. Failed dependency installs were undiagnosable — internal/deps/install.go:129,227, npm.go:181, dump.go:54 discarded the manager's CombinedOutput on error, so apply --deps aborted with exit status 1 and no failing package named, while the uninstall rail (cmd/restore.go:522,544) already surfaced output. All four sites now carry the diagnostics (TestInstallBrew_FailureSurfacesManagerOutput, watched fail pre-fix).
  5. The exported Snapshot bypassed the containment guard — internal/backup/snapshot.go:67-69 vs the guarded sibling restore.go:55-71. ferry work receive's pre-receive snapshot (internal/work/receive.go:190) read its target paths with no resolved-parent guard, so a parent symlinked out of $HOME let the snapshot read and persist out-of-home content into the snapshot store — content the downstream write boundary refuses. Adversarial review downgraded severity (no route to ~/.ssh in practice; plan-time already reads the same bytes; blobs land 0600 in a 0700 store) but confirmed the invariant gap. snapshotCurrent now runs guardResolvedContainment (resource paths skipped) before creating or reading anything, covering both entry points identically (TestSnapshotRefusesParentSymlinkEscapingHome, watched fail pre-fix).
  6. The release version-stamp gate passed on the failure it guards — .github/workflows/release.yml:338-344 and scripts/release.sh:156-159 used grep -qF "$TAG" on ferry version output. The dev line is next-release-plus--dev (cmd/root.go:17, v0.11.0-dev), so an unstamped binary's output contains the modal next tag and the substring match greened on exactly the moved-symbol -ldflags failure the step exists to catch. Both gates now require the output to equal the literal string "ferry " followed by the tag, verbatim (format confirmed against the built binary).

Confirmed and fixed — docs/nitpick

  1. The "one privileged step" sudo claim was false — README.md:99-103 and docs/tutorials/getting-started.md:19-22 named apply --deps as the sole privileged operation, but restore --packages runs apt-get remove as root (cmd/restore.go:542; the code's own comments at internal/deps/runner.go:32-33 name both rails) and no user-facing page said so. Both claims now cover the full apt rail, and the apt reference section documents the uninstall half.
  2. Compatibility contract misstated the claim-file writers — docs/reference/compatibility.md:57 said claim files are "written by ferry work pack", but receive and take-back write them too (internal/work/receive.go:225,163), each account appending only to its own file (claim.go:18-21). The row now names all three writers, so a version-envelope change on receive's write path is in scope.
  3. internal/deps/npm.go:94 was the lone repo-side manifest writer violating the documented backup.AtomicWrite canon (internal/backup/atomic.go:13-16) — now atomic (folded into fix 2).
  4. scripts/consistency-lint.sh:86's advice — git rm --cached — greens the gate while already-committed history still publishes the private tier on push; the advice now mandates amending the unpushed commits that carry it. (A range-based check was considered and rejected: @{push}..HEAD breaks CI's detached checkout and new branches under set -euo pipefail.)
  5. docs/reference/commands.md lacked the agents parent-noun row its bundle/work siblings have.
  6. make preflight (the pre-push gate, added round 7) appeared in no build-and-check list; now in AGENTS.md and CONTRIBUTING.md.

Considered and rejected (refuted)

  • Terminal-capture placeholder plaintext leak: the overwrite mechanics are real, but the only writer of a placeholder is the blocked-capture path, reached only when secret.GateValue already rated that content High — the re-export re-trips the gate, and the iTerm2 allowlist admits no secret-bearing key. Unreachable without a hand-authored placeholder over non-High content.
  • Hand-over sudo mkdir: round-1 adjudication stands — a host-OS setup example, not a ferry operation; README's claim scopes to ferry's own operation, and on MDM-hardened /Users/Shared the sudo form is the one that works.
  • Release how-to's five-step list: an explicitly non-exhaustive summary ("The release workflow then…"), which already omits whole verify jobs documented elsewhere.
  • +-metadata tags becoming latest: SemVer-correct and deliberate per the workflow's own comment (release.yml:410-412); the sanctioned release path refuses such tags first.
  • Attestation docs' missing visibility caveat: the docs are true of everything a reader can download; the gate is a dormant contingency documented in the workflow.
  • Retention-policy sentence scope: self-scoping appositive ("complete journal runs and restore snapshots"), 32 lines inside a journal-focused subsection.
  • [work] missing from the compatibility schema surface: [work] lives in machine config, never ferry.toml (internal/config/machine.go:31-35) — the enumeration is correct.
  • Topic-plan lifecycle: plans/README.md:29 states the shipped in vX.Y.Z convention universally; a version with no plan is documented as fine.
  • behavioral in evals/README.md: mirrors the package's own doc comment (evals/harness.go:1); the root-markdown prose-tier rule does not reach component READMEs.
  • "Works today" release framing: present-tense fail-closed design description, not stale narration (the same page documents the live pipeline).
  • Attribution-rule carve-out: the rule is already scoped to credit; functional harness references satisfy host-agnosticism, and amending owner policy prose is an owner call.
  • Pre-push hook not installed per-clone: the ~/.githooks dispatcher is deliberately external and per-machine; the hazard population equals the hook population, and an in-repo core.hooksPath would suppress the dispatcher's other hooks.
  • Hunter-self-refuted: capture's completion hint (literally true; init --github names ferry sync), and the path-keyed managed carry (no privilege delta — sync independently re-enforces origin scheme, secret gates, and no-force push). Both stay on the carry-forward list.

Gates

make build, gofmt -l . (empty), go vet ./..., go test ./..., go test -race ./internal/..., the full eval suite with FERRY_BIN set, and scripts/consistency-lint.sh are all green on this branch. New tests for fixes 4 and 5 were watched failing against the pre-fix code; fix 2's change-detection tests construct the pre-fix-impossible assertions directly.

REPPL added 2 commits August 14, 2026 08:02
… summary, local-overlay terminal compare, deps error diagnostics, snapshot containment guard, exact release version gate

Six code/infra fixes, each adversarially confirmed before fixing:

- guided apply groups risky changes per FileDomain in registry order; a
  two-name literal switch had bucketed keybindings/emacs/iterm2-profiles
  under "dotfiles", so the bulk consent named the wrong domain and one
  "yes" spanned a mixed bucket
- capture counts a brew/npm re-dump only when the manifest bytes changed
  (brew bundle dump --force rewrites unconditionally, so every clean run
  claimed "wrote 1 change(s)" over an empty git status); the npm list is
  written via backup.AtomicWrite and left untouched when identical
- capture compares a terminal preference domain against the local-wins
  source (terminalRepoStatusSource), as apply/status/diff already do; a
  domain captured to [l]ocal was re-offered as drifted forever
- the deps install/dump rails include the package manager's combined
  output in their errors, matching the uninstall rail, so a failed
  apply --deps names the failing package instead of "exit status 1"
- snapshotCurrent runs the resolved-parent containment guard, covering
  the exported Snapshot entry point work receive uses; a parent symlink
  out of $HOME let the pre-receive snapshot persist out-of-home content
- the release version-stamp gate compares exactly against "ferry <tag>"
  in release.yml and release.sh; the dev line is next-release-plus--dev,
  so the substring grep passed an unstamped binary on the modal release

Docs: the README/tutorial "one privileged step" claim covers the apt
uninstall rail (restore --packages runs apt-get as root); the
compatibility contract names pack, receive, and take-back as claim-file
writers; the commands reference gains the agents parent-noun row;
make preflight enters the build-and-check lists; the consistency lint's
private-tier advice mandates amending committed history.

Assisted-by: Claude:claude-fable-5
…eps error site, empty-manifest change detection, groupRisky tests

Addresses the final dual review of PR #37:

- docs/reference/commands.md's apply row still enumerated the guided
  walkthrough groups as dotfiles/agents/terminals — the exact list the
  groupRisky fix deleted — and now names all six domains (the reviewer's
  one blocking finding); the twin stale comments in cmd/apply.go updated
- internal/deps/status.go's read-only drift dump was a fifth site
  discarding the manager's output on error; now consistent with the
  other rails, and the CHANGELOG wording is accurate
- a brew dump that CREATES an empty manifest now counts as a change:
  nil-vs-empty bytes compare equal, so reDump checks existence too
- groupRisky gains the unit tests it shipped without (per-domain names,
  registry order, no lumping into dotfiles, preference-item fallback)
- the dump.go change-detection comment and capture's "exactly as apply"
  claim corrected; CHANGELOG narrates the npm 0644 normalisation and
  receive's abort-first refusal ordering

Assisted-by: Claude:claude-fable-5
@REPPL
REPPL merged commit 996183b into main Aug 14, 2026
13 checks passed
@REPPL
REPPL deleted the bughunt/round-8 branch August 14, 2026 08:24
@REPPL REPPL mentioned this pull request Aug 14, 2026
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