Skip to content

Autonomous thread-resolve: pin pre-push isOutdated state (per-thread comment pins don't stop own-push displacement) #571

Description

@kyle-sexton

Summary

Follow-up from Codex P1 on #561 (thread on plugins/source-control/skills/babysit-prs/scripts/babysit_resolve_thread.py). The per-thread pins added in #561 (--expected-comment-count, --expected-last-updated) close the bulk-resolve and comment-drift gaps, but they do not close the specific hole the PR set out to close: an autonomous worker clearing a bot thread that its own push merely displaced (not addressed).

The gap

--expected-comment-count and --expected-last-updated pin the comment count and latest comment-edit timestamp. A code push that displaces a finding's anchored lines flips GitHub's isOutdated to true without adding a reply or editing a comment — so both pins still match. Under --autonomous, classify() requires isOutdated == true and accepts the now-outdated thread; the unchanged comment pins match; the resolve succeeds. The finding is cleared even though the worker's push only displaced it.

This falsifies the property the PR docstring (lines 26–33) and SKILL.md step 7 (~L398–402) currently claim: "a worker cannot clear a thread its own push merely displaced."

Why it is not a trivial patch

isOutdated == true because my push addressed the finding and isOutdated == true because my push displaced the finding are deterministically indistinguishable at the post-push head: same flag, same unchanged comments. A post-push --expected-outdated pin is inert (it reads true in both the legit and attack cases). The only deterministic discriminator is: resolve only threads that were outdated at the pre-push head — i.e. add --expected-head and require the resolve to run while the live head still equals the vetted pre-push head, moving thread resolution to before the worker's push (an active thread is then refused naturally by classify()'s isOutdated gate).

The posture decision this forces (why it is deferred, not auto-fixed)

The merge gate (babysit_merge.py:253) blocks on any unresolved review thread regardless of isOutdated. Under the pre-push-head fix, a thread that the worker's own fix genuinely addressed only becomes outdated after the push, so it can no longer be autonomously resolved — it will block the merge gate and require attended resolution. That is a defensible security posture (an actor must not clear findings on the strength of its own unverified push), but it materially reduces autonomous-merge reach for the common case (worker fixes a finding → that finding's thread blocks the gate). Choosing that trade-off — vs. a narrower discriminator (e.g. gating on the reviewing bot's re-review/dismissal signal rather than raw isOutdated), vs. accepting the residual risk — is a design/policy call that should be made deliberately, not bundled into a review-response pass on #561.

Note: no deterministic guard defeats an actively malicious worker that reorders (push, then pin the post-push head, then resolve); isOutdated-based distinguishability is impossible there. The realistic goal is making the honest documented flow fail-closed.

Proposed work

  1. Add --expected-head to babysit_resolve_thread.py; require live PR head == pinned head for --autonomous --resolve (mirroring babysit_merge.py's --expected-head, but anchored to the pre-push head).
  2. Rewrite SKILL.md step 7 to resolve pre-push-outdated bot threads before the worker's push, pinning the pre-push head.
  3. Correct the docstring (L26–33) so the stated guarantee matches what the guards deliver.
  4. Decide and document the merge-gate posture for own-push-addressed threads (attended resolution vs. an alternative discriminator).
  5. Add a test mirroring the attack trace: not-outdated-at-snapshot → refused.

Source: #561 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.wayfind: designWayfind decision item: design-space or domain-model decision; human in the loop.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions