Skip to content

chore: Duplicate-PR guard conflates "exists" with "correct" — blocks re-review of a wrong-content PR indefinitely - #628

Merged
getappz merged 1 commit into
masterfrom
task/192-duplicate-pr-guard-conflates-exists-with
Aug 28, 2026
Merged

getappz merged 1 commit into
masterfrom
task/192-duplicate-pr-guard-conflates-exists-with

Conversation

@getappz

@getappz getappz commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Item work (PR identity, task 0): fixed duplicate-PR guard to exclude self-repair's own PR (via persisted metadata.pr.number) and stale CI-red duplicates (>3d unmerged+failing); tests 67/67, build+clippy clean; not yet committed.


Opened by claude-code on flared:51bb8de6c33b for item #192 via agentflare.

…self-repair's own PR (via persisted metadata.pr.number) and stale CI-red duplicates (>3d unmerged+failing); tests 67/67, build+clippy clean; not yet committed.

Agentflare-Agent: claude-code_2-1-246_agent
Agentflare-Branch: task/192-duplicate-pr-guard-conflates-exists-with
Agentflare-Item: 192-duplicate-pr-guard-conflates-exists-with
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 21 minutes.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 56 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b1ab27e-2f5b-4095-a2b4-351b13733cc6

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1e46d and 8bded36.

📒 Files selected for processing (4)
  • src/cli/work_duplicate_pr.rs
  • src/cli/work_duplicate_pr_tests.rs
  • src/github/models.rs
  • src/worktree.rs

Comment @coderabbitai help to get the list of available commands.

@getappz
getappz merged commit c02beb2 into master Aug 28, 2026
17 checks passed
@getappz
getappz deleted the task/192-duplicate-pr-guard-conflates-exists-with branch August 28, 2026 08:33
getappz added a commit that referenced this pull request Sep 22, 2026
* fix(mcp): reject plan_required with no assignee_agent and no plan

item(action="create"/"update") let a caller set plan_required: true (or a
non-empty plan_approver) with neither an assignee_agent nor a submitted
plan, producing an item no agent can ever claim to write a plan for and
no human has anything to review yet. Item #628 hit exactly this: it sat
inert for ~40 minutes, gated every supervisor tick with no comment
explaining why.

validate_plan_gate_claimable now rejects that specific combination on
create/update while leaving the normal gate-then-plan-later flow (gate
with an assignee, no plan yet) and the plan-up-front flow (no assignee,
plan_asset_id provided) untouched.

Also: the supervisor's plan-gate skip was logged server-side every tick
but never posted to the item's own comment thread, unlike the
no-assignee skip which does post one. It now posts once per gate
(mirroring first_time_gated's existing pattern) so the item's comment
thread explains why it's stuck.

Agentflare-Agent: claude-code
Agentflare-Branch: task/629-item-create-update-lets-plan-required-tr
Agentflare-Item: 629
Agentflare-Session: bfffbfdb-38b6-4dfc-b22e-6e99e3f7172d

* fix(test): pin owner identity in end_to_end_plan_gate_blocks_then_unblocks_claim

item 629's new claimability check requires the test's create call to set
assignee_agent="claude-code", but the test's final claim() relied on ambient
agent-detection (owner_id() -> agent_detector::agent_name()) to match that
same identity. That resolves to "claude-code" only when actually running
inside Claude Code, so the test passed locally but failed deterministically
on CI's bare Ubuntu runner (falls back to "cli", tripping claim()'s
BlockedByAssignee handoff freeze). Wrap the test body in
with_owner_override("claude-code:test", ...) so it no longer depends on the
environment it happens to run in.

Agentflare-Agent: claude-code
Agentflare-Branch: task/629-item-create-update-lets-plan-required-tr
Agentflare-Item: 629
Agentflare-Session: ceb8a60f-fbb3-4fb4-9c7b-92a9d64885c8

* fix(test): pin owner identity in item_claim_blocked_by_plan too

Same bug as end_to_end_plan_gate_blocks_then_unblocks_claim: this test's
create call also needed assignee_agent="claude-code" added (to satisfy
plan_required's new claimability check), which made its final claim()
depend on ambient agent-detection matching that identity -- true only
when actually running inside Claude Code. Wrap it in the same
with_owner_override("claude-code:test", ...) pattern.

Agentflare-Agent: claude-code
Agentflare-Branch: task/629-item-create-update-lets-plan-required-tr
Agentflare-Item: 629
Agentflare-Session: ceb8a60f-fbb3-4fb4-9c7b-92a9d64885c8
getappz added a commit that referenced this pull request Sep 25, 2026
…inned MCP merges (#809)

* fix(worktree): self-heal wedged task worktrees and stop clobbering remote work

Multi-agent runs kept leaving task worktrees in states nothing recovered
from automatically. create_worktree/rebase/commit/push now heal them:

- Detached HEAD / other branch: a clean checkout is switched back onto
  its task branch in place (fast-forwarding over detached commits, or
  pinning diverged ones under refs/agentflare/rescue/) instead of
  `worktree add` failing on the occupied path every redispatch.
- Interrupted git: abandoned index.lock (>10 min, or ours after a
  timeout kill) is removed and a half-finished rebase aborted; the abort
  result is checked instead of discarded.
- Half-created checkout (worktree add killed mid-populate, "initializing"
  lock left) is recreated rather than reused and committed as mass
  deletions.
- Broken .git pointer: repaired, else snapshotted and cleared; commit and
  cleanup refuse to run in it (git would act on the main repo).
- push uses --force-if-includes and, on rejection, integrates
  origin/<branch> and retries once, so human/update-branch commits on the
  PR branch are kept instead of dropped or rejected forever.
- worktree add retries recompute -b after a partial success, match real
  ref/index lock errors, and run under a timeout.
- branch_diverged checks against origin/<target> as well as the (never
  pulled) local target.
- gc_orphans drops only the removed path's registration instead of a
  repo-wide `worktree prune`; relative gitdir pointers are resolved;
  the branch-scoped cleaner honors locked registrations.
- cleanup pins detached-only commits before removal; squash_since
  refuses a base that is no longer an ancestor of HEAD.

* fix(git): run daemon git non-interactively in C locale; lock claimed worktrees

- Every git spawn now gets LC_MESSAGES=C/LANGUAGE= (LC_ALL removed):
  lock-race retries, push-rejection recovery and the force-if-includes
  fallback all classify git's English stderr, and silently did nothing
  on a non-English workstation.
- GIT_TERMINAL_PROMPT=0 / GCM_INTERACTIVE=never: a credential prompt
  fails fast as an auth error instead of hanging until a timeout, or
  forever for calls without one.
- GIT_OPTIONAL_LOCKS=0: the daemon's read-only status polling no longer
  takes index.lock in a worktree an agent is committing in.
- Claimed worktrees are `git worktree lock`ed with an agentflare reason
  so an outside `worktree prune`/gc can't drop their registration; our
  own cleaners unlock/remove only registrations carrying that reason.
- A child item stops targeting its parent's branch once the parent is
  completed or cancelled, instead of stacking on an already-landed
  (squash-merged) branch and conflicting on every rebase.
- A worktree whose changes can't be auto-committed because its checkout
  is broken now fails `done` loudly (item #92) instead of reading as
  "nothing to commit".

* fix(claims): atomic, state-gated item transitions; stop double dispatch

- mark_completed / mark_in_review / promote / redispatch / release use
  IMMEDIATE transactions: DEFERRED read-then-write failed instantly with
  SQLITE_BUSY_SNAPSHOT under concurrent writers, erroring `done` after
  its PR was already open.
- mark_in_review / mark_completed only move items in the started or
  in_review group, so a late finalize can't revive a cancelled,
  completed or redispatched item.
- promote_in_review_to_completed_if_pr compare-and-sets on the PR number
  that was verified merged.
- acquire_if_stale_only / ClaimLedger::acquire_existing: a steal never
  re-creates a released claim row, so a reassigned job can't take the
  item back and publish.
- Supervisor dispatch decision waits on ANY live claim (the item's own
  assignee session included -- it was invisible, causing dispatch ->
  Held -> failure-cap -> needs-manual-dispatch) using the real 4h item
  claim TTL instead of the 30 min repo-claim TTL; same TTL fix in
  handoff. New tier waits while blocking dependencies are incomplete.
- Fallback claim owner is <agent>:<pid>-<random>, unique per process,
  instead of <agent>:<pid> which collided across sandboxes/machines.
- `git worktree audit/doctor`: claimed_sequence_ids and
  item_state_groups read backend.db (they read the wrong database and
  always came back empty), are scoped to the repo's own project, and the
  claim guard fails closed.

* fix(restart): executor leases for workflow runs; reap orphaned agents; single daemon

After a daemon crash/stop/upgrade work could be double-run, waited on
forever, or left with an old agent still editing the worktree.

- flare-workflow: runs carry an executor lease (host:pid:nonce, renewed
  every TTL/4, stored in state_json so older binaries still load rows).
  recover() skips runs another live process is driving, takes over stale
  ones (a same-host lease with a dead pid is stale immediately), and
  isolates per-run failures instead of aborting all recovery on one bad
  row. New resume_run / is_driving / is_leased_elsewhere / recover_with
  and patch_run_data (a context patch that survives an in-flight step's
  write-back).
- agentflare-jobs: a watchdog timeout now flags the attempt cancelled so
  its agent children and run actually stop before the retry, instead of
  abandoning a live thread that keeps heartbeating the claim.
- Launched agents record pid + start-time under agent-pids/<worktree>;
  orphan reconcile kills that recorded group (not only `pgrep -f`, which
  never matched on macOS/Windows where the path isn't in argv) and, on
  Linux, processes whose cwd is inside the worktree -- except ones with a
  controlling terminal, i.e. a human's shell.
- orphan reconcile: the terminal-failure label swap runs in one BEGIN
  IMMEDIATE transaction (retried), so an item can no longer end up with
  neither `dispatched` nor `ready-for-work` and vanish from dispatch;
  restore_ready_for_work uses IMMEDIATE too.
- daemon: the lock file is never deleted (deleting it let two processes
  both "hold" it) and a lifetime flock is the liveness test.

* fix(completion): cross-project review sweep, closed PRs, no-PR repos, atomic metadata

- Review sweep uses a project-scoped MCP view per batch: items outside
  the daemon's cwd project could never be promoted and sat in_review
  forever with the lease held.
- PrCiStatus::Closed (REST + GraphQL): a PR closed without merging
  requeues its item (redispatch to backlog, lease released, metadata.pr
  cleared, one comment) instead of leaving it in_review forever; a closed
  own PR is never reused.
- push_and_open_pr returns PrOutcome; "no PR possible by configuration"
  (non-GitHub/no origin/no credentials) completes the item with
  metadata.no_pr instead of failing `done` forever; transient failures
  still error so finalize retries.
- find_existing queries head=<owner>:<branch> (one request, no unbounded
  pagination), prefers metadata.pr.number, and a lookup error never
  falls through to creating a duplicate PR.
- Metadata writes (PR identity, run id, item_update) re-read, merge and
  write in one IMMEDIATE transaction instead of clobbering concurrent
  writes from a snapshot.
- Finalize treats item_done's "unchanged" as a failure (re-armed by the
  terminal-failure hook) rather than reporting success.
- item_claim rolls back the claim and prior state when the worktree
  can't be created (non-retryable); item_done refuses completed or
  cancelled items; discovery resumes its own earlier PR claim.
- Supervisor: one project's DB error or missing folder no longer aborts
  the discovery tick for every project; per-project cap on active work
  jobs when several projects have ready items; job_in_flight uses an
  EXISTS query instead of the 100 newest jobs; a failed boot recover()
  retries every 60s instead of leaving dispatch off until restart.
- Restart wiring: an attached run adopts the current owner (finalize was
  refused after a restart and all work redone), stale runs are taken
  over instead of waited on forever, a lost claim or cancelled job stops
  the run, and boot recovery skips runs whose item is already finished.
- done/release steal only an existing stale claim: a job that lost its
  item to a reassignment can't re-mint the lease and publish.
  promote compare-and-sets on the PR number verified merged.

* feat(sessions): live agent-session registry in agentflare.db

One row per running agent session (interactive or daemon job), keyed by
its claim owner. Liveness is exact by pid on this host (so an idle but
open session stays live) and by last_seen_at elsewhere. Foundation for
releasing dead sessions' claims in minutes instead of after the claim
TTL, and for inter-agent messaging.

* chore(claude): SessionStart hook installs lean-ctx and prefetches crates

Web sessions get lean-ctx (via cargo install -- the npm installer's
GitHub releases download is blocked by the sandbox proxy), onboarded
without shell aliases and with rustfmt/python3/timeout on its shell
allowlist, plus cargo fetch so tests and clippy start warm.

* feat: claim liveness, job controls, agent failover and realtime messaging

Claim liveness (src/claim_liveness.rs): every discovery tick judges each
item claim's owner -- a daemon job by its job row, a session by the
session registry (exact pid on this host), a pid-form owner by pid plus
claim silence -- and releases a dead owner's claim after two consecutive
dead verdicts, commenting and re-arming the item. Claims of crashed
interactive sessions or killed `agentflare work` processes are freed in
about a minute instead of after the 4h TTL. release/done take over a
dead owner's claim immediately. Headless runs register their session.

Job controls (src/job_controls.rs, flare-workflow, agentflare-jobs):
- cancel really stops: cancels the item's run and queued/running jobs,
  kills the agent, releases the claim, and is never retried/re-queued.
- pause/resume: the engine stops at the next step boundary (the stopped
  step is not journaled as a failure) and resumes the same run in the
  same worktree; a `paused` label holds the item meanwhile.
- `agentflare job cancel|pause|resume`, `agentflare item
  cancel|pause|resume|redispatch [--agent]`, MCP item actions, dashboard
  POST /api/jobs/{id}/cancel and /api/items/{id}/{action}, chat
  /cancel /pause /resume /redispatch.
- handoff of a claimed item to another agent releases the old claim and
  cancels its jobs.

Agent failover (src/auth_runner_classify.rs, src/quota/failover.rs,
src/cli/work_failover.rs, agent-registry): agent failures are classified
(short rate limit, long rate limit, credit exhausted, usage window with
parsed reset time, auth expired). Credit/window exhaustion and long rate
limits cool the agent down until its reset and move the item to the next
available agent immediately, without charging the item's dispatch
failure ceiling; with no alternative the retry is scheduled at the reset
time. Configurable via AGENTFLARE_FAILOVER, [failover] in config.toml,
and per-item allowed_agents / failover:false. A hung agent whose output
only repeats while its worktree doesn't change is killed after 45 min
(AGENTFLARE_STALL_WINDOW_SECS); repeated mid-run exhaustion errors kill
it early into failover.

Realtime inter-agent messaging (src/messages.rs, src/hook_messages.rs):
MCP tool `message` (send/list/inbox/read/whoami), CLI `agentflare
message send|list|inbox|read|whoami|watch`, dashboard /api/sessions,
/api/messages and an SSE stream, chat /msg. Addresses: session key or
name, item:<id>, agent:<name>, *. Claude Code receives messages on its
next tool call via PreToolUse additionalContext and a Stop hook keeps it
from going idle with mail waiting; other MCP hosts get them piggybacked
on agentflare tool results; headless runs drain them between turns.
Bodies are wrapped as untrusted peer input in <agentflare-message>
envelopes that a body cannot forge or close.

* refactor: cut boilerplate in the item/pipeline flow; back under LOC gate

Behavior-preserving:
- AgentflareMcp::post_item_comment replaces 11 copies of the
  best-effort `comment_impl(CommentRequest{action:"create",..})` call.
- metadata_object() replaces the copied coerce-metadata-to-object chain
  (merge_item_metadata, handoff merges, persist_comment_cursor).
- item.rs: require_id / internal_err replace 13 id preambles and 12
  internal-error mappings; the progress sender is fetched once instead
  of calling create_worktree / push_and_open_pr through two branches.
- claims::ttl_secs delegates to the backend's identical default; wrapper
  tests that duplicated db-kit's ClaimLedger tests are dropped.
- groom/standup/health handlers move verbatim to item_reports.rs and the
  finalize step to work_item_pipeline/finalize.rs: item.rs 3113 -> 2633
  and work_item_pipeline.rs 1749 -> 1488 lines, both back under
  scripts/loc-gate.sh limits.

* fix(github): pin merges to the checked SHA; honor statuses, required checks, reviews, rate limits

- merge / update-branch send the head SHA the CI verdict was computed on
  (merge "sha", update-branch expected_head_sha); a head that moved in
  between is skipped for the tick instead of merging unverified code.
- CI status counts legacy commit statuses (Jenkins/CircleCI/Vercel-style
  StatusContext, REST /commits/{sha}/status) alongside check runs, and
  decides on required contexts only when the repo marks any required;
  statusCheckRollup is the fallback. A clean, mergeable PR with no CI at
  all is Passing instead of Pending forever.
- New PrCiStatus::AwaitingReview (blocked + REVIEW_REQUIRED /
  CHANGES_REQUESTED with green checks) takes the approval-gate path, so
  the human is actually pinged, with no merge attempt.
- Secondary rate limits (403/429 with retry-after or "secondary rate
  limit"/"abuse") map to RateLimited instead of a token-scope error; a
  shared per-host backoff deadline short-circuits calls until it passes;
  mutating requests are serialized >= 1s apart; GraphQL RATE_LIMITED
  errors map to RateLimited.
- The gh/vault token is resolved once per process and refreshed on 401.
- After a merged item is promoted, its remote head branch is deleted
  unless the repo auto-deletes, it is the default/protected branch, or
  the head lives in a fork.
- PRs for items bridged from a GitHub issue include "Closes #N" (or
  owner/repo#N across repos).

* refactor: split supervisor, its tests and dashboard server back under the LOC gate

Pure moves, no behavior change: the PR merge/promotion/closed-PR block
to supervisor/merge.rs, the auto-merge test group to
supervisor/tests/auto_merge_tests.rs, and work-dispatch boot/recovery
retry to dashboard/dispatch_boot.rs (supervisor.rs 2888 -> 2472,
supervisor_tests.rs 2810 -> 2549, server.rs 1550 -> 1463).

* refactor: split flare-git-core worktree and flare-workflow engine under the LOC gate

Pure moves, no behavior change: worktree self-heal and push-lease
helpers to worktree_heal.rs, subprocess timeout/kill to
worktree_process.rs, orphan audit/gc to worktree_orphans.rs, this
branch's heal tests to worktree_heal_tests.rs; engine executor-lease /
recovery to engine_lease.rs and pause/resume/cancel to
engine_control.rs. Public paths are unchanged via re-exports
(worktree.rs 1888 -> 1214, worktree_tests.rs 1839 -> 1461,
engine.rs 1873 -> 1434).

* fix(pipeline): a cancelled run always reports as a deliberate stop

An operator cancel marks the workflow run Cancelled before it flags the
job, so a waiter polling in between reported a generic failure (flaky
pause_tests::cancel_stops_the_running_job_and_its_run_for_good under
load). A Cancelled run needs no job-flag check.

* fix(claude): SessionStart hook downloads prebuilt lean-ctx instead of compiling

The hook ran `cargo install lean-ctx --locked`, which on the 4-core web
sandbox compiles for well over 10 minutes and hit Claude Code's 600s
SessionStart timeout on both startup and resume, so it never reached
`lean-ctx onboard` and sessions started without lean-ctx at all.

Install the prebuilt release tarball from the fixed
github.com/yvgude/lean-ctx/releases/latest/download/<asset> URL instead,
verified against the release's SHA256SUMS. That path needs no GitHub API
call (api.github.com answers 403 from the sandbox's shared egress IP,
which is also why `npm install -g lean-ctx-bin` fails there) and takes
seconds. `cargo install` stays as the fallback, and settings.json raises
the hook timeout to 1800s so the fallback can finish if it is ever
needed. LEAN_CTX_VERSION pins a release; unset tracks latest. The binary
lands in ~/.local/bin, which the hook also exports via CLAUDE_ENV_FILE.

Validated in the sandbox: full hook run (download, checksum, onboard,
allowlist, cargo fetch) completes in ~5s.

* fix: address review findings on the pipeline hardening PR

Workflow engine / jobs:
- start_workflow claims `driving` before save(), so an in-process
  recover() can't start a second driver for a brand-new run.
- A lease takeover marks the run superseded: the old driver stops at the
  next check without writing; terminal writes are refused while another
  executor holds a fresh lease. cancel only moves unsettled runs.
- A paused/superseded driver drains in-flight parallel steps before
  letting go, so resume can't relaunch a sibling that is still running.
- request_cancel refuses running subprocess jobs (NotCancellable)
  instead of reporting success for a job it can't stop.
- An abandoned attempt that finishes Ok during the grace period is
  recorded as completed, not failed and retried.

Git / GitHub:
- Integrating commits pushed to the PR branch by someone else merges
  origin/<branch> instead of rebasing onto it (which replayed new
  target commits as the PR's own).
- audit_orphans skips a task dir that isn't its own checkout.
- Merged-branch deletion keeps a branch whose tip moved past the merged
  head (BranchCleanup::Advanced).
- Rate-limit backoff is tracked separately for anonymous and
  authenticated requests; GraphQL RATE_LIMITED arms the backoff and
  stops the chunked batch.
- Auth errors are logged as a fixed string (GitHubError::log_safe).
- SessionStart hook path is quoted.

Messaging / sessions / failover:
- `message watch` and the SSE stream requeue a taken message whose
  print/send failed instead of losing the rest of the batch.
- Envelope-tag escaping is case- and whitespace-insensitive.
- The MCP server's provisional session row is deleted, not ended, so
  its live claims aren't judged dead.
- Session host identity uses the kernel hostname (same for daemons and
  shells), then the machine id, before "localhost".
- A typo'd failover allow-list disables failover instead of widening to
  any agent; the agent-wide "no alternative" cache is only armed for
  unrestricted items; short rate-limit retries post the neutral
  agent-unavailable marker; a stale cancelled run id no longer makes a
  fresh dispatch's setup error read as a cancel; handoff compares
  canonical agent names.

* fix(worktree): match stale registrations by canonical parent, not raw path

Windows CI: git records a worktree's gitdir under the long path
(C:/Users/runneradmin/...) while the caller can hold the 8.3 short-name
(RUNNER~1) spelling of the same directory, so the path-scoped
registration cleaner never matched and left a prunable registration
behind after cleanup (which also blocks a later re-claim). Compare via
the canonicalized parent directory, which still exists after the
checkout is removed, case-insensitively on Windows.

* docs(audits): Claude Code feature audit and gap analysis

Audit of every way agentflare uses Claude Code (host instrumentation via
init, headless worker via work/run/workflows/apps, and the agent-agnostic
registry) against Claude Code's full documented surface: CLI flags,
settings keys, all 33 hook events and 5 hook types, memory, skills,
subagents, MCP, plugins, sessions, cloud integrations and the Agent SDK.

Findings are ranked P0-P2 with file:line evidence, a per-feature status
matrix, the current per-agent capability table, a list of Claude-Code-
style features worth universalising with the agentflare mechanism each
maps onto, and a roadmap.

Two bugs surfaced by the audit are recorded for follow-up rather than
fixed here: `agentflare work` passes both `--output-format json` and
`--output-format stream-json`, which makes the whole-stdout JSON parse
fail and silently drops session_id and cost (so SDD-loop `--resume` never
fires in real dispatch); and `agentflare run --mode` forwards `--mode`,
which is not a Claude Code flag (`--permission-mode` is).

Force-added under docs/ like docs/superpowers/plans, since /docs/ is
gitignored at the root.

* fix: propagate lean-ctx prebuilt install failures and escape table pipes

Steps inside install_lean_ctx_prebuilt now return explicitly, since set -e
is suppressed under 'if !', so a failed download/extract/install falls
back to cargo. Pipes inside code spans in the gap-analysis tables are
escaped so the rows render with the right column count.

* fix(sessions): qualify host identity with the machine id

Two machines sharing a hostname and a db resolved to the same host, so
liveness_of checked one machine's pids against the other's. The host is
now <hostname>@<machine-id> when both are available.

* fix(launch): stop emitting two --output-format flags; add per-role specs for SDD dispatch

The audit (docs/audits/2026-09-24) found that `agentflare work` launched
Claude Code with `--output-format json` (from run_headless_impl when a
structured reply was requested) followed by `--output-format stream-json
--verbose` (from build_extra_args). The last flag wins, stdout became a
multi-line stream-json transcript, `parse_json_reply` failed to parse it
as one object, and every dispatch silently lost its session_id and cost —
so the SDD loop's `--resume` between fix rounds never fired in real runs.

- `headless_full_args` skips the registry's JSON-output flags whenever the
  caller already pins `--output-format` (any spelling, incl. `--stream-json`).
- `parse_json_reply` falls back to the final line when the whole stdout is
  not a single JSON object, so both output shapes yield session and cost.
- `agentflare run --mode` forwarded a bare `--mode`, which Claude Code
  rejects; `agent_registry::mode_flag` maps it to `--permission-mode` for
  Claude Code and keeps the pass-through for other agents.

Roadmap P0 item 2, first slice: `agent_registry::role` introduces
`RoleSpec` (system prompt, allowed/disallowed tools, permission mode) and
`compile_role(agent, &spec)`. Claude Code compiles to
`--append-system-prompt`, `--allowedTools`, `--disallowedTools` and
`--permission-mode`; every other agent gets an empty argv and the identity
folded into the user prompt via `prompt_with_system_fallback`, never an
unconfirmed flag. The SDD loop (`work_item_pipeline/roles.rs`) now gives
each dispatch a role: implementer keeps every tool, the analyst and the
reviewers lose the editing tools (Bash stays for verification), the judge
also loses the shell. Resume args still lead the argv; a mid-flight
correction still leads the prompt.

Docs: README and AGENTS.md no longer describe `optimize context` as
PreCompact-driven compaction (the hook is inert); the audit's roadmap and
bug list record what landed here.

Verified: agent-registry tests (68), targeted agentflare bin tests for
agent_launch/work_item_pipeline/cli::work (258), `cargo clippy` with the
CI flags on both crates, `cargo fmt --check`.

* feat(mcp): pin the pr_merge action to the head SHA the caller last saw

The MCP flare_git pr_merge action merged whatever the PR head was at the
moment of the call, so a commit pushed after the caller judged CI green
rode along unchecked. It now goes through pulls::merge_at_head like the
supervisor's own auto-merge: a new head_sha parameter pins the merge to
the head the caller saw, and without one the current head is read first
and pinned, closing the read-to-merge window. GitHub's 409 on a moved
head is reported as an invalid-params error naming both SHAs and the
head_sha to retry with. pr_get and pr_status now report head_sha so a
caller has the value to pass back.

* fix(supervisor): approval card names a GitHub review as the blocker, not the label

When branch protection held a CI-green PR for a review (PrCiStatus::
AwaitingReview), the Telegram card still said the PR was mergeable and
awaiting status:pr:approved, and a human tapping Approve got nothing:
the label cannot merge a review-blocked PR. The card now says GitHub is
blocking the merge until a review lands, whether an approving review is
required or a reviewer requested changes (AwaitingReview and
CiGreenMerge::BlockedOnReview now carry changes_requested), links the
PR's review, and keeps the Approve button only while the label is also
missing. The review card has its own once-per-item key so the plain
label card is still sent once the review lands with the label absent.

* feat(supervisor): arm GitHub's native auto-merge and support merge queues

Once an item's PR carries the approval label and its CodeRabbit findings
are clear, the sweep now calls enablePullRequestAutoMerge (pinned to the
head it judged with expectedHeadOid) when the repo allows auto-merge, so
GitHub itself merges the PR the moment every requirement holds: this is
what lets a review-blocked PR merge as soon as the review lands, and the
only way onto a merge queue. The direct merge_if_approved path stays as
the fallback when the repo has auto-merge off or GitHub refuses to arm
it (a PR it could merge right now), and promotion of merged items stays
in the sweep. Auto-merge is disarmed again when findings appear or the
label is removed, keeping item #628's invariant.

The batch GraphQL query now also fetches the PR node id, autoMergeRequest
and the merge-queue flags; a base branch with a merge queue never reads
"clean", so green required checks on a queue-enabled branch count as
Passing instead of Pending forever, and an enqueued PR waits. The merge
method comes from the repo's allowed methods (GET /repos, cached with
repos::settings: squash, then merge, then rebase) instead of a hard-coded
squash. GraphQL mutations now get the same write spacing as REST writes.
CI gains a merge_group trigger so a queue can run the required checks.

* feat(github): conditional GETs with a bounded per-host ETag cache

Every GET now remembers the response's ETag and body per URL and
representation (Accept header), sends If-None-Match on the next request
for the same URL, and serves the remembered body when GitHub answers 304.
A 304 costs no primary rate-limit budget, so polling unchanged PRs,
issues and check runs every sweep tick no longer eats the 5,000/hour
allowance. The cache lives with the per-host throttle state (so mock
servers in tests never share entries), is capped at 1024 entries evicted
oldest-first and 512 KiB per body, and drops an entry whenever a response
comes back without a validator. The secondary-rate-limit backoff still
refuses calls before any network or cache lookup, since a cached body is
only known current once the server has confirmed it, and the 401 token
refresh retries through the same conditional path. The test mock server
now records request headers so the conditional headers can be asserted.

* feat(worktree): open item PRs as drafts and mark them ready once the item is in review

push_and_open_pr now opens every item PR as a draft, so CI starts but
nobody is asked to review and nothing can merge until item_done has
recorded the item as in_review; item_done then flips it ready for review
(GraphQL markPullRequestReadyForReview, using the node id the create
already returned) and records metadata.pr.ready. Repositories that
refuse draft PRs (GitHub's 422) get an ordinary PR instead.

Nothing stalls on a draft: the CI-status paths (REST and batched
GraphQL, which now fetches isDraft) report a new PrCiStatus::Draft
before any check state, and the review sweep re-runs the ready flip for
an in-review item whose flip never landed. A draft the item already
records as flipped was converted back by a human and is held: not
re-flipped, not merged, and not self-repaired, since Draft is decided
before Failing/Passing. Discovery already skipped drafts and keeps doing
so. item status reports "draft"; the MCP pr_create action accepts
draft=true.

* fix(sessions): judge by last_seen when the host lacks a machine id

A bare hostname can be shared by two machines, so a matching host only
proves the pid is local when it carries the OS machine id.

* fix(workflow,jobs): keep cancellation authoritative over settlement

Queue::complete finishes a cancel-requested job as killed in the same
UPDATE, so an executor returning Ok can't turn it into exited.
Completed/Failed settlement and Waiting writes skip runs that are already
terminal, and publish their event only when they wrote. WorkflowCancelled
is published only by cancel_workflow_impl, once, when it changed the status.

* fix(github): header-driven GraphQL rate-limit backoff and sha-guarded branch delete

GraphQL RATE_LIMITED 200s now arm the host backoff from retry-after /
x-ratelimit-reset via Client::graphql, and mutations are spaced like other
writes. Merged head branches are deleted with updateRefs guarded by
beforeOid; there is no unguarded REST fallback.

* fix(messaging,claims): at-least-once SSE take, atomic identity adoption, fenced forced completion

- release_claim disarms the guard only on an explicit released: true.
- Provisional identity adoption reroutes mail and forgets the row in one
  IMMEDIATE transaction; a failure keeps the row for a retry.
- SSE take=true takes one message per granted send permit and requeues
  in-flight ids when the client goes away.
- Forced merged completion is fenced on the PR that was checked as merged
  (mark_completed_forced_if_pr).

* fix(messages): keep taken SSE ids requeueable until the stream closes

Being pulled off the channel doesn't prove the client received an event,
so the take stream no longer forgets in-flight ids after an idle poll.
message watch skips ids it already printed, so a reconnect after a
requeue doesn't print a message twice.

* fix(supervisor): only disarm auto-merge the sweep armed, and disarm it when the head moves

Review findings on PR 809. The sweep now records what it arms
(metadata.pr.auto_merge: the pinned head and the PR node id) and only
ever disarms an auto-merge it armed itself, so a maintainer's own
"Enable auto-merge" is left alone. A push from a write-access account
keeps GitHub's auto-merge armed, so every tick first re-judges a
recorded arming against the fresh snapshot, whatever the CI state:
a moved head disarms it before the new, unjudged commits can merge, and
an arming GitHub no longer reports is forgotten. Failing CI and a merge
conflict disarm it too, ahead of any repair push. Also decodes a cached
empty body served on 304 as Null, the same as the first time, and adds
the docstrings CodeRabbit's coverage check counted as missing.

* fix(supervisor): keep a failed disarm on record, and stamp judged heads for a merge-time gate

Follow-up on the head-moved review thread. A disarm GitHub refused used
to clear the sweep's arming record anyway, leaving nothing to retry;
now the record stays until GitHub confirms the disarm. And because
sweep-time reconciliation alone cannot stop GitHub merging a head that
was pushed and went green between two ticks, the sweep now posts a
per-commit status (context agentflare/judged) on the exact head it
armed auto-merge on. A repo that requires that context in branch
protection gets a merge-time gate: a later push is a new sha without
the status, so GitHub waits for the sweep to judge it. Repos that don't
require it see an informational status.

* fix(supervisor): stamp the judged head before choosing the merge path, retry a missing stamp

Follow-up on the judged-status review threads. The agentflare/judged
status was posted only right after a fresh arming, so an arming from an
earlier tick whose stamp failed, a review-blocked PR, or a direct merge
under a repo that requires the context could sit unstamped. The stamp
now runs first in merge_approved_pr, after a lookup of the head's
statuses: posted when missing, skipped when present, and left for the
next tick when the lookup itself fails. The context's docs now say it
is not for branches that merge through a merge queue, where required
statuses are evaluated on the queue's merge-group commit the sweep
never sees; there the merge_group CI run is the gate.

* fix(supervisor): arm GitHub auto-merge only behind a merge-time gate

Auto-merge armed on a branch with no merge-time gate lets GitHub merge a
head pushed and gone green between two sweep ticks that nobody judged for
findings. Arm it only when something stops that: the base branch merges
through a merge queue (the queue's own merge_group CI run is the gate,
and a direct merge is refused there anyway), or the base branch's
protection or rulesets require the agentflare/judged status the sweep
stamps on every judged head. Otherwise the sweep merges directly, pinned
to the judged head, as before; a review-blocked PR on such a branch waits
for the review and merges on the next tick.

repos::required_status_contexts reads branch protection and rulesets for
a branch once per process (an unprotected or unreadable branch reads as
"nothing required"). AutoMergeRef carries the PR's base branch and
merge-queue flag from the same fetch as the CI verdict (GraphQL
baseRefName/isMergeQueueEnabled, REST base.ref).

* fix(github): do not cache a failed required-status-contexts read

required_status_contexts cached its result even when the branch
protection or rulesets read failed with a transient error (5xx, rate
limit, 403). The branch then read as "nothing required" for the rest of
the process, so auto_merge_gate stopped arming auto-merge on a branch
that requires agentflare/judged until a restart. Cache only a complete
read (both reads succeeded, or rulesets returned NotFound), like
repos::settings; a failed read is retried on the next ask.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant