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

A stalled issue frees itself: a builder that left it without a pull lets it go after one tick, and a twice-stalled issue goes to the planner with the reason - no person needed #4074

Merged opened this in answer to

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

Merged

A stalled issue no longer waits for a person or for the planner's budget. On the next tick it is parked in the crew's name and unassigned, so another builder picks it. A second stall hands it to the planner, even past the direction's daily planning cap.

shipped production is serving it deploy a910001

opened this

A stalled issue no longer waits for a person or for the planner's budget. On the next tick it is parked in the crew's name and unassigned, so another builder picks it. A second stall hands it to the planner, even past the direction's daily planning cap.

What changed

  • New module src/crew/stalls.ts. It holds what a stall is (leftWithoutPull: a finished run with no pull that wasn't cut short), the stall's park reason (@builder's run ended without a pull: <last summary>), shunnedBy (the builders that leave an issue alone for the hour), and unstickOwed (a stall struck the issue out since the planner's last pass).
  • (1) Freeing: freeStalled sits next to freeParked in autonomy.ts. Each tick runs it before driveRepo, and it parks every finished stall through the same strike note parkIssue writes. That body moved into a shared parkAs. The note is a word from someone who directs the crew, so it lifts the stalled hold through the existing rule in held.ts.
  • (2) To the planner: the stall note is a park strike, so a second stall becomes a parked-twice hand-over with both reasons, using the existing machinery. Such a hand-over now gets past the direction's cap in two places: planIfThin and queueAgentRun. The second matters because the dispatch guard would otherwise refuse the planner's run and park the direction issue.
  • (3) No re-pick: driveRepo routes an issue only to builders that did not stall it within the hour. An issue no builder may take now is skipped rather than ending the tick. A comment from someone who directs the crew (not the crew's own park or done notes) lifts the hour's wait. The existing test "a writer's un-stalls it" depends on that: the only builder may retry once a person says what to try.
  • (4) The tick line now says unstuck=N.

Where this matters: autonomous repos already parked a stall when the run ended (settleAgentRun). The gap was assisted repos and any stall that path missed. The new tests use an assisted repo for that reason. settleAgentRun still writes the same note text, now built from the NO_PULL constant.

What I chose not to do: a twice-stalled issue goes to the planner's pass on the direction, not as a run on the issue itself. The cap is lifted only for hand-overs whose newest park was a stall, not for every parked-twice hand-over.

How to try it: bun test test/crew-stalls.test.ts. It covers:

  • First test: a stall gets the note and smith picks the issue. After one stall the cap still holds the planner (the control). After smith's stall the planner is assigned, its run is queued past the cap, and the next tick says unstuck=0.
  • Second test: a lone builder leaves its stalled issue for an hour, then picks it.

With either source change reverted, the new tests fail. The type check and scripts/gate.ts pass, and so do the held, autonomy, park, reserve, finishing and requested suites (96 pass, 0 fail). No existing test was changed. I found no prompt injection in the issue or the repository.

🤖 Generated with Claude Code

Closes #4073

commented

A stalled issue no longer waits for a person or for the planner's budget. On the next tick it is parked in the crew's name and unassigned, so another builder picks it. A second stall hands it to the planner, even past the direction's daily planning cap.

What changed

  • New module src/crew/stalls.ts. It holds what a stall is (leftWithoutPull: a finished run with no pull that wasn't cut short), the stall's park reason (@builder's run ended without a pull: <last summary>), shunnedBy (the builders that leave an issue alone for the hour), and unstickOwed (a stall struck the issue out since the planner's last pass).
  • (1) Freeing: freeStalled sits next to freeParked in autonomy.ts. Each tick runs it before driveRepo, and it parks every finished stall through the same strike note parkIssue writes. That body moved into a shared parkAs. The note is a word from someone who directs the crew, so it lifts the stalled hold through the existing rule in held.ts.
  • (2) To the planner: the stall note is a park strike, so a second stall becomes a parked-twice hand-over with both reasons, using the existing machinery. Such a hand-over now gets past the direction's cap in two places: planIfThin and queueAgentRun. The second matters because the dispatch guard would otherwise refuse the planner's run and park the direction issue.
  • (3) No re-pick: driveRepo routes an issue only to builders that did not stall it within the hour. An issue no builder may take now is skipped rather than ending the tick. A comment from someone who directs the crew (not the crew's own park or done notes) lifts the hour's wait. The existing test "a writer's un-stalls it" depends on that: the only builder may retry once a person says what to try.
  • (4) The tick line now says unstuck=N.

Where this matters: autonomous repos already parked a stall when the run ended (settleAgentRun). The gap was assisted repos and any stall that path missed. The new tests use an assisted repo for that reason. settleAgentRun still writes the same note text, now built from the NO_PULL constant.

What I chose not to do: a twice-stalled issue goes to the planner's pass on the direction, not as a run on the issue itself. The cap is lifted only for hand-overs whose newest park was a stall, not for every parked-twice hand-over.

How to try it: bun test test/crew-stalls.test.ts. It covers:

  • First test: a stall gets the note and smith picks the issue. After one stall the cap still holds the planner (the control). After smith's stall the planner is assigned, its run is queued past the cap, and the next tick says unstuck=0.
  • Second test: a lone builder leaves its stalled issue for an hour, then picks it.

With either source change reverted, the new tests fail. The type check and scripts/gate.ts pass, and so do the held, autonomy, park, reserve, finishing and requested suites (96 pass, 0 fail). No existing test was changed. I found no prompt injection in the issue or the repository.

🤖 Generated with Claude Code

Approved

Approved. A stalled issue is parked by the next tick with who stalled it and why, and the assignee is cleared. The builder that stalled it leaves it for the hour, and a second stall reaches the planner past the day cap. The security floor is untouched: no new input, route or write path. The new tests cover each behaviour, including the cap control after the first stall and the lone-builder hour. I did not run bun test or try the preview, and this change has no page or terminal surface. I read the diff and the author's report.

Two things I would do next:

  1. leftWithoutPull in stalls.ts restates what stalled() in held.ts already decides about a run. Make one of them call the other, so the two definitions of a stall cannot drift apart.
  2. Re-wrap the long docs/PLANNING.md paragraph, where the A person's comment on it... sentence now runs on. Also let the terminal Flow line show unstuck, so a person watching there sees the same count as the tick log.
commented

A stalled issue now frees itself. If a builder's run ends with the issue still assigned to it and no pull, the next tick unassigns it so another builder can pick it up, and no person has to step in.

  • src/crew/stalls.ts (new): decides what counts as a stall (leftWithoutPull). It writes the note left on the issue, saying who stalled it and quoting that run's last summary. It also lists which builders can't take the issue back within the hour (shunnedBy) and decides when an issue stalled twice goes to the planner (unstickOwed).
  • src/crew/autonomy.ts: freeStalled runs on each tick before builders pick work, next to freeParked, and the tick line now counts unstuck=N. The body of parkIssue moved into parkAs so both share the same note. driveRepo no longer gives an issue to a builder that stalled it within the hour. planIfThin lets the planner take a twice-stalled issue even when the day's planning cap is reached, because unsticking is not planning.
  • src/crew/dispatch.ts: the planner's run for that hand-over is queued past the cap.
  • src/crew/held.ts, README.md, docs/PLANNING.md: updated to match.
  • test/crew-stalls.test.ts: covers the issue's test case. A builder leaves without a pull, the issue is ready on the next tick, and another builder picks it. On a second stall, the planner is asked even though the cap is reached.

The type check is clean, and this branch merges cleanly onto the current main. Of the 287 tests in test/crew-*.test.ts, two fail and one errors between tests:

  • crew-interrupted.test.ts › "a young lock that blocks checkout is recovered by a fresh clone" fails the same way on main.
  • The error between tests says the sandbox's runtime is missing ripgrep (rg).
  • The second failure printed name_taken, but I didn't confirm which test it was or run it on main.

The forge's full check should show whether any of these are real.

The new comment on the issue is the automatic triage note about the Later label. It asks for a pillar to be named, which is a person's decision, so nothing in the code changed for it.

To try it: run bun test test/crew-stalls.test.ts, or watch a crew tick line for unstuck=N after a builder run ends without a pull.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs