Skip to content

feat(sync): gate a peer's publishing on review - #11

Merged
rt2zz merged 3 commits into
mainfrom
feat/review-gate
Oct 19, 2026
Merged

rt2zz merged 3 commits into
mainfrom
feat/review-gate

Conversation

@rt2zz

@rt2zz rt2zz commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Redaction catches secrets that look like secrets. It can't tell that a note names a customer or describes an unreleased plan. This adds an opt-in review gate: a peer whose committed .sidecar sets review = "required" publishes nothing until a review — usually a coding agent, sometimes a person — approves exactly what would leave the machine. Sync on the machine itself is unchanged; sibling worktrees still settle within seconds.

Policy is committed; the reviewer is machine-local (sidecar review reviewer "<command>"), because a committed command would be one any clone could make a daemon run. A review's verdict comes from sidecar review approve|reject, not the reviewer's exit status, so a reviewer can hand the review to a discussion thread and decide later. See docs/review.md.

Changes:

  • Publishing model. Gated peers never push local history. Each approved review publishes one commit of the reviewed tree on the public base it was measured from; the canonical merge combines only what the remote already holds. Every review has a public base: a new remote starts from an empty root, and a remote main that was started over is joined to local main by a link commit, never merged or reset. Before any push, every parent of the publish commit must already be public, and the prepared commit is kept reachable under refs/sidecar-publishing/<id> until it lands. Local main is never reset and is joined to each publish by an -s ours link merge, so edits made after an approval don't fork.
  • One policy per store. Reviews are per repo family and cover local main: a rejection holds every sibling, and siblings never race the same content. A store a gated peer has used is marked (sidecar.review in its git config), and an ungated peer refuses to snapshot, merge, or push through it; working copies with different review policies never share a store.
  • Hermetic publishing git. Everything that builds or sends a gated peer's commits (and every heartbeat) runs under withPublicGit: fixed user@host identity, UTF-8, no hooks, rerere, signing, push options, replacement objects, or local attributes/drivers/filters; --no-follow-tags; remote.origin.pushurl elsewhere is refused. "Already public" comes only from sidecar's own refs/sidecar-public/* fetch, never origin/*.
  • Reviews. Records and packets (diff plus full content of binary/non-UTF-8 files) live in the state dir, owner-only. Each review launches its detached reviewer once; review retry retires the review and opens a new one of the same change, so a stale reviewer can only ever decide a closed review. Approved reviews can't be retried, and a publish in progress can't be rejected. Verdicts are written under the sync lock. Rejected, failed, never-started, reviewer-less, or past-review-timeout reviews need attention in sync, status, and the health heartbeat, which carries the state but never a reason. Optional .sidecar-review guidance is committed policy beside the config.
  • Failure details stay local. Git's messages quote paths, and a gated peer's paths can be unreviewed content. Once a process touches a gated (or unreadable) config or a review packet, its failures reach the terminal only: the event log, the daemon log, and the heartbeat carry the failing step, not the message.
  • Strict config. .sidecar now refuses unknown keys and other versions, so a policy key an older CLI doesn't know fails closed instead of being dropped. Re-running init keeps existing settings for omitted flags, so it can't drop the gate.
  • Commands and docs. sidecar review [show|approve|reject|retry|reviewer], init --review/--review-timeout, snapshot --push refused on gated peers before anything is snapshotted, gated merge and refresh paths, docs/review.md, and updates to README, commands, sync, rules, agent guide, and skill.

Review and testing:

  • Four rounds of review by Codex, a Claude cleanup pass, and a joint Claude/Codex hardening round (second commit); every finding was fixed and reproduced end to end against real repositories.
  • bun run check clean; fast suite 225 passed; bun run test:integration 368 passed on the hardening commit, including new gated-publishing tests; an out-of-tree smoke run of 94 real-git scenarios (two machines, sibling worktrees, rejection→fix, retry, binary/non-UTF-8 content, forged refs, hooks, merge drivers, replacement objects, timeouts) passed on the final commit.

Known limits are listed in docs/review.md: machines on a sidecar from before this change ignore review (update every machine first), heartbeat metadata publishes without review, url.*.insteadOf routing still applies, and turning review off requires unsetting sidecar.review on the clone, after which the next sync publishes the local history review kept back.

rt2zz added 3 commits October 18, 2026 17:34
A peer whose committed .sidecar sets `review = "required"` publishes nothing
until a review approves exactly what would leave the machine. Redaction
catches secrets that look like secrets; a reviewer, usually a coding agent,
catches what pattern matching cannot.

- Gated peers never push local history: each approved review publishes one
  commit of the reviewed tree on the public base it was measured from, and
  the canonical merge is built only from what the remote already holds.
  Local main is never reset and is joined to each publish by a link merge,
  so later edits merge without spurious forks. Reviews are per repo family
  and cover local main, so a rejection holds every sibling worktree.
- Publishing git runs hermetically (withPublicGit): fixed identity, no
  hooks, rerere, signing, push options, replacement objects, or local
  attributes, drivers, and filters. "Already public" comes only from
  sidecar's own refs/sidecar-public fetch.
- Review records and packets live in the state dir. A detached runner
  starts the machine-local reviewer command; verdicts come from
  `sidecar review approve|reject`, taken under the sync lock, so a reviewer
  can hand a review off to a discussion and decide later. Reviews undecided
  past `review-timeout`, rejected, failed, or without a reviewer need
  attention in sync, status, and the health heartbeat, which never carries
  a reason.
- .sidecar parsing is now strict: unknown keys and other versions are
  refused, so a policy key an older CLI doesn't know fails closed.
- Add `sidecar review`, `init --review/--review-timeout`, optional
  `.sidecar-review` guidance, docs/review.md, and tests.
Fixes from a joint review (Claude and Codex) of the review gate.

- A shared store is bound to review: a gated peer marks it
  (`sidecar.review` in its git config) and an ungated peer refuses to
  snapshot, merge, or push through it. Families never link across review
  modes, so an ungated sibling can no longer publish gated work.
- One review launches its reviewer once. `review retry` retires the review
  and opens a new one of the same change, so a stale reviewer can only
  decide a closed review. The attempt files and resetReview are gone; a
  launch that never records a run counts as failed.
- Every review has a public base. A remote main that was started over is
  joined to local main by a link commit, never merged or reset. Publishing
  checks every parent of the cut or resumed commit is public, and keeps the
  prepared commit reachable under refs/sidecar-publishing/<id>. The
  empty-tree packet and parentless publish paths are removed.
- Gated failure details reach only the terminal: once a process touches a
  gated (or unreadable) config or a review packet, cli.ts logs failures
  without a message; the heartbeat carries the stage only; the daemon logs
  a gated peer's exit status and omits watcher error details.
- Re-running init keeps existing settings for omitted flags, so it can no
  longer drop the review gate. `snapshot --push` on a gated peer is refused
  before anything is snapshotted. `review reviewer` takes the command as one
  argument. The gated heartbeat reads its previous record from public refs.
  Refresh points gated work at review, and publishing skips a redundant
  fetch.
Retry is for a reviewer that broke. On an approved review not yet
publishing, it superseded the approval and opened a pending review in its
place, quietly withdrawing a decision. It now refuses; `reject` is the way
to withdraw an approval before it publishes.
@rt2zz
rt2zz merged commit 8d6976d into main Oct 19, 2026
1 check passed
@rt2zz
rt2zz deleted the feat/review-gate branch October 19, 2026 04:32
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