You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
F: publish a finished task's pull request in production (#103) - #110
TaskPublishing (runner/publishing.ts) publishes a task's pull request in production, with the real publisher from setUpRunner. The publisher uses GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher.
Pushing. The push goes from the runner-owned repository to pushUrl(github.repository) on GH_HOST, never to a configured url (#101 review, finding 6). ownedCommits is the ledger's origin: 'owned' SHAs. Before #22, only recordRebase writes foreign entries, and nothing in production calls it (#101 review, finding 4).
Mode, decided by task status. A running task whose plan items have all completed at the current revision gets a ready PR. A task in needs human, by any path, gets a draft PR. Its problems come from what sent it there: the spent budget, or the current attempt's safety finding or diagnostic.
When it publishes:
after a start/resume run ends;
when a refused start/resume moves the task to needs human (an expired budget);
on the new publish runner action (expectedStateVersion plus an action ID);
once at startup, if a publish is owed. The CLI starts it after it re-verifies the runner lock.
Last outcome. Schema v10 adds publish_outcomes. GET /api/runner shows it as publish: { available, active, publishable, draft, last }, and the same transaction updates the publish action's replay. The upgrade records tasks already running or in needs human as not published, so the first start opens nothing unasked.
Shutdown. Step 2a in runner-lifecycle.md: publishes are aborted in step 1 and awaited before the write gate closes.
Guards:
Start, resume and a second publish are refused while a publish runs.
Demos never publish.
github.baseBranch is required with a runner block, and is checked before the lock.
Decisions (user, 2026-10-03): github.baseBranch; publish decided by status after the run; a durable outcome (v10); one startup publish if owed.
Tests
test/runner-publishing.test.ts, 12 tests. They use a local bare repository as the remote and a fake GitHub. They cover:
a completed run opens one ready PR, with the branch at the task head;
a needs-human task gets a draft PR with its problems;
a refused resume that moves the task to needs human publishes a draft;
a refused resume on a task already in needs human publishes nothing;
BranchPushRefused leaves the task running, the publish action retries it, and the replay reports the outcome;
start, resume and a second publish are refused while a publish runs;
shutdown during the push, and during the opening, closes the Store only after the publish settled, and the next start publishes once or recovers the opening by its marker;
a PR whose reply arrives after the abort is recorded before the write gate closes;
a demo never publishes;
baseBranch validation.
cli.test.ts: a missing or full-ref baseBranch is refused before the lock and before the token check.
Schema tests: v9→v10, and the not published baseline.
Each guard was broken on purpose, and a test failed each time: the startup publish, the after-run publish, the publish after a refused resume, the status-change check, the busy guard, the replay update, and the shutdown wait (removed, or moved after the write gate).
Current local counts. Typecheck passes. Without agent-container and agent-supervisor, 1,402 tests ran: 1,394 passed and 1 was skipped. The other 7 timed out while a parallel Docker run loaded the machine; their six test files pass when rerun alone (144 tests). The two Docker files failed locally (10 tests). A comparison run on main was stopped at its 30-minute limit, so it is not yet known whether they fail without this change. The real-docker job decides.
Review rounds
Round
Source
Findings
Outcome
1
/code-review high + independent review agent
budget-expiry needs-human not published; stale safety finding in the problems; first upgraded start would publish old tasks; stale publishing replay; refs//HEAD base accepted; missing race tests; weak assertions ("restart publishes nothing", replay); no test that the publish wait comes before closeWrites; double progress read; unredacted gh text in outcomes; late GH_HOST check; misleading refusal text; orphaned doc comment
all fixed
2
/code-review high
a refused resume re-ran an owed publish; startup publish before the CLI's lock re-check; #problems read every attempt; mode computed twice
all fixed
3
/code-review high
docs lagged behind the fixes
fixed
4
/code-review high
none
—
5
/code-review high (fresh pass on the full diff)
a spent budget hid the current safety finding in a draft; the push used the setup's env while the gh gateways used process.env; recorded errors were redacted with process.env's tokens; no test that the CLI starts the owed publish
fixed in cfb38a9, each with a regression that fails without it
6
Copilot review on b6a7db7
a crash after the publish action committed its publishing reply, but before the publish recorded an outcome, left the replay saying publishing for ever
fixed in b2fe796, with no schema change. A saved reply that still says publishing counts as unsettled. Any later outcome of the task settles it, and startup settles it as stopped (interrupted) when nothing is owed. Regressions: two crash/restart tests, one for each half of the fix, and each fails without its half
7
Copilot review on b2fe796 ("previously missed")
an automatic publish (after a run, at startup) had no durable in-flight marker, so a crash after its PR opened left publish.last empty or stale for good
fixed in b35a2a0. Every publish records publishing before its first external call, and does not run if that write fails. Startup publishes again when one is owed, and otherwise settles a leftover record as stopped. Regressions: the marker is written before GitHub, a leftover is settled at startup, and no marker means no publish; each fails without its piece
8
Copilot review on b35a2a0
(high) a task whose earlier run owed a scope pause or a safety escalation (its write failed, or the process stopped) could be published as a ready PR showing unreviewed out-of-scope changes, since every item had run and nothing would pay it; (previously missed) the CLI checked baseBranch before the lock only when a github block existed
fixed in 1439ccf. Publishing refuses owed work, and a run's end, startup and a refused publish action have the executor pay it first (ItemExecutor.owes/payOwed). The CLI refuses a missing github block before the lock. Regressions: the unrecorded-pause state at startup and on the action, and the CLI with the lock held by another runner; each fails without its part
Cancel during a draft opening: it can leave a cancelled task with an open draft PR. The PR is recorded, and closing it is the existing "close the draft on cancel" slice.
Untracked push on a crash: the production push registers no process group, so a crash mid-push leaves the push untracked. A normal shutdown aborts and awaits it.
Outcome version stamp: the version is stamped at record time, not at the publish's last guarded write. No failing interleaving was found (low confidence).
CI test and real-docker are still running; real-docker is not a required check.
Review-lesson audit. Each finding falls under an existing AGENTS.md rule:
the idempotent replay ("keep the saved response current");
the shutdown ordering ("track an in-flight irreversible subprocess");
token redaction (the guarded-external-actions rules);
tests that must fail ("A test's setup must leave the state the production path would", and the Design system and plan format #1 E2E rule).
The Copilot round 8 findings fall under "Check an operation's source-state preconditions before any shortcut" and "Prefer validating dependencies before acquisition". The Copilot round 7 finding falls under "Record in-flight ownership before the first external write". The Copilot round 6 finding falls under the idempotency rules ("keep the saved response current with the durable outcome it reports"). The upgrade baseline and the resume trigger are one-off product decisions. No new AGENTS.md rule is needed.
TaskPublishing (runner/publishing.ts) publishes the task's PR with the real
publisher: GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher
that pushes from the runner-owned repository to the configured repository on
GH_HOST (never a configured URL), overwriting only ledger-owned commits.
- Mode by status: a running task whose plan items have all run gets a ready
PR; a task in needs human gets a draft PR with what sent it there.
- When: after a start/resume run ends; when a refused start/resume moves the
task to needs human; on the new `publish` runner action; once at startup,
after the CLI re-verifies the lock, if a publish is owed.
- Schema v10 `publish_outcomes`: the last outcome, shown as `publish` in
GET /api/runner and replayed by the publish action. Tasks already running
or in needs human at the upgrade are recorded as `not published`.
- Shutdown: publishes are aborted in step 1 and awaited before the Store
write gate closes. Start, resume and a second publish are refused while a
publish runs. Demos never publish.
- github.baseBranch is required with a runner block, checked before the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up issues for the deferred findings: #111 (close the draft on cancel), #112 (track the push's process group across a crash), #113 (publish only ledger-recorded commits; with #22), #114 (outcome version stamp, low confidence).
…env, CLI test
- A draft PR's problems name the current attempt's safety finding before a
spent budget, so a budget that ran out later cannot hide the violation.
- The already-fixed and PR gateways take the setup's env, the same as the
push, so GH_HOST and credentials agree; recorded errors are redacted with
that env's token values (RunnerAssembly.env, TaskPublishing).
- A CLI test shows the startup publish runs after the lock is verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…103)
The publish action's ID lived only in memory, so a crash after its
'publishing' reply committed and before the publish recorded an outcome
left the replay saying 'publishing' for ever. A reply that still says
'publishing' now counts as unsettled: any later outcome of the task
settles it (one publish runs per task), and startup settles it as
'stopped' (interrupted) when no publish is owed. Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An automatic after-run/startup publish has no durable in-flight marker. If opening succeeds and moves the task to in review, but this outcome write fails (or the process exits between those writes), this catch clears the in-memory run and startup later sees neither an owed publish nor an unsettled publish action. publish.last therefore remains null or stale permanently despite the recorded open PR. Persist ownership before every automatic publish and reconcile it at startup, as is done for action-triggered publishes.
…ver record at startup (#103)
An automatic publish (after a run, at startup) had no durable in-flight
marker: a crash after its PR opened and moved the task to in review, but
before its outcome was written, left publish.last empty or stale for good,
since startup then saw nothing owed and no unsettled action. Every publish
now records 'publishing' before its first external call (and does not run
if that write fails); startup publishes again when one is owed and
otherwise settles a leftover 'publishing' record as 'stopped'. Copilot
review on PR #110 (previously missed finding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validate baseBranch before locking when github configuration is absent
web/cli.ts:43
This conditional skips the promised pre-lock validation when the entire github block is absent. A runner with that malformed configuration acquires the runner lock first (and can report a lock conflict instead of the configuration error), contrary to the PR description and pull-request-opening.md:132. Validate baseBranch unconditionally for every non-demo runner.
…lock before the lock (#103)
- A task whose earlier run owes a safety escalation or a scope pause (its
write failed, or the process stopped after the item settled) is not
finished work, but every item has run, so no later run would pay it and a
ready PR would show unreviewed out-of-scope changes. Publishing refuses it
(mode), and a run's end, startup and a refused publish action have the
executor pay it first (ItemExecutor.owes/payOwed).
- The CLI refuses a runner with no github block before taking the lock, so
the missing block is reported as itself, not as a lock conflict.
Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A ready publish needs every item of the current plan revision completed
at that revision, checked in the Store's publish guard before every
guarded write: a revision that adds an item during a publish's first
GitHub lookups no longer gets a ready PR.
- Only a publish action refused because of owed work pays it; a refusal
for a stale view, a busy task or a replay starts nothing.
- A task recorded as 'not published' at the v10 upgrade stays unpublished
until a person asks, whatever changes since.
- A failure while paying owed work no longer stops startup from settling
an in-flight record or a stuck reply.
- A publish refused while a lost opening settles is retried at that
opening's deadline (settleRemaining).
- Startup settles a reply that is the only thing stuck with the outcome on
record, leaving that record (and its version stamp) as it is.
- The owed-work scan runs only for a status that could publish, not on
every poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A publish that ends with an opening still in flight (its own reply was
lost, not only a refusal while one settles) is retried at the opening's
deadline; a list-lag OpeningUnsettled is retried after 30 s.
- 'not published' (v10) holds only until a run starts after the upgrade.
- Only the publish action's own owed-work refusal pays that work; a stale
request or any replay applies nothing.
- A draft waits only for an owed safety finding, not an owed scope pause
(which a needs-human task cannot record).
- startup() leaves a publish of this process alone; the stopped record it
writes keeps the interrupted publish's draft flag; the retry decision
cannot throw out of run().
- owes() reads only owed findings in memory and completed out-of-scope
attempts (Store.scopeFindingHeads), not every attempt, on a poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bound the automatic retry (AGENTS.md: back off recurring external
polling): one timer per task; a lost opening is waited for only while its
deadline is ahead, so one still stuck after it (two PRs carry its marker,
the repository changed, GitHub keeps failing) no longer re-publishes
every second; the list-lag retry runs once per chain.
- A person's start or resume that moves an upgraded task (v10 'not
published') ends that hold, so a refused resume that sends it to needs
human gets its draft.
- Owed work is paid only where a run would (running, queued) or a draft
waits on it (needs human).
- The 'not published' tests space their timestamps, which are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Retry budget per chain (until a publish settles): at most one retry at a
lost opening's deadline and one short retry, so a GitHub outage that
fails every open no longer republishes every settle time for ever.
- An 'opened' outcome that left the PR or the task where the publish did
not mean them (GitHub showed another head, so the PR was drafted and the
task stayed running; a draft change failed) is recorded with
`reconcile`: still owed, and given the short retry.
- A refusal just as an opening's deadline passes gets the short retry.
- The short retry's delay is a test seam (RunnerAssembly.shortRetryMs);
tests now cover the list-lag retry, the outage bound and reconciling.
- Tests that proved less than they claimed: the owed-work failure test now
reaches the failure; the CLI baseBranch half holds the lock; the CLI
startup test stops only after the outcome is recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A run's end, a person's action or a restart starts a new retry chain with
its own budget (AGENTS.md: reset on a lifecycle change or user action);
a retry timer's own publish does not, so retries stay bounded.
- No short retry while a lost opening's deadline is still ahead (it would
only be refused again).
- `reconcile` only while the task can still be published; the action reply
carries it; the settleHead comment no longer overstates.
- The owed-work path of a refused publish action carries the person's
intent (personAsked), like start/resume.
- publishOwed runs the lock check itself, first: no order of the CLI's
lines can start a startup publish before the lock is verified.
- Tests: the one-short-retry bound, the new chain on a person's action, and
the lock check running first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A person's publish action drops a retry the old chain armed, so it cannot
fire later on the new chain's budget (at most two automatic publishes per
chain, not three).
- Tests for two round-5 fixes that no test covered: no reconcile for a task
that can no longer be published, and the action reply carrying reconcile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tests for the owed-safety-finding gate: a ready publish is refused while
a finding is owed, the action pays it and the draft follows; a
needs-human task's owed finding is paid at startup before its draft; a
replay of that refusal pays nothing.
- The draft of a task whose owed finding was paid names that finding: the
executor settles it in memory only, so TaskPublishing keeps the reason
payOwed returned (it was lost before).
- A run's end starting a new chain drops the old chain's armed retry, as a
person's action does (#newChain): one timer per task, tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paid safety finding names a draft only while the task stays in needs
human on the same current attempt; a later return has its own reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A publish refused by a guard (a race) gets the one short retry; a draft
names a finding whose save failed even after the budget ran out; a
needs-human task pays only an owed finding, not a scope pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
It combines irreversible GitHub operations, asynchronous recovery and shutdown behavior, and a database migration across multiple lifecycle boundaries.
#116)
* F: publish a finished task's pull request in production (#103)
TaskPublishing (runner/publishing.ts) publishes the task's PR with the real
publisher: GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher
that pushes from the runner-owned repository to the configured repository on
GH_HOST (never a configured URL), overwriting only ledger-owned commits.
- Mode by status: a running task whose plan items have all run gets a ready
PR; a task in needs human gets a draft PR with what sent it there.
- When: after a start/resume run ends; when a refused start/resume moves the
task to needs human; on the new `publish` runner action; once at startup,
after the CLI re-verifies the lock, if a publish is owed.
- Schema v10 `publish_outcomes`: the last outcome, shown as `publish` in
GET /api/runner and replayed by the publish action. Tasks already running
or in needs human at the upgrade are recorded as `not published`.
- Shutdown: publishes are aborted in step 1 and awaited before the Store
write gate closes. Start, resume and a second publish are refused while a
publish runs. Demos never publish.
- github.baseBranch is required with a runner block, checked before the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* D: show a push left running by a crash is harmless; document it (#112)
A crashed or killed codeboost can leave its git push running, and nothing
records its process group. The push is leased to the branch value its own
run read, so it lands only while the branch still holds that value:
- before a newer publish reads the branch: read as codeboost's commit;
- between that read and its push: the newer push is refused as stale, and
the next publish moves on;
- after the newer publish moved the branch: the leftover is refused.
pushArguments is now the one place that builds the push, used by the pusher
and by tests that replay a leftover push against a real bare remote in each
order. Dropping the lease fails the two lease tests. The docs also cover a
branch that returns to the read value (moved back by a person, or the same
commit pushed again). No process group is recorded (decision on #112).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: review fixes for publishing (#103): finding before budget, one gh env, CLI test
- A draft PR's problems name the current attempt's safety finding before a
spent budget, so a budget that ran out later cannot hide the violation.
- The already-fixed and PR gateways take the setup's env, the same as the
push, so GH_HOST and credentials agree; recorded errors are redacted with
that env's token values (RunnerAssembly.env, TaskPublishing).
- A CLI test shows the startup publish runs after the lock is verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* D: document and test a leftover push recreating a deleted branch (#112)
A run that read no branch pushes with an empty lease, which matches again
once a newer publish's branch has been deleted (by a person, or by GitHub
after the PR merged): the leftover recreates the branch at its own codeboost
head, with no PR, overwriting nothing. Copilot review on PR #116.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: settle a publish action whose process stopped before its outcome (#103)
The publish action's ID lived only in memory, so a crash after its
'publishing' reply committed and before the publish recorded an outcome
left the replay saying 'publishing' for ever. A reply that still says
'publishing' now counts as unsettled: any later outcome of the task
settles it (one publish runs per task), and startup settles it as
'stopped' (interrupted) when no publish is owed. Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: record every publish as in flight before it starts; settle a leftover record at startup (#103)
An automatic publish (after a run, at startup) had no durable in-flight
marker: a crash after its PR opened and moved the task to in review, but
before its outcome was written, left publish.last empty or stale for good,
since startup then saw nothing owed and no unsettled action. Every publish
now records 'publishing' before its first external call (and does not run
if that write fails); startup publishes again when one is owed and
otherwise settles a leftover 'publishing' record as 'stopped'. Copilot
review on PR #110 (previously missed finding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: pay what an earlier run owes before publishing; check the github block before the lock (#103)
- A task whose earlier run owes a safety escalation or a scope pause (its
write failed, or the process stopped after the item settled) is not
finished work, but every item has run, so no later run would pay it and a
ready PR would show unreviewed out-of-scope changes. Publishing refuses it
(mode), and a run's end, startup and a refused publish action have the
executor pay it first (ItemExecutor.owes/payOwed).
- The CLI refuses a runner with no github block before taking the lock, so
the missing block is reported as itself, not as a lock conflict.
Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from an independent crash-point review of publishing (#103)
- A ready publish needs every item of the current plan revision completed
at that revision, checked in the Store's publish guard before every
guarded write: a revision that adds an item during a publish's first
GitHub lookups no longer gets a ready PR.
- Only a publish action refused because of owed work pays it; a refusal
for a stale view, a busy task or a replay starts nothing.
- A task recorded as 'not published' at the v10 upgrade stays unpublished
until a person asks, whatever changes since.
- A failure while paying owed work no longer stops startup from settling
an in-flight record or a stuck reply.
- A publish refused while a lost opening settles is retried at that
opening's deadline (settleRemaining).
- Startup settles a reply that is the only thing stuck with the outcome on
record, leaving that record (and its version stamp) as it is.
- The owed-work scan runs only for a status that could publish, not on
every poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 2 of the independent review of publishing (#103)
- A publish that ends with an opening still in flight (its own reply was
lost, not only a refusal while one settles) is retried at the opening's
deadline; a list-lag OpeningUnsettled is retried after 30 s.
- 'not published' (v10) holds only until a run starts after the upgrade.
- Only the publish action's own owed-work refusal pays that work; a stale
request or any replay applies nothing.
- A draft waits only for an owed safety finding, not an owed scope pause
(which a needs-human task cannot record).
- startup() leaves a publish of this process alone; the stopped record it
writes keeps the interrupted publish's draft flag; the retry decision
cannot throw out of run().
- owes() reads only owed findings in memory and completed out-of-scope
attempts (Store.scopeFindingHeads), not every attempt, on a poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 3 of the independent review of publishing (#103)
- Bound the automatic retry (AGENTS.md: back off recurring external
polling): one timer per task; a lost opening is waited for only while its
deadline is ahead, so one still stuck after it (two PRs carry its marker,
the repository changed, GitHub keeps failing) no longer re-publishes
every second; the list-lag retry runs once per chain.
- A person's start or resume that moves an upgraded task (v10 'not
published') ends that hold, so a refused resume that sends it to needs
human gets its draft.
- Owed work is paid only where a run would (running, queued) or a draft
waits on it (needs human).
- The 'not published' tests space their timestamps, which are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 4 of the independent review of publishing (#103)
- Retry budget per chain (until a publish settles): at most one retry at a
lost opening's deadline and one short retry, so a GitHub outage that
fails every open no longer republishes every settle time for ever.
- An 'opened' outcome that left the PR or the task where the publish did
not mean them (GitHub showed another head, so the PR was drafted and the
task stayed running; a draft change failed) is recorded with
`reconcile`: still owed, and given the short retry.
- A refusal just as an opening's deadline passes gets the short retry.
- The short retry's delay is a test seam (RunnerAssembly.shortRetryMs);
tests now cover the list-lag retry, the outage bound and reconciling.
- Tests that proved less than they claimed: the owed-work failure test now
reaches the failure; the CLI baseBranch half holds the lock; the CLI
startup test stops only after the outcome is recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 5 of the independent review of publishing (#103)
- A run's end, a person's action or a restart starts a new retry chain with
its own budget (AGENTS.md: reset on a lifecycle change or user action);
a retry timer's own publish does not, so retries stay bounded.
- No short retry while a lost opening's deadline is still ahead (it would
only be refused again).
- `reconcile` only while the task can still be published; the action reply
carries it; the settleHead comment no longer overstates.
- The owed-work path of a refused publish action carries the person's
intent (personAsked), like start/resume.
- publishOwed runs the lock check itself, first: no order of the CLI's
lines can start a startup publish before the lock is verified.
- Tests: the one-short-retry bound, the new chain on a person's action, and
the lock check running first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 6 of the independent review of publishing (#103)
- A person's publish action drops a retry the old chain armed, so it cannot
fire later on the new chain's budget (at most two automatic publishes per
chain, not three).
- Tests for two round-5 fixes that no test covered: no reconcile for a task
that can no longer be published, and the action reply carrying reconcile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 7 of the independent review of publishing (#103)
- Tests for the owed-safety-finding gate: a ready publish is refused while
a finding is owed, the action pays it and the draft follows; a
needs-human task's owed finding is paid at startup before its draft; a
replay of that refusal pays nothing.
- The draft of a task whose owed finding was paid names that finding: the
executor settles it in memory only, so TaskPublishing keeps the reason
payOwed returned (it was lost before).
- A run's end starting a new chain drops the old chain's armed retry, as a
person's action does (#newChain): one timer per task, tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 8 of the independent review of publishing (#103)
A paid safety finding names a draft only while the task stays in needs
human on the same current attempt; a later return has its own reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 9 of the independent review of publishing (#103)
A publish refused by a guard (a race) gets the one short retry; a draft
names a finding whose save failed even after the budget ran out; a
needs-human task pays only an owed finding, not a scope pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ommits (#113) (#119)
* F: publish a finished task's pull request in production (#103)
TaskPublishing (runner/publishing.ts) publishes the task's PR with the real
publisher: GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher
that pushes from the runner-owned repository to the configured repository on
GH_HOST (never a configured URL), overwriting only ledger-owned commits.
- Mode by status: a running task whose plan items have all run gets a ready
PR; a task in needs human gets a draft PR with what sent it there.
- When: after a start/resume run ends; when a refused start/resume moves the
task to needs human; on the new `publish` runner action; once at startup,
after the CLI re-verifies the lock, if a publish is owed.
- Schema v10 `publish_outcomes`: the last outcome, shown as `publish` in
GET /api/runner and replayed by the publish action. Tasks already running
or in needs human at the upgrade are recorded as `not published`.
- Shutdown: publishes are aborted in step 1 and awaited before the Store
write gate closes. Start, resume and a second publish are refused while a
publish runs. Demos never publish.
- github.baseBranch is required with a runner block, checked before the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: test and document that a published head carries the branch's own commits (#113)
A task's head is the user's branch (base..HEAD, recorded by the review
before the runner's first commit) plus the runner's commits on top, so the
published PR carries commits the ledger has no entry for. That is the
design: they count as foreign, review shows their changes in the Unplanned
row, and the merge gate blocks until each is assigned or accepted.
A test publishes the demo task, whose last commit a person made, and checks
the branch holds it, review shows its change as Unplanned, and the merge
gate reports the unplanned blocker. The docs say what is published and that
the gate still inspects github.pullRequest, not the published PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: review fixes for publishing (#103): finding before budget, one gh env, CLI test
- A draft PR's problems name the current attempt's safety finding before a
spent budget, so a budget that ran out later cannot hide the violation.
- The already-fixed and PR gateways take the setup's env, the same as the
push, so GH_HOST and credentials agree; recorded errors are redacted with
that env's token values (RunnerAssembly.env, TaskPublishing).
- A CLI test shows the startup publish runs after the lock is verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: settle a publish action whose process stopped before its outcome (#103)
The publish action's ID lived only in memory, so a crash after its
'publishing' reply committed and before the publish recorded an outcome
left the replay saying 'publishing' for ever. A reply that still says
'publishing' now counts as unsettled: any later outcome of the task
settles it (one publish runs per task), and startup settles it as
'stopped' (interrupted) when no publish is owed. Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: record every publish as in flight before it starts; settle a leftover record at startup (#103)
An automatic publish (after a run, at startup) had no durable in-flight
marker: a crash after its PR opened and moved the task to in review, but
before its outcome was written, left publish.last empty or stale for good,
since startup then saw nothing owed and no unsettled action. Every publish
now records 'publishing' before its first external call (and does not run
if that write fails); startup publishes again when one is owed and
otherwise settles a leftover 'publishing' record as 'stopped'. Copilot
review on PR #110 (previously missed finding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: pay what an earlier run owes before publishing; check the github block before the lock (#103)
- A task whose earlier run owes a safety escalation or a scope pause (its
write failed, or the process stopped after the item settled) is not
finished work, but every item has run, so no later run would pay it and a
ready PR would show unreviewed out-of-scope changes. Publishing refuses it
(mode), and a run's end, startup and a refused publish action have the
executor pay it first (ItemExecutor.owes/payOwed).
- The CLI refuses a runner with no github block before taking the lock, so
the missing block is reported as itself, not as a lock conflict.
Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from an independent crash-point review of publishing (#103)
- A ready publish needs every item of the current plan revision completed
at that revision, checked in the Store's publish guard before every
guarded write: a revision that adds an item during a publish's first
GitHub lookups no longer gets a ready PR.
- Only a publish action refused because of owed work pays it; a refusal
for a stale view, a busy task or a replay starts nothing.
- A task recorded as 'not published' at the v10 upgrade stays unpublished
until a person asks, whatever changes since.
- A failure while paying owed work no longer stops startup from settling
an in-flight record or a stuck reply.
- A publish refused while a lost opening settles is retried at that
opening's deadline (settleRemaining).
- Startup settles a reply that is the only thing stuck with the outcome on
record, leaving that record (and its version stamp) as it is.
- The owed-work scan runs only for a status that could publish, not on
every poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 2 of the independent review of publishing (#103)
- A publish that ends with an opening still in flight (its own reply was
lost, not only a refusal while one settles) is retried at the opening's
deadline; a list-lag OpeningUnsettled is retried after 30 s.
- 'not published' (v10) holds only until a run starts after the upgrade.
- Only the publish action's own owed-work refusal pays that work; a stale
request or any replay applies nothing.
- A draft waits only for an owed safety finding, not an owed scope pause
(which a needs-human task cannot record).
- startup() leaves a publish of this process alone; the stopped record it
writes keeps the interrupted publish's draft flag; the retry decision
cannot throw out of run().
- owes() reads only owed findings in memory and completed out-of-scope
attempts (Store.scopeFindingHeads), not every attempt, on a poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 3 of the independent review of publishing (#103)
- Bound the automatic retry (AGENTS.md: back off recurring external
polling): one timer per task; a lost opening is waited for only while its
deadline is ahead, so one still stuck after it (two PRs carry its marker,
the repository changed, GitHub keeps failing) no longer re-publishes
every second; the list-lag retry runs once per chain.
- A person's start or resume that moves an upgraded task (v10 'not
published') ends that hold, so a refused resume that sends it to needs
human gets its draft.
- Owed work is paid only where a run would (running, queued) or a draft
waits on it (needs human).
- The 'not published' tests space their timestamps, which are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 4 of the independent review of publishing (#103)
- Retry budget per chain (until a publish settles): at most one retry at a
lost opening's deadline and one short retry, so a GitHub outage that
fails every open no longer republishes every settle time for ever.
- An 'opened' outcome that left the PR or the task where the publish did
not mean them (GitHub showed another head, so the PR was drafted and the
task stayed running; a draft change failed) is recorded with
`reconcile`: still owed, and given the short retry.
- A refusal just as an opening's deadline passes gets the short retry.
- The short retry's delay is a test seam (RunnerAssembly.shortRetryMs);
tests now cover the list-lag retry, the outage bound and reconciling.
- Tests that proved less than they claimed: the owed-work failure test now
reaches the failure; the CLI baseBranch half holds the lock; the CLI
startup test stops only after the outcome is recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 5 of the independent review of publishing (#103)
- A run's end, a person's action or a restart starts a new retry chain with
its own budget (AGENTS.md: reset on a lifecycle change or user action);
a retry timer's own publish does not, so retries stay bounded.
- No short retry while a lost opening's deadline is still ahead (it would
only be refused again).
- `reconcile` only while the task can still be published; the action reply
carries it; the settleHead comment no longer overstates.
- The owed-work path of a refused publish action carries the person's
intent (personAsked), like start/resume.
- publishOwed runs the lock check itself, first: no order of the CLI's
lines can start a startup publish before the lock is verified.
- Tests: the one-short-retry bound, the new chain on a person's action, and
the lock check running first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 6 of the independent review of publishing (#103)
- A person's publish action drops a retry the old chain armed, so it cannot
fire later on the new chain's budget (at most two automatic publishes per
chain, not three).
- Tests for two round-5 fixes that no test covered: no reconcile for a task
that can no longer be published, and the action reply carrying reconcile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 7 of the independent review of publishing (#103)
- Tests for the owed-safety-finding gate: a ready publish is refused while
a finding is owed, the action pays it and the draft follows; a
needs-human task's owed finding is paid at startup before its draft; a
replay of that refusal pays nothing.
- The draft of a task whose owed finding was paid names that finding: the
executor settles it in memory only, so TaskPublishing keeps the reason
payOwed returned (it was lost before).
- A run's end starting a new chain drops the old chain's armed retry, as a
person's action does (#newChain): one timer per task, tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 8 of the independent review of publishing (#103)
A paid safety finding names a draft only while the task stays in needs
human on the same current attempt; a later return has its own reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 9 of the independent review of publishing (#103)
A publish refused by a guard (a race) gets the one short retry; a draft
names a finding whose save failed even after the budget ran out; a
needs-human task pays only an owed finding, not a scope pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#114) (#123)
* F: publish a finished task's pull request in production (#103)
TaskPublishing (runner/publishing.ts) publishes the task's PR with the real
publisher: GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher
that pushes from the runner-owned repository to the configured repository on
GH_HOST (never a configured URL), overwriting only ledger-owned commits.
- Mode by status: a running task whose plan items have all run gets a ready
PR; a task in needs human gets a draft PR with what sent it there.
- When: after a start/resume run ends; when a refused start/resume moves the
task to needs human; on the new `publish` runner action; once at startup,
after the CLI re-verifies the lock, if a publish is owed.
- Schema v10 `publish_outcomes`: the last outcome, shown as `publish` in
GET /api/runner and replayed by the publish action. Tasks already running
or in needs human at the upgrade are recorded as `not published`.
- Shutdown: publishes are aborted in step 1 and awaited before the Store
write gate closes. Start, resume and a second publish are refused while a
publish runs. Demos never publish.
- github.baseBranch is required with a runner block, checked before the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: review fixes for publishing (#103): finding before budget, one gh env, CLI test
- A draft PR's problems name the current attempt's safety finding before a
spent budget, so a budget that ran out later cannot hide the violation.
- The already-fixed and PR gateways take the setup's env, the same as the
push, so GH_HOST and credentials agree; recorded errors are redacted with
that env's token values (RunnerAssembly.env, TaskPublishing).
- A CLI test shows the startup publish runs after the lock is verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: stamp a publish outcome with the state version the publish last saw (#114)
A publish outcome was stamped with the task's state version at recording
time. Where a GitHub call came after the publish's last Store write (a draft
change that failed and left the PR ready, or one refused because drafts are
unsupported), a change someone else made during that call counted as seen,
and TaskPublishing.owed() could suppress a publish owed for it.
Every PublishOutcome now carries a required seenVersion: the version read
where its path returns, right after its last Store write or guarded read,
or from before the last GitHub call when one came after that write. The
type checker makes every return path decide it. Store.recordPublish stamps
it and refuses a version newer than the current one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: settle a publish action whose process stopped before its outcome (#103)
The publish action's ID lived only in memory, so a crash after its
'publishing' reply committed and before the publish recorded an outcome
left the replay saying 'publishing' for ever. A reply that still says
'publishing' now counts as unsettled: any later outcome of the task
settles it (one publish runs per task), and startup settles it as
'stopped' (interrupted) when no publish is owed. Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: record every publish as in flight before it starts; settle a leftover record at startup (#103)
An automatic publish (after a run, at startup) had no durable in-flight
marker: a crash after its PR opened and moved the task to in review, but
before its outcome was written, left publish.last empty or stale for good,
since startup then saw nothing owed and no unsettled action. Every publish
now records 'publishing' before its first external call (and does not run
if that write fails); startup publishes again when one is owed and
otherwise settles a leftover 'publishing' record as 'stopped'. Copilot
review on PR #110 (previously missed finding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: pay what an earlier run owes before publishing; check the github block before the lock (#103)
- A task whose earlier run owes a safety escalation or a scope pause (its
write failed, or the process stopped after the item settled) is not
finished work, but every item has run, so no later run would pay it and a
ready PR would show unreviewed out-of-scope changes. Publishing refuses it
(mode), and a run's end, startup and a refused publish action have the
executor pay it first (ItemExecutor.owes/payOwed).
- The CLI refuses a runner with no github block before taking the lock, so
the missing block is reported as itself, not as a lock conflict.
Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from an independent crash-point review of publishing (#103)
- A ready publish needs every item of the current plan revision completed
at that revision, checked in the Store's publish guard before every
guarded write: a revision that adds an item during a publish's first
GitHub lookups no longer gets a ready PR.
- Only a publish action refused because of owed work pays it; a refusal
for a stale view, a busy task or a replay starts nothing.
- A task recorded as 'not published' at the v10 upgrade stays unpublished
until a person asks, whatever changes since.
- A failure while paying owed work no longer stops startup from settling
an in-flight record or a stuck reply.
- A publish refused while a lost opening settles is retried at that
opening's deadline (settleRemaining).
- Startup settles a reply that is the only thing stuck with the outcome on
record, leaving that record (and its version stamp) as it is.
- The owed-work scan runs only for a status that could publish, not on
every poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 2 of the independent review of publishing (#103)
- A publish that ends with an opening still in flight (its own reply was
lost, not only a refusal while one settles) is retried at the opening's
deadline; a list-lag OpeningUnsettled is retried after 30 s.
- 'not published' (v10) holds only until a run starts after the upgrade.
- Only the publish action's own owed-work refusal pays that work; a stale
request or any replay applies nothing.
- A draft waits only for an owed safety finding, not an owed scope pause
(which a needs-human task cannot record).
- startup() leaves a publish of this process alone; the stopped record it
writes keeps the interrupted publish's draft flag; the retry decision
cannot throw out of run().
- owes() reads only owed findings in memory and completed out-of-scope
attempts (Store.scopeFindingHeads), not every attempt, on a poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 3 of the independent review of publishing (#103)
- Bound the automatic retry (AGENTS.md: back off recurring external
polling): one timer per task; a lost opening is waited for only while its
deadline is ahead, so one still stuck after it (two PRs carry its marker,
the repository changed, GitHub keeps failing) no longer re-publishes
every second; the list-lag retry runs once per chain.
- A person's start or resume that moves an upgraded task (v10 'not
published') ends that hold, so a refused resume that sends it to needs
human gets its draft.
- Owed work is paid only where a run would (running, queued) or a draft
waits on it (needs human).
- The 'not published' tests space their timestamps, which are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 4 of the independent review of publishing (#103)
- Retry budget per chain (until a publish settles): at most one retry at a
lost opening's deadline and one short retry, so a GitHub outage that
fails every open no longer republishes every settle time for ever.
- An 'opened' outcome that left the PR or the task where the publish did
not mean them (GitHub showed another head, so the PR was drafted and the
task stayed running; a draft change failed) is recorded with
`reconcile`: still owed, and given the short retry.
- A refusal just as an opening's deadline passes gets the short retry.
- The short retry's delay is a test seam (RunnerAssembly.shortRetryMs);
tests now cover the list-lag retry, the outage bound and reconciling.
- Tests that proved less than they claimed: the owed-work failure test now
reaches the failure; the CLI baseBranch half holds the lock; the CLI
startup test stops only after the outcome is recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 5 of the independent review of publishing (#103)
- A run's end, a person's action or a restart starts a new retry chain with
its own budget (AGENTS.md: reset on a lifecycle change or user action);
a retry timer's own publish does not, so retries stay bounded.
- No short retry while a lost opening's deadline is still ahead (it would
only be refused again).
- `reconcile` only while the task can still be published; the action reply
carries it; the settleHead comment no longer overstates.
- The owed-work path of a refused publish action carries the person's
intent (personAsked), like start/resume.
- publishOwed runs the lock check itself, first: no order of the CLI's
lines can start a startup publish before the lock is verified.
- Tests: the one-short-retry bound, the new chain on a person's action, and
the lock check running first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 6 of the independent review of publishing (#103)
- A person's publish action drops a retry the old chain armed, so it cannot
fire later on the new chain's budget (at most two automatic publishes per
chain, not three).
- Tests for two round-5 fixes that no test covered: no reconcile for a task
that can no longer be published, and the action reply carrying reconcile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 7 of the independent review of publishing (#103)
- Tests for the owed-safety-finding gate: a ready publish is refused while
a finding is owed, the action pays it and the draft follows; a
needs-human task's owed finding is paid at startup before its draft; a
replay of that refusal pays nothing.
- The draft of a task whose owed finding was paid names that finding: the
executor settles it in memory only, so TaskPublishing keeps the reason
payOwed returned (it was lost before).
- A run's end starting a new chain drops the old chain's armed retry, as a
person's action does (#newChain): one timer per task, tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 8 of the independent review of publishing (#103)
A paid safety finding names a draft only while the task stays in needs
human on the same current attempt; a later return has its own reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 9 of the independent review of publishing (#103)
A publish refused by a guard (a race) gets the one short retry; a draft
names a finding whose save failed even after the budget ran out; a
needs-human task pays only an owed finding, not a scope pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* F: publish a finished task's pull request in production (#103)
TaskPublishing (runner/publishing.ts) publishes the task's PR with the real
publisher: GhAlreadyFixedGateway, GhPullRequestGateway and a GitBranchPusher
that pushes from the runner-owned repository to the configured repository on
GH_HOST (never a configured URL), overwriting only ledger-owned commits.
- Mode by status: a running task whose plan items have all run gets a ready
PR; a task in needs human gets a draft PR with what sent it there.
- When: after a start/resume run ends; when a refused start/resume moves the
task to needs human; on the new `publish` runner action; once at startup,
after the CLI re-verifies the lock, if a publish is owed.
- Schema v10 `publish_outcomes`: the last outcome, shown as `publish` in
GET /api/runner and replayed by the publish action. Tasks already running
or in needs human at the upgrade are recorded as `not published`.
- Shutdown: publishes are aborted in step 1 and awaited before the Store
write gate closes. Start, resume and a second publish are refused while a
publish runs. Demos never publish.
- github.baseBranch is required with a runner block, checked before the lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: close a cancelled task's pull requests (#111)
When a task is cancelled, codeboost closes every open PR it opened for the
task, draft or ready, and keeps the codeboost/ branch.
- The cancel is never refused and never waits for GitHub. A publish in
progress is aborted (PublishCancelled, recorded as stopped); the close
runs once it settles, after recovering a lost opening by its marker.
- PullRequestPublisher.closeAll closes opened records by number (GitHub's
list can lag), adopts and closes an abandoned opening's PR found by its
marker, attempts every PR even when one fails, and re-checks the closing
flags right before each close (beforeClose).
- GhPullRequestGateway.close reads the PR by number, closes an open one
only while it is still from the task branch with its marker, and requires
GitHub to show it closed.
- The outcome is recorded like a publish (action: close). An owed close
runs at startup, after the settle time when an opening was unsettled, and
on the new close-pull-requests runner action, whose replay reports it.
- Fix a vacuous bare-regex toMatchObject assertion in runner-shutdown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: report a cancelled task's PR recorded in another repository instead of recording the close as done (#111)
A record whose repository differs from the configured one (a rename, a
transfer, a configuration change) was skipped, and closeAll succeeded, so
the close was recorded as closed and never retried. It is now reported as
PullRequestMisplaced, the close stays owed, and the records in this
repository are still closed. Copilot review on PR #115.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: review fixes for publishing (#103): finding before budget, one gh env, CLI test
- A draft PR's problems name the current attempt's safety finding before a
spent budget, so a budget that ran out later cannot hide the violation.
- The already-fixed and PR gateways take the setup's env, the same as the
push, so GH_HOST and credentials agree; recorded errors are redacted with
that env's token values (RunnerAssembly.env, TaskPublishing).
- A CLI test shows the startup publish runs after the lock is verified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: retry a refused close at the opening's own deadline; test the abandoned-opening close (#111)
- A close refused while an opening settles was retried a full settle time
after the refusal; it now waits only until that opening's deadline (its
creation plus the settle time, on recovery's clock) plus a second
(PullRequestPublisher.settleRemaining).
- Tests: settleRemaining against a fixed clock; an aged opening's close is
retried at its deadline; an abandoned opening's PR is adopted and closed;
two PRs carrying one abandoned marker are both left open and refused.
Copilot review on PR #115.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: settle a publish action whose process stopped before its outcome (#103)
The publish action's ID lived only in memory, so a crash after its
'publishing' reply committed and before the publish recorded an outcome
left the replay saying 'publishing' for ever. A reply that still says
'publishing' now counts as unsettled: any later outcome of the task
settles it (one publish runs per task), and startup settles it as
'stopped' (interrupted) when no publish is owed. Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: record every publish as in flight before it starts; settle a leftover record at startup (#103)
An automatic publish (after a run, at startup) had no durable in-flight
marker: a crash after its PR opened and moved the task to in review, but
before its outcome was written, left publish.last empty or stale for good,
since startup then saw nothing owed and no unsettled action. Every publish
now records 'publishing' before its first external call (and does not run
if that write fails); startup publishes again when one is owed and
otherwise settles a leftover 'publishing' record as 'stopped'. Copilot
review on PR #110 (previously missed finding).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: owe a job an unsettled action asked for; test close order and cancel during an attempt (#111)
- A publish or close action whose process stopped before its job recorded
anything makes that job owed, whatever an earlier job settled (a close
asked for after a PR was reopened is run at startup, not reported as
stopped).
- Tests: a refused first PR does not stop the next one from closing; a
cancel while an attempt runs answers at once, closes nothing until the
attempt settles, then closes the PR; the reopened-PR crash/restart case.
Copilot review on PR #115.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: pay what an earlier run owes before publishing; check the github block before the lock (#103)
- A task whose earlier run owes a safety escalation or a scope pause (its
write failed, or the process stopped after the item settled) is not
finished work, but every item has run, so no later run would pay it and a
ready PR would show unreviewed out-of-scope changes. Publishing refuses it
(mode), and a run's end, startup and a refused publish action have the
executor pay it first (ItemExecutor.owes/payOwed).
- The CLI refuses a runner with no github block before taking the lock, so
the missing block is reported as itself, not as a lock conflict.
Copilot review on PR #110.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from an independent crash-point review of publishing (#103)
- A ready publish needs every item of the current plan revision completed
at that revision, checked in the Store's publish guard before every
guarded write: a revision that adds an item during a publish's first
GitHub lookups no longer gets a ready PR.
- Only a publish action refused because of owed work pays it; a refusal
for a stale view, a busy task or a replay starts nothing.
- A task recorded as 'not published' at the v10 upgrade stays unpublished
until a person asks, whatever changes since.
- A failure while paying owed work no longer stops startup from settling
an in-flight record or a stuck reply.
- A publish refused while a lost opening settles is retried at that
opening's deadline (settleRemaining).
- Startup settles a reply that is the only thing stuck with the outcome on
record, leaving that record (and its version stamp) as it is.
- The owed-work scan runs only for a status that could publish, not on
every poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 2 of the independent review of publishing (#103)
- A publish that ends with an opening still in flight (its own reply was
lost, not only a refusal while one settles) is retried at the opening's
deadline; a list-lag OpeningUnsettled is retried after 30 s.
- 'not published' (v10) holds only until a run starts after the upgrade.
- Only the publish action's own owed-work refusal pays that work; a stale
request or any replay applies nothing.
- A draft waits only for an owed safety finding, not an owed scope pause
(which a needs-human task cannot record).
- startup() leaves a publish of this process alone; the stopped record it
writes keeps the interrupted publish's draft flag; the retry decision
cannot throw out of run().
- owes() reads only owed findings in memory and completed out-of-scope
attempts (Store.scopeFindingHeads), not every attempt, on a poll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 3 of the independent review of publishing (#103)
- Bound the automatic retry (AGENTS.md: back off recurring external
polling): one timer per task; a lost opening is waited for only while its
deadline is ahead, so one still stuck after it (two PRs carry its marker,
the repository changed, GitHub keeps failing) no longer re-publishes
every second; the list-lag retry runs once per chain.
- A person's start or resume that moves an upgraded task (v10 'not
published') ends that hold, so a refused resume that sends it to needs
human gets its draft.
- Owed work is paid only where a run would (running, queued) or a draft
waits on it (needs human).
- The 'not published' tests space their timestamps, which are compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 4 of the independent review of publishing (#103)
- Retry budget per chain (until a publish settles): at most one retry at a
lost opening's deadline and one short retry, so a GitHub outage that
fails every open no longer republishes every settle time for ever.
- An 'opened' outcome that left the PR or the task where the publish did
not mean them (GitHub showed another head, so the PR was drafted and the
task stayed running; a draft change failed) is recorded with
`reconcile`: still owed, and given the short retry.
- A refusal just as an opening's deadline passes gets the short retry.
- The short retry's delay is a test seam (RunnerAssembly.shortRetryMs);
tests now cover the list-lag retry, the outage bound and reconciling.
- Tests that proved less than they claimed: the owed-work failure test now
reaches the failure; the CLI baseBranch half holds the lock; the CLI
startup test stops only after the outcome is recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 5 of the independent review of publishing (#103)
- A run's end, a person's action or a restart starts a new retry chain with
its own budget (AGENTS.md: reset on a lifecycle change or user action);
a retry timer's own publish does not, so retries stay bounded.
- No short retry while a lost opening's deadline is still ahead (it would
only be refused again).
- `reconcile` only while the task can still be published; the action reply
carries it; the settleHead comment no longer overstates.
- The owed-work path of a refused publish action carries the person's
intent (personAsked), like start/resume.
- publishOwed runs the lock check itself, first: no order of the CLI's
lines can start a startup publish before the lock is verified.
- Tests: the one-short-retry bound, the new chain on a person's action, and
the lock check running first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 6 of the independent review of publishing (#103)
- A person's publish action drops a retry the old chain armed, so it cannot
fire later on the new chain's budget (at most two automatic publishes per
chain, not three).
- Tests for two round-5 fixes that no test covered: no reconcile for a task
that can no longer be published, and the action reply carrying reconcile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 7 of the independent review of publishing (#103)
- Tests for the owed-safety-finding gate: a ready publish is refused while
a finding is owed, the action pays it and the draft follows; a
needs-human task's owed finding is paid at startup before its draft; a
replay of that refusal pays nothing.
- The draft of a task whose owed finding was paid names that finding: the
executor settles it in memory only, so TaskPublishing keeps the reason
payOwed returned (it was lost before).
- A run's end starting a new chain drops the old chain's armed retry, as a
person's action does (#newChain): one timer per task, tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 8 of the independent review of publishing (#103)
A paid safety finding names a draft only while the task stays in needs
human on the same current attempt; a later return has its own reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* F: fixes from round 9 of the independent review of publishing (#103)
A publish refused by a guard (a race) gets the one short retry; a draft
names a finding whose save failed even after the budget ran out; a
needs-human task pays only an owed finding, not a scope pause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #103.
What it does
TaskPublishing(runner/publishing.ts) publishes a task's pull request in production, with the real publisher fromsetUpRunner. The publisher usesGhAlreadyFixedGateway,GhPullRequestGatewayand aGitBranchPusher.Pushing. The push goes from the runner-owned repository to
pushUrl(github.repository)onGH_HOST, never to a configuredurl(#101 review, finding 6).ownedCommitsis the ledger'sorigin: 'owned'SHAs. Before #22, onlyrecordRebasewritesforeignentries, and nothing in production calls it (#101 review, finding 4).Mode, decided by task status. A
runningtask whose plan items have all completed at the current revision gets a ready PR. A task inneeds human, by any path, gets a draft PR. Its problems come from what sent it there: the spent budget, or the current attempt's safety finding or diagnostic.When it publishes:
start/resumerun ends;start/resumemoves the task to needs human (an expired budget);publishrunner action (expectedStateVersionplus an action ID);Last outcome. Schema v10 adds
publish_outcomes. GET /api/runner shows it aspublish: { available, active, publishable, draft, last }, and the same transaction updates the publish action's replay. The upgrade records tasks already running or in needs human asnot published, so the first start opens nothing unasked.Shutdown. Step 2a in runner-lifecycle.md: publishes are aborted in step 1 and awaited before the write gate closes.
Guards:
github.baseBranchis required with a runner block, and is checked before the lock.Decisions (user, 2026-10-03):
github.baseBranch; publish decided by status after the run; a durable outcome (v10); one startup publish if owed.Tests
test/runner-publishing.test.ts, 12 tests. They use a local bare repository as the remote and a fake GitHub. They cover:BranchPushRefusedleaves the task running, thepublishaction retries it, and the replay reports the outcome;baseBranchvalidation.cli.test.ts: a missing or full-refbaseBranchis refused before the lock and before the token check.not publishedbaseline.Current local counts. Typecheck passes. Without
agent-containerandagent-supervisor, 1,402 tests ran: 1,394 passed and 1 was skipped. The other 7 timed out while a parallel Docker run loaded the machine; their six test files pass when rerun alone (144 tests). The two Docker files failed locally (10 tests). A comparison run onmainwas stopped at its 30-minute limit, so it is not yet known whether they fail without this change. The real-docker job decides.Review rounds
/code-review high+ independent review agentpublishingreplay;refs//HEADbase accepted; missing race tests; weak assertions ("restart publishes nothing", replay); no test that the publish wait comes beforecloseWrites; double progress read; unredactedghtext in outcomes; lateGH_HOSTcheck; misleading refusal text; orphaned doc comment/code-review high#problemsread every attempt;modecomputed twice/code-review high/code-review high/code-review high(fresh pass on the full diff)ghgateways usedprocess.env; recorded errors were redacted withprocess.env's tokens; no test that the CLI starts the owed publishcfb38a9, each with a regression that fails without itb6a7db7publishaction committed itspublishingreply, but before the publish recorded an outcome, left the replay sayingpublishingfor everb2fe796, with no schema change. A saved reply that still sayspublishingcounts as unsettled. Any later outcome of the task settles it, and startup settles it asstopped(interrupted) when nothing is owed. Regressions: two crash/restart tests, one for each half of the fix, and each fails without its halfb2fe796("previously missed")publish.lastempty or stale for goodb35a2a0. Every publish recordspublishingbefore its first external call, and does not run if that write fails. Startup publishes again when one is owed, and otherwise settles a leftover record asstopped. Regressions: the marker is written before GitHub, a leftover is settled at startup, and no marker means no publish; each fails without its pieceb35a2a0baseBranchbefore the lock only when agithubblock existed1439ccf. Publishing refuses owed work, and a run's end, startup and a refusedpublishaction have the executor pay it first (ItemExecutor.owes/payOwed). The CLI refuses a missinggithubblock before the lock. Regressions: the unrecorded-pause state at startup and on the action, and the CLI with the lock held by another runner; each fails without its partDeclined or deferred. These are posted on #103.
snapshot.headcomes from the review snapshot and may include commits the ledger does not record. This belongs with Add pre-merge rebase and head-bound command checks #22.Validated head.
1439ccf:testandreal-dockerare still running;real-dockeris not a required check.Review-lesson audit. Each finding falls under an existing AGENTS.md rule:
The Copilot round 8 findings fall under "Check an operation's source-state preconditions before any shortcut" and "Prefer validating dependencies before acquisition". The Copilot round 7 finding falls under "Record in-flight ownership before the first external write". The Copilot round 6 finding falls under the idempotency rules ("keep the saved response current with the durable outcome it reports"). The upgrade baseline and the resume trigger are one-off product decisions. No new AGENTS.md rule is needed.
🤖 Generated with Claude Code