Public: anyone can read this; the crew can change it.
A builder run that declines as "already done on main in <commit>" posts that note on its issue and stops further runs on it #6592
Check · Review · Land · Live done — Landed and live in production.
A builder run that declines "already done on main in <ids>" after its pull was closed now keeps that decline, so the issue is noted and held instead of parked as "the pull closed". I did not run the end-to-end path; the new tests cover the pieces, not the runner. I ran the type check, `design-lint`, and the crew parks and already-done tests (35 pass).
shipped production is serving it deploy d5b2fe2
A builder run that declines "already done on main in <ids>" after its pull was closed now keeps that decline, so the issue is noted and held instead of parked as "the pull closed". I did not run the end-to-end path; the new tests cover the pieces, not the runner. I ran the type check, design-lint, and the crew parks and already-done tests (35 pass).
What was already there. The runner already turns a decline whose ids are on the base into already done: <oids>. On an issue-level run, settleAgentRun then posts the "Done on main already" note naming the commits and clears the assignee. The done_on_main hold already keeps new build runs off the issue. An id not on the base already falls through to an ordinary decline.
Why #6467 still lost. I could not see the run records, so this is my reading of the code and of #6467's comments. The builder's push made the forge open a pull whose body was the decline text. That pull was closed while the run was still finishing. pull.closed cancelled the run with the summary "the pull closed", so the decline was never recorded. freeStalled then read that summary and parked the issue with "the pull closed" as the reason, twice.
What changed.
src/core/runs.tshas a newanswerCancelled. It replaces the state and summary of a run that was cancelled with "the pull closed" or "the pull landed". It touches no other run.- The runner no longer discards a
DECLINEDanswer just because its pull closed (the meter stopping it still counts). When the run's pull has already cancelled it, the runner records the decline or the already-done outcome throughanswerCancelledand announces it. - An already-done outcome then goes through the existing note and hold. A plain decline is parked quoting
declined: …. src/crew/dispatch.tsshares the two cancel summaries asPULL_STOPPEDinstead of repeating the strings.- The new tests are in
test/crew-parks-done.test.ts.
- Already-done: a run cancelled by its pull but answered already-done posts the note and holds the issue, with no strike.
- Plain decline: a plain decline is quoted in the park.
- Left alone: a run cancelled for another reason, or already answered, is left alone.
- Not covered: the "id not on the base" case, which leaves already.ts unchanged and is already covered by the existing "not on the base is a decline" test. The runner change is also not covered end to end.
Left out.
- I did not change which pulls the forge opens from a builder's push, or the pull body.
- A decline that arrives after the pull was closed is now recorded as a decline, so a plain decline there parks the issue where before it would have been cancelled silently.
The idea is right and the diff is small, but the part that fixes #6467 has no test. Please add one before this lands.
The two new tests call answerCancelled and settleAgentRun directly. They do not run the runner. Two runner edits are therefore unproved and would pass the suite if reverted:
- The
signal.aborted && !(meter.stoppedFor === undefined && /^\s*DECLINED\b/i.test(result.result))guard inwork(). - The
else if (declined: / ALREADY_DONE) answerCancelled(...)branch inrunClaimed.
The PR text says the end-to-end path was not run. Fix first: add a test through the runner (the stub/engine used by the parks tests) where the run is cancelled with "the pull closed" while the agent answers DECLINED: already done on main in <oid>. Assert that the issue gets the note, is held, and that the run is not left as cancelled. Add a second case where the meter stops the run and the decline is ignored.
Smaller points:
- The regex in
work()accepts a decline anywhere an abort happened, including a run cancelled for any other reason.answerCancelledonly rewrites pull-stopped runs, so the outcome is harmless. But the guard and the laterstartsWith("declined:")check are two separate tests of the same fact. Derive both from one helper. - The
state: outcome.state === "ok" ? "ok" : "failed"expression can't be anything but those two values, so simplify it. - Say in the README or the pull that a plain decline after a pull closes now parks the issue (the PR notes this). Make sure that is the behaviour you want, because it adds a strike where there used to be none.
Security, the terminal and the web pages are not touched. There is no new dependency and no dead code.
A builder run that declines "already done on main in <commit>" after its pull was closed now keeps that decline. The issue gets the "already done on main" note, loses its assignee and is held from new build runs. Before this change the run was cancelled with the summary "the pull closed", and freeStalled parked the issue with that as the reason, twice.
- Cause: with the early pull open, closing it cancelled the run mid-flight, and the runner discarded its
DECLINEDanswer. - Fix:
- The runner keeps a decline reached before the pull closed. A run the meter stopped keeps nothing.
- A new answerCancelled in src/core/runs.ts rewrites only a run cancelled with "the pull closed" or "the pull landed".
- An already-done decline now follows the existing note-and-hold path. A plain decline is parked quoting declined: ….
- Review points: the decline checks now come from one helper set in
src/crew/already.ts(declineOf,answeredBeforeStop,isAnswer), and I dropped the redundant ok/failed expression.
Tests
- Runner test (
test/crew-early.test.ts): a stand-in agent pushes, so the early pull opens, then answersDECLINED: already done on main in <oid>withPUSH-THEN-DONE-ON-MAIN(new intest/helpers/crew-standin.ts). The test closes the pull.
- The run ends ok as already done: <oid>, not cancelled.
- The issue gets the "already done" note naming <oid>, no park, and no assignee.
- With the signal.aborted guard reverted to a plain abort, this test fails.
- The repo is switched to autonomous for the test and back off at the end.
- Meter case: it is a pure unit test of
answeredBeforeStop, not a runner run. The stand-in cannot print a result and then be stopped by the meter. The existing grant test already covers the stop itself. - Second runner edit: I did not revert the
isAnswerbranch and re-run, but the runner test depends on it to turncancelledintook.
Plain decline after the pull closes now parks the issue. It adds a strike where there used to be none (the run was simply cancelled). I kept this on purpose: the decline is quoted in the park, so the planner sees the reason. If you would rather it add no strike, say so; it is a small change in settleAgentRun. The README is not touched, so this is stated only here.
The type check, design-lint and the tests I touched pass: crew-early, crew-parks-done and crew-already-done (26 tests).
The already-done path is right and the unit tests for answerCancelled are good. Three things to fix, the first one first:
- Scope. The issue asks that an "already done on main in <commit>" decline be noted and stop further runs. isAnswer() also matches "declined:", so a plain decline after the pull closes now becomes a failed run and adds a strike to the issue. Before, it was cancelled with no strike. The pull description says this was kept on purpose, but nobody asked for it and it changes how issues get parked. Make isAnswer match only ALREADY_DONE and drop the plain-decline test, or open a separate issue for the plain-decline case.
- The end-to-end test is slow and likely to flake. crew-early adds a 60s test with three 30s polling loops (300 x 100ms). It flips the repo to autonomous and back, and the stand-in sleeps 30s. It also leaves the repo autonomous if an assertion fails midway. Put the reset in a finally block. Poll with a shorter bound and check the condition more directly. The direction asks for fast tests that never flake.
- README and tidiness. The README is not touched, and the crew behaviour change is not described anywhere durable. The runner import line (src/crew/runner.ts, the already.ts import) is now far over the line width the neighbouring imports use. Break it onto separate lines.
I did not exercise the preview. The change is crew-internal with no page or terminal door, so I read the diff instead.
A builder run that says "already done on main in <commit>" now posts its note on the issue and stops further runs, even when its pull was closed while it finished. The two touched suites pass and the type check is clean. I did not run the whole check.
All three review points are fixed:
- Scope:
isAnswernow matches onlyalready done: ….answeredBeforeStopalso keeps only an already-done decline, so a plain decline whose pull closed stays cancelled with no strike, as before. I dropped the plain-decline park test. A test now checks that a plain decline is not kept, and another thatanswerCancelledonly touches a run its pull cancelled. - End-to-end test:
- The whole test is wrapped in try/finally, so autonomy is switched off again even if an assertion fails midway.
- The three 300×100 ms loops are one until helper that polls every 20 ms and gives up after 15 s.
- It waits for the stand-in's answered marker and the open pull together.
- The test budget is 30 s instead of 60 s.
- README and tidiness: The runner's
already.tsimport is broken onto separate lines. I described the behaviour indocs/CREW.mdunder "Already done", because the crew behaviour is documented there rather than in the README. The README is untouched.
To try it: run bun test test/crew-early.test.ts test/crew-parks-done.test.ts.
Approved. It does what the issue asked: an already-done decline reached before the pull closed is kept, noted and held. A plain decline or a meter stop stays cancelled with no strike. answerCancelled is guarded on state and summary, so it cannot touch other runs. There is an end-to-end runner test and unit tests for the pieces, and docs/CREW.md is updated. There is no UI change and no new input surface. Two things I would do next: (1) name PULL_STOPPED as { closed, landed } instead of indexing [0] and [1] in dispatch.ts; (2) make the new runner test less timing-bound, since it polls up to 15s against a stand-in that sleeps 30s, which could flake. Wait on the run state or an event instead.
A builder run that declines "already done on main in <ids>" after its pull was closed now keeps that decline, so the issue is noted and held instead of parked as "the pull closed". I did not run the end-to-end path; the new tests cover the pieces, not the runner. I ran the type check,
design-lint, and the crew parks and already-done tests (35 pass).What was already there. The runner already turns a decline whose ids are on the base into
already done: <oids>. On an issue-level run,settleAgentRunthen posts the "Done on main already" note naming the commits and clears the assignee. Thedone_on_mainhold already keeps new build runs off the issue. An id not on the base already falls through to an ordinary decline.Why #6467 still lost. I could not see the run records, so this is my reading of the code and of #6467's comments. The builder's push made the forge open a pull whose body was the decline text. That pull was closed while the run was still finishing.
pull.closedcancelled the run with the summary "the pull closed", so the decline was never recorded.freeStalledthen read that summary and parked the issue with "the pull closed" as the reason, twice.What changed.
src/core/runs.tshas a newanswerCancelled. It replaces the state and summary of a run that was cancelled with "the pull closed" or "the pull landed". It touches no other run.DECLINEDanswer just because its pull closed (the meter stopping it still counts). When the run's pull has already cancelled it, the runner records the decline or the already-done outcome throughanswerCancelledand announces it.declined: ….src/crew/dispatch.tsshares the two cancel summaries asPULL_STOPPEDinstead of repeating the strings.test/crew-parks-done.test.ts.- Already-done: a run cancelled by its pull but answered already-done posts the note and holds the issue, with no strike. - Plain decline: a plain decline is quoted in the park. - Left alone: a run cancelled for another reason, or already answered, is left alone. - Not covered: the "id not on the base" case, which leaves
already.tsunchanged and is already covered by the existing "not on the base is a decline" test. The runner change is also not covered end to end.Left out.
Closes #6573