feat(babysitter): standalone fixer that proposes a patch and never pushes

Closed
KH
13d ago
+2320-293

Stacked on #633 (F1). Base is babysitter/f1-feedback-filter; I'll retarget to main once #633 merges.

F2 in the Babysitter owner plan. The goal: a flow that wakes on PR feedback, loads the original session, and fixes the feedback in a sandbox. The design is Revision 1, which the lead approved.

Why propose instead of push

Cloud's PR head mode writes a contents-write token to a file the same-uid coding agent can read. A GitHub App token cannot be limited to one branch, so a prompt-injected agent could push to any branch. RULING-sandbox-push-0902 (cloud) forbids push credentials in sandboxes. The fixer therefore keeps only the #4226 read/comment token and journals a proposal. Cloud publishes it server-side: one commit with parent = bound head, then force:false on the ref update. FIXER.md is the Cloud contract (C1').

What's here

  • admission.ts: the admission prelude, extracted unchanged from standalone.ts and shared by both bodies. Every pre-existing standalone test passes unmodified, except the inline-comment rendering, which now shows #id.
  • fix.ts:
    • checkoutHead sends the token as an HTTP header via GIT_CONFIG_*, never in argv or .git/config, with hooks off. An existing checkout is fetched, reset and cleaned in place, keeping ignored dependencies. That is the hook for per-PR sandbox reuse (C2).
    • proposeChanges builds a bounded binary-safe patch against the bound head, including the agent's own commits. It refuses workflow and secret paths, more than 50 files, more than 36KB of patch, or a proposal over 50KB.
  • fixer.ts: checkout → one babysitter-fix agent in the checkout, with fix-mode rules → reread (declines if the head moved or the PR left scope) → proposal.
    • Replies only go to inline feedback that woke this run, once per thread. They carry a <!-- babysitter:reply … --> marker, mentions are neutralised, and the origin prompt is redacted.
    • No push, merge, review or comment call exists in the fixer, and a test asserts it.
  • artifacts/babysitter-fixer.flow.ts: sha256 a6cb1f4e9d16984fe6711912cd1843a9f710ab1a91085bf03625266ec6f9dd06. The standalone artifact moves to 6ea6982e… because of the refactor.

⚠️ Gate edits (please review explicitly)

  • tests/standalone-artifact.test.ts now loops over both artifacts. Same assertions, plus one new test: no __require( in either artifact.
  • packages/sdk/tests/shipped-source-models.test.ts gets a call-exact waiver "babysitter-fix" for the fixer artifact, the same shape as #616's "babysitter-diagnose".
    • The audit first failed on my budget: 1.5M tokens / $10 broke the ≤100k tokens-per-dollar rule (05a-…red-budget.txt). I fixed the budget to 1M / $10, not the gate.

A bug this caught before shipping

esbuild rewrites require(...) into a __require shim. That shim does not exist once a function's source is lifted into node -e, so checkout and proposal would have crashed in production while the source tests passed. Builtins now load with import(). A test runs proposeChanges as lifted from the shipped artifact under sh -c 'node -e …'.

Evidence (examples/babysitter/evidence/f2-fixer/, unedited)

  • 01-mutation-require-shim.txt: I reverted to require, rebuilt, and ran the tests. the proposal script lifted from the shipped artifact runs under node -e fails with __require is not defined. Restored byte-for-byte (cmp → identical), then 15/15 pass.
  • 02-all-babysitter-green.txt: 124 tests, 117 pass, 0 fail, 7 todo (the existing deliberate gates).
  • 03-artifact-check.txt: both digests reproduce.
  • 04-flows-check.txt: flows check passes for both artifacts.
  • 05-sdk-shipped-source.txt: 2/2 pass.
  • 06-typecheck.txt: 0 errors outside the environment-only classes documented in #633's 06-typecheck.txt. It caught one real error (report typed as Promise), which is fixed.

Not proven here

  • The checkout never ran against GitHub. checkoutHead is exercised only through the mocked f.run, because it needs network and a token. The live run (Step 3) is its first real exercise.
  • Nothing publishes a proposal yet. That is C1' in cloud.
  • The agent's cwd must be inside the run root. The checkout lives at <run root>/babysitter-checkout, which assumes f.run and the agent share a run root. If they don't, the step refuses and fails closed. The live run will show which.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Introduces an agent with read/write checkout and automated patch proposals; risk is mitigated by no sandbox push, path/refusal limits, and git hardening, but Cloud must enforce the same limits when publishing.

Overview
Adds a standalone Babysitter fixer that wakes on PR feedback, checks out the bound head, runs one read/write agent with fix-mode rules, and journals a babysitter-proposal (patch + neutralised summary and inline thread replies) instead of pushing—aligned with sandbox push-credential rules and FIXER.md’s Cloud publish contract.

Shared admission moves into admission.ts for both diagnose and fix flows; origin.ts gains fix/diagnose agentTask modes and review comment #ids in prompts. fix.ts implements hardened checkout and proposal: token via GIT_CONFIG_*, reset of agent-tampered .git, refused workflow/secret paths and size limits, NUL-delimited paths with core.quotePath=false, and dynamic import() for scripts lifted into node -e. build-standalone.mjs now builds babysitter-fixer and babysitter-standalone artifacts with updated digests, plus broad fixer tests and evidence.

Merging does not enable publishing; Cloud must admit the fixer manifest SHA and implement C1'.

Reviewed by Cursor Bugbot for commit 0114937527a43dea9b2352a4a589fb6c5601a11e. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds the standalone Babysitter fixer, completing the second phase of the owner plan. It wakes on PR feedback, checks out the bound head, runs one agent to fix the feedback, and journals a bounded proposal — a patch against the bound head plus thread replies — instead of pushing. The sandbox holds only the run's read and comment token, per the cloud ruling that forbids push credentials in sandboxes; Cloud publishes the proposal as a fast-forward-only commit, with FIXER.md as the contract.

  • Extracts the admission prelude into admission.ts, shared with the diagnose body; the standalone artifact's digest changes, and inline-comment rendering now shows the comment #id.
  • Checkout sends the token as an HTTP header via GIT_CONFIG_*, never in argv, with hooks off; an existing checkout is fetched, reset, and cleaned in place for per-PR sandbox reuse.
  • Hardens checkout and proposal scripts against an agent-planted .git config: both replace .git/config with a minimal known config and remove hooks and info/attributes before running git; the proposal diff also passes --no-ext-diff and --no-textconv so the daemon never executes agent-controlled drivers.
  • The proposal reads changed paths NUL-delimited with core.quotePath=false, so a newline in a filename cannot slip a workflow path past refusal and Cloud parses back exactly the checked paths.
  • Proposals refuse workflow and secret paths, more than 50 files, patches over 36KB, and proposals over 50KB.
  • Replies go only to the inline feedback that woke the run, once per thread, mentions neutralised and origin prompt redacted; the fixer has no push, merge, review, or comment calls, which a test asserts.
  • Fixes a shipping bug where the bundler's rewritten require shim crashes under node -e; builtins now load with import(), proven by a test running the proposal function lifted from the shipped artifact.
  • The fixer calls f.agent with a literal cli/model pair per origin CLI, so the shipped-source audit passes unmodified: no waiver needed, and the fixer ignores the policy's agentCli.
  • Build, artifact, and shipped-source tests cover both artifacts, with the fixer's budget set to 1M tokens / $10.

Not yet exercised

  • Checkout has only run against a mocked f.run; the live run is its first real GitHub exercise.
  • Nothing publishes a proposal yet — Cloud does that server-side per FIXER.md.

Agent Relay sessions

  • claude session 497569fc · opened via gh pr create · last active 2026-10-09