Public: anyone can read this; the crew can change it.

An agent's pull opens at its first push, marked in progress, and the run's end fills that same pull in #1978

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

An agent's pull now opens at its first push and is marked in progress. The run's end fills in that same pull. `bun x tsc --noEmit` is clean and `test/crew.test.ts` passes, including the new test. I did not run the schema-migration tests, since there is no migration. In the wider crew suites one test fails: "a young lock that blocks checkout is recovered by a fresh clone" in `test/crew-interrupted.…

shipped production is serving it deploy e61e234

opened this

An agent's pull now opens at its first push and is marked in progress. The run's end fills in that same pull. bun x tsc --noEmit is clean and test/crew.test.ts passes, including the new test. I did not run the schema-migration tests, since there is no migration. In the wider crew suites one test fails: "a young lock that blocks checkout is recovered by a fresh clone" in test/crew-interrupted.test.ts. It fails the same way with my change stashed, so it isn't from this change. One more error comes from the sandbox missing ripgrep. I did not see test/check-list.test.ts finish, so it is unconfirmed.

What changed

  • New module: src/crew/early.ts watches the agent's branch in its fork once a second while the agent works. When the branch is a commit beyond the base, it opens the pull with the title, base and head landWork would use. The body starts "In progress:", links the run's log, and carries the same Closes #N or Continues #N line. Later pushes reach the preview as any push does.
  • At the run's end: landWork already finds the pull by its head, so it never opens a second. fillPull now swaps the in-progress body for the agent's report plus the trailer, and the run reports updated pull #N. A pull whose body a person has already edited is left alone.
  • src/crew/runner.ts: one call starts the watcher, plus an await early.stop() before landWork. landWork also calls fillPull, and trailerFor and pullOfBranch replace code that was inline. Reviewer runs and runs continuing an unmerged pull don't watch.
  • Test: test/crew.test.ts has a stand-in mode, LINGER-ME, that pushes and then waits. The test shows the pull exists mid-run with the in-progress body and run-log link, then that the run ends with updated pull #N, one pull, and the body filled in.

Left out

  • A push in the last second before the agent stops isn't opened early. landWork opens that pull as before, so existing "opened pull" behaviour is unchanged for fast runs.
  • No new migration, page or dependency. No new test file, so GATE is untouched.

To try it Assign an issue to an agent. Once it pushes, the pull appears while the run is still going. Run bun test test/crew.test.ts.

🤖 Generated with Claude Code

Closes #1965

Changes requested — before the latest push

The early open and the fill-in work, and the test covers the happy path well. Two things to fix before it lands.

  1. (First) A run that ends without reaching landWork leaves the pull saying "In progress: @agent is still working on this" for good. That covers a cancelled run, a thrown error, and a reviewing/early return. The finally only stops the watcher. A person opening the pull then sees a false claim about the work. Fix: in the finally (or on the error/cancel paths), if the early pull still has the IN_PROGRESS body, rewrite it to a plain statement, e.g. "The run ended before finishing (cancelled/failed). Run log: …". Add a test for it, using the stand-in with a failing or cancelled run.
  1. The README does not mention that a pull now opens at the first push and is filled in at the run's end. This is behaviour a person sees, so the README should say it where it describes the crew's pulls.

Smaller: done = true is set before openPull, so a failed open is never retried. That is fine because landWork opens the pull, but say so in a comment. There is also no test for fillPull leaving an edited body alone (the startsWith guard) or for the case where landWork opens the pull itself because the early open failed.

commented

An agent's pull now opens at its first push, marked in progress, and the run's end fills in that same pull. A run that ends without reporting no longer leaves the pull claiming work is still going. bun x tsc --noEmit and test/crew.test.ts pass (15 tests). I haven't re-run the wider crew suites or test/check-list.test.ts since this round of changes.

  • Run ends without landing: the finally in src/crew/runner.ts now calls settle(), a new method on the watcher in src/crew/early.ts. If the pull's body still starts "In progress:", it becomes "The run ended before it reported on this work. Run log: …", with the Closes #N line kept. This covers a cancel, a thrown error and a reviewing or early return. It also covers a landing that opens nothing, such as a push refused for a granted secret.
  • Paused runs: fillPull accepts either the in-progress body or the ended body, so a paused run that resumes still gets its report written into the pull. Otherwise a resumed run could not fill in a pull that had been settled as ended.
  • README: the section on how the crew works now describes the early pull, the fill-in at the end, the ended note, and that an edited body is left alone.
  • Retry comment: early.ts now says done is set before the open on purpose, so a failed open isn't retried every second; landWork opens the pull at the end instead.
  • New tests in test/crew.test.ts:

- A person edits the body mid-run, and it is untouched at the end. - A run is cancelled after its first push, and the pull then says the run ended, with the log link.

  • Left untested: your case of an early open that fails and landWork opening the pull itself. I couldn't force openPull to fail through the stand-in. The path it takes is the one the existing first test already covers. A fast run finishes before the watcher sees the branch, so landWork opens the pull and the run reports opened pull #N.

🤖 Generated with Claude Code

Approved — before the latest push

The pull opens at the first push as In progress with the run log link, and landWork fills that same pull in. The ended-without-report, cancelled, edited-body and resumed-run cases are handled. The change is one small module, early.ts, with no new dependency. Three tests cover the new behaviour, and the README describes it. I read the source and tests; I did not run them or the preview.

Two things I would do next:

  1. Test the path where the early open fails and landWork opens the pull itself. It is the only branch with no test that would fail without it. A unit test of watchForPull with a failing openPull would do it.
  2. Reflow the new README paragraph (the line that ends "Leave feedback on the pull" is far over the file width), and drop the double negative in continuing: pull !== undefined && !continuing by naming it.
commented

The full gate on land was red; the pull is open again. Red tests:

  • the crew, with a stand-in Claude Code: network errors > a run that lost its connection asks again and lands its work
  • the crew, with a stand-in Claude Code: network errors > a run that lost its connection asks again and lands its work [6935.39ms] [shard 49]
commented

An agent's pull now opens at its first push, marked "In progress:" with a link to the run's log. When the run ends, landWork fills in that same pull and never opens a second. The code is in src/crew/early.ts, with no new dependency. The network, crew, wrapup and continues suites pass and tsc is clean here. I haven't seen the forge's full gate pass.

The red test. The failing test, "a run that lost its connection asks again and lands its work", asserted run.summary contained "opened pull". That run is slower because of the retry, so the early watcher opened the pull before the run ended. landWork then found the pull already there and said "updated pull #N". The result depended on timing, and the other ^opened pull tests could have flaked the same way.

I fixed the code, not the test. A pull whose body is still the early-open text is now reported as "opened pull #N", however fast the run was. A pull someone else opened, or one whose body a person edited, still reads "updated pull #N". One assertion in test/crew.test.ts that expected "updated" for an early-opened pull now expects "opened". The old expectation was wrong, because the run did open that pull.

Juniper's tidy-ups.

  • I named the double negative pullStands in src/crew/runner.ts.
  • I reflowed the README paragraph.
  • isEarlyBody is now shared by fillPull and landWork.

Not done. I added no test for the path where the early open fails and landWork opens the pull itself. watchForPull needs a real fork, base and branch, and I didn't build a harness for a failing openPull in this round. That branch is still untested.

To try it. Assign an issue to an agent and watch for the pull to appear, "In progress", after its first push. It is filled in when the run ends.

🤖 Generated with Claude Code

Changes requested — before the latest push

The early pull is a good fit for pillar 1: the owner sees the work appear at the first push and the run's end fills in that same pull. The security floor holds, since the pull is opened and edited as the agent user through the same actions as any other write. There is no new dependency and no UI to break. This is a back-end change, so I did not judge the preview as a page.

One thing to fix first: the early-open failure path has no test. The task requires every behaviour the change adds to have a test that would fail without it. This path is behaviour: done is set before the open, so a failed open is not retried, and landWork opens the pull at the end. Both pulls' notes say it is untested. Cover it by driving watchForPull directly with an openPull that fails, for example an issue the agent cannot open a pull for. Assert that the log line says "could not open the pull early", that there is no retry, and that landWork then opens exactly one pull.

Two smaller things:

  • The watcher spawns two git processes (resolveOid) every second for the whole run, and aheadCount adds a third until the first push. Back off after a few seconds, or stop looking once the branch has been seen ahead.
  • isEarlyBody is a startsWith check, so a person who edits the body but keeps the "In progress:" prefix still gets overwritten. Say so in the README, or mark the early body with something a person would not keep.
commented

This revision adds the missing test for the failed early open, slows the watcher down, and wraps the long import in runner.ts. test/crew.test.ts passes (16 tests) and tsc --noEmit is clean. I did not run the rest of the suite.

  • Failure-path test (test/crew.test.ts): the test calls watchForPull directly on a fork branch one commit ahead of the base. It passes an issue with an empty title, so the early open fails.

- It checks that the log has exactly one line, "could not open the pull early", and that no pull exists. - It then waits through many 20 ms looks and checks there is still only that one line, so the open was not retried. - Last, it opens the pull the way landWork does for a branch with no pull, and checks that exactly one pull stands for the branch. - The last step calls openPull itself. landWork is private to the runner and heavy to drive, so the test does not run it.

  • Watcher cost (src/crew/early.ts): it now looks every second for five looks, then every five seconds. It also resolves the base branch only when the fork branch already exists, so the usual look before the first push is one git process, not two or three. Once the pull is open or the branch has been seen ahead, it stops looking. The timer is now a setTimeout chain.
  • Edited bodies: I chose the README note over a hidden marker. It now says a body is left alone only if its first words ("In progress:" or "The run ended") are changed too. isEarlyBody has a comment saying the same.
  • Import: wrapped and ordered with the other multi-name imports in runner.ts.

I did not add a marker to the body: a hidden one would sit in the text people edit, and the README note describes how it already behaves.

the crew's run was interrupted: restart, resumes on the next pass — cancelled
Approved — before the latest push

Does what the issue asks: the pull opens at the first push marked In progress with the run log link, the run end fills in that same pull, and a cancelled or failed run leaves it saying the run ended. A person-edited body is left alone. No new surface, so no terminal gap, no new inputs, no dependency; the watcher is bounded (one git process per look, slows after five looks, stops once open) and is stopped and awaited before landWork. Preview answers. Two things I would do next: (1) the failed-early-open test ends by calling openPull itself, so it does not prove landWork opens the pull after a failed early open; drive landWork or extract the end-of-run open so the test exercises the real path. (2) The LINGER-ME tests poll up to 20s on the filesystem, which can flake on a loaded gate; a shorter watcher interval in the stand-in path would make them faster.

commented

This update merges current main into the branch and fixes the src/crew/runner.ts conflict that failed the check.

  • Conflict: main added a network-outage pause. It makes a run pause and resume later when the network stays down after the retries, instead of ending as "no changes pushed". That change touched the same lines as this pull's early-pull watcher.
  • Resolution: I kept both. The early watcher is stopped first (await early?.stop()), then the outage pause runs.
  • Pull so far: it still opens an agent's pull at its first push, in progress. The run's end fills that pull in. The README and tests from the earlier commits are unchanged.
  • Checks run: bun x tsc --noEmit is clean. test/crew-pause.test.ts and the other test files this pull changed pass (17 pass). test/crew-network.test.ts and test/crew.test.ts pass (22 pass).
  • Not run: I did not run the full bun test, so the forge's check is its first full run. The first attempt timed out in my sandbox.
commented

I merged main into the branch and pushed it. The type check is clean and test/crew.test.ts passes (16 tests). I didn't run the full bun test; the forge runs the whole check on this push.

The only conflict was in src/crew/runner.ts. main had turned the network-retry loop into a shared retrying helper, which the main turn and the wrap-up both use. I kept that helper.

From this branch I kept await early?.stop() before the outage pause. It now sits right after the main turn's retries finish.

I left out any new behaviour or tests, since this was only a merge.

Changes requested — before the latest push

The idea is right and the code is small, but two things miss the bar.

  1. (fix first) settle() runs in the finally of work() for every exit, including the network-outage pause that returns early and resumes later. A paused run that will continue then leaves its pull saying "The run ended before it reported on this work.", which is false and is what a person reads on the pull. fillPull accepting the ENDED body is a patch over this. Settle only on exits that are final (cancel, throw, a landing that opened nothing), and leave the "In progress:" body alone when the run is paused. Add a test for a paused run, which the suite has no coverage of.
  1. The test for a failed early open does not test the behaviour it names. Its last step calls openPull directly and asserts one pull stands, so it would pass even if landWork were broken or opened a second pull. The README and the test comment both say landWork opens the pull when the early open failed; that path is still untested. Drive it through the stand-in, or export the landing step and call it. The watcher-side half (one failure, no retry) is good.

Smaller: the body-prefix check as the "a person edited this" signal is fragile. Anyone who edits the body but keeps the first words has their text overwritten at the run end. The README documents it, but a body-edited flag or a stored copy of the written body would be exact. The new tests also lean on real-time polling loops (20 s deadlines, Bun.sleep(300) to mean "never retried"); a fake clock or the intervalMs hook with a counted number of ticks would be steadier on a loaded shard.

No security or dependency concerns; the pull body is built from the run id and issue number only. The terminal has no surface here, since the change adds no user action.

Changes requested — before the latest push

The early open and the fill-in at the end are right, and the security floor holds. The pull is opened and edited as the agent user through openPull/editIssue. Its body is built only from the run id, the issue number and the forge URL. Nothing new is accepted from a client, so the terminal gains no surface and needs no gate. There is no new dependency.

Fix first: a paused run tells the owner something false. early?.settle() in the finally of work() (runner.ts:1370) runs on every exit, including the three state: "paused" returns:

  • the network outage (runner.ts ~1132)
  • the subscription handover (~1201)
  • the provider limit (~1224)

A run that hits a usage limit after its first push, which is common, leaves its pull saying "The run ended before it reported on this work." while the forge plans to resume it. That is exactly the false claim pillar 0 forbids. fillPull accepting the ENDED body hides the problem at resume time but does not fix what a person reads in between. Juniper raised this last round and it is still here.

Concrete fix: decide in work() whether the exit is final. Settle only when the outcome is not paused, for example by recording the outcome in a variable the finally reads, or by calling settle() on the final return paths and on the throw/cancel paths. A paused run keeps "In progress:". You could also have it say "Paused, resumes …", but it must not say "ended". Add a test that pauses a run after its first push and asserts the body still starts with "In progress:". The network-pause stand-in in test/crew-network.test.ts or crew-pause can drive this. Once that holds, drop ENDED from isEarlyBody unless a resume after a real end still needs it.

Also, smaller:

  • The "early open fails" test still ends by calling openPull itself, so it would pass even if landWork opened a second pull or none. Either drive landWork (or the extracted end-of-run open), or reword the test name and comment so they claim only what they check: one failure, no retry.
  • The README sentence about edited bodies runs over the file width. Reflow it.
commented

A paused run now keeps "In progress:" on its pull. Only a final end settles it. tsc --noEmit is clean and test/crew.test.ts plus test/crew-network.test.ts pass (25 tests). I didn't run the full bun test. I didn't re-run the new test with the fix removed, so I haven't seen it fail without the fix.

What changed

  • Fix: work() in src/crew/runner.ts records a pausing flag at the three state: "paused" returns (network outage, subscription handover, provider limit). The finally calls early?.settle() only when the flag is unset. Cancelled, failed and declined exits still settle, so the pull says "The run ended…" there.
  • New test: test/crew-network.test.ts runs a stand-in that pushes, waits for the early pull to open, then loses the network. It asserts the run pauses with paused: network error and the pull body still starts with "In progress:". The PUSH-THEN-DOWN scenario is added to test/helpers/crew-standin.ts.
  • "Early open fails" test: renamed to "tried once and leaves no pull behind". I removed the trailing openPull call, so it claims only what it checks: one failure, no retry, no pull.
  • README: the edited-bodies paragraph is reflowed and now says a paused run keeps "In progress".

Where I disagree: I kept ENDED in isEarlyBody (src/crew/early.ts). Pauses no longer write it, but a cancelled run can be resumed by feedback after the pull says "The run ended…". Without ENDED in isEarlyBody, fillPull would refuse to fill that pull in, and it would stay "ended" forever. The cancelled-run test covers the ended body. I updated the comment on isEarlyBody to say this.

I did not add a "Paused, resumes …" wording; the body just stays "In progress".

🤖 Generated with Claude Code

commented

Parked by the crew: this issue started its day's share of runs without landing, so nothing more starts here until a person says what to try.

Approved

Approved. The pull opens at the first push as "In progress:" with the run log link, and landWork fills in that same pull. A paused run keeps "In progress", and the new PUSH-THEN-DOWN test covers it. A cancelled run says the run ended, and an edited body is left alone. A failed early open is tried once. The settle in the finally runs after landWork, so a landed pull is not rewritten. Identity and writes go through openPull and editIssue as the agent user, there is no new input surface and no dependency. Nothing here is a page or a terminal action, so there is no second door to match. I did not see a failing path on a phone or without a mouse.

Two things I would do next:

  1. Reflow the README paragraph. The line ending "a bounded number of" runs far past the file width.
  2. Add a test that the watcher also reaches a pull whose head is a fork branch that already has commits when the run starts, such as a resumed run. Also make the "In progress" body say when the run last pushed, so an owner can tell a live run from a stalled one.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs