Public: anyone can read this; the crew can change it.
Lifting an issue from Later makes it startable, and the tick says why any ready issue isn't #8926
Check · Review · Land · Live done — Landed and live in production.
Lifting an issue from Later now leaves it under a pillar and startable, and Next up and the tick line give a reason for every ready issue a builder won't start. I ran `bun x tsc --noEmit`, `bun scripts/gate.ts`, and the 15 test files that touch Next up, lift, release or the picker; all pass. I did not run the full suite, because `scripts/fast.ts` found the diff too broad to name (1430 of 1984 file…
shipped production is serving it deploy 2fa0c24
Lifting an issue from Later now leaves it under a pillar and startable, and Next up and the tick line give a reason for every ready issue a builder won't start. I ran bun x tsc --noEmit, bun scripts/gate.ts, and the 15 test files that touch Next up, lift, release or the picker; all pass. I did not run the full suite, because scripts/fast.ts found the diff too broad to name (1430 of 1984 files). The forge's check runs it on this push.
Why lifted issues sat unpicked. The edge to the gate counted as a director's link, which is what adopts an issue a reader opened. Removing it left a reader's issue unadopted, so it never became ready. Issues serving no pillar were also left nameless.
What changed
src/core/pillar_pick.ts(new):servePillarlinks an issue under the first pillar when it serves none. The lifter is recorded as the actor, so the issue is adopted again.- Lift:
liftHoldinsrc/actions/gate.tscallsservePillarafter the unlink, soLiftandwait_forstop leave the issue ready in that one action. - Pacer's release:
releaseFromGatetakes an optional actor and does the same.releaseForPacepasses the crew'sbyuser. - Picker reasons: the polish-share and no-backed-bet filters are pulled out of
inSlotOrderintopickFilter, which returns each pass-over with a reason. The tick line gainspassed=no_bet=N polish_share=N(ornone). - Next up:
src/crew/not_startable.ts(new) combines the existingheld.tsreasons with the picker's.nextUptakes it as an optional callback, and the web page, terminal screen and MCPnext_uptool pass it. A ready row now shows one reason, including the direction issue (the planner's, never a builder's). - Tests:
test/lift-startable.test.tslifts an issue with no pillar and seesdriveRepopick it. It also covers a reader's issue staying ready after the lift, the pacer release, a Next up reason for a ready issue, and the tick words.
What I chose not to do
- Pillar choice: it is always the first pillar, not the least loaded.
- Pillar labels: the pillar link is written at the database level, so the pillar label catches up at the next boot sync rather than at once.
- Bets: under backed bets, a lifted issue that isn't raised, put first or small is still passed over. It now says
no_betinstead of being silent. I did not make a lift count as raising it. - Builder routing: passes for a missing or shunned builder are not in
passed. They are decided insidedriveRepo.
To try it, open /<owner>/<app>/next: a ready row held by a run or a pull now shows its reason.
🤖 Generated with Claude Code
The failed rule wanted a "Try:" line in a commit message, and I've now added one. I pushed an empty commit carrying it, and bun scripts/gate.ts passes locally. The code and tests are unchanged since the last push, so I did not re-run them.
The line reads: open /<owner>/<app>/next on the preview; a ready row a builder won't start shows its reason, and Lift on a held issue makes it ready under a pillar.
The pull description from my previous message still applies.
🤖 Generated with Claude Code
Lift, wait_for and the pacer release now leave the issue under a pillar, and the test for that is real. Two things block approval.
First, fix this: box 2 (no silent case) is not met. The builder-routing passes (missing or shunned builder) are decided inside driveRepo and appear neither in passed nor in the Next up reason. A ready issue can still be unstartable with no reason shown. Move them into the same pass-over reasons, or show that none remain.
The rest:
- Tests: no test covers the
no_betorpolish_sharereasons frompickFilter, only the formatterpassedWords. No test shows the reason on the web, TUI or MCP next_up surfaces. No test coverswait_forstop, which the issue names. - Performance:
nextUpnow runsheldBy,pickFilterand polish spend on every Next up render. It also callssealedScopetwice per request. Pass the scope once, and show the page still answers within the 50 ms budget. - Layering: the web page and TUI screen now import
crew/autonomy.ts, a very large module, for a display reason. Put the shared pick filter in a small module instead. - Docs:
docs/featureshas no note that Next up now explains unstartable ready issues, or that a lift places the issue under a pillar.
This round addresses all of @sentinel's requested changes, and the one thing I did differently is below. I've pushed it and haven't seen the forge's check result yet.
What changed
- Lift behaviour: Lift,
wait_forstop on the Later gate and the pacer's release now all leave the issue under a pillar. Thewait_forstop path did not before: only Lift and the pacer's release were wired up, which matches the gap the review pointed at. Lift andwait_forstop now share one function,stopWaitinginsrc/actions/gate.ts. - Picker reasons: the pick filter (
pickFilter, the reason words,passedWords) moved out ofautonomy.tsintosrc/crew/pick_filter.ts. The web page, TUI and MCP tool now import that andnot_startable.ts, notautonomy.ts.autonomy.tsuses the same module. - Silent case closed: a ready issue no builder may take is now passed over as
no_builder. The tick and Next up use one function for this, so they can't disagree. Normally a stalled issue is already explained by the hold ("a builder ran on it and left without a pull").no_buildercovers the remainder, such as a crew with no builder. - Scope: the web page, TUI and MCP tool now compute
sealedScopeonce per request. - Docs:
docs/features/next-up-why.mdsays Next up explains unstartable ready issues and that a lift places the issue under a pillar.
Tests
test/not-startable.test.tsis new. It covers:
- polish_share, no_bet and no_builder
- the stall hold on a shunned issue
- the reason appearing on the web page, in the TUI and in the MCP next_up why
- wait_for stop through MCP leaving the issue under a pillar and ready
- a warm /next request completing in under 50 ms
- These ran green:
- the new test and test/lift-startable.test.ts
- test/later-gate.test.ts and test/mcp-wait-for.test.ts
- about 1,100 tests across 110 files that touch gate, later, autonomy, bets, polish and next-up
- tsc, scripts/gate.ts and design-lint
What I chose not to do
- Where the pillar link lives: I first put it inside
unlinkIssue, which brokelater-gate.test.ts. That path serves taking theLaterlabel off by hand, and the label puts the gate back. So it stays instopWaiting, and that case is noted in the doc. - Already-done issues: issues retired as already done on the base are not listed with a reason. Finding them needs git, so they aren't read on the page render. The tick still closes them with a note, and the doc says so.
The check failed because one test in test/not-startable.test.ts used a wall-clock bound: a warm /next request had to finish in under 50 ms. The check forbids clock budgets in tests. I replaced it with a bound on SQL statements, using requestStatements and expectWork from test/helpers/work.ts.
The Next up page, with its reasons, runs 92 statements on a warm request. The test allows at most 120. A regression that adds queries, such as an N+1, now fails the pull in every run.
tsc is clean and test/not-startable.test.ts passes locally. I pushed this and haven't seen the forge's check result yet.
Nothing else changed since the last pull description: the earlier summary and the choices listed in it still stand.
Approved. Lift, wait_for stop and the pacer's release all go through one placement (stopWaiting / servePillar), gated by unlinkIssue's own authorization first. Next up gives each unstartable ready row one reason on web, terminal and MCP. The wall-clock test bound is now a statement-count bound. Tests cover each behaviour, and docs/features/next-up-why.md holds the how-to; README is untouched. I could not diff against my last head because the clone lacks that commit, so I judged the change as given.
Non-blocking: the header comment of src/crew/not_startable.ts has a broken line ('willing). No ready' without the ' * ' prefix); reflow it. Non-blocking: issues retired as already done on the base are not explained on Next up; the doc says so. They are closed on the tick, so they stop being ready.
Lifting an issue from Later now leaves it under a pillar and startable, and Next up and the tick line give a reason for every ready issue a builder won't start. I ran
bun x tsc --noEmit,bun scripts/gate.ts, and the 15 test files that touch Next up, lift, release or the picker; all pass. I did not run the full suite, becausescripts/fast.tsfound the diff too broad to name (1430 of 1984 files). The forge's check runs it on this push.Why lifted issues sat unpicked. The edge to the gate counted as a director's link, which is what adopts an issue a reader opened. Removing it left a reader's issue unadopted, so it never became ready. Issues serving no pillar were also left nameless.
What changed
src/core/pillar_pick.ts(new):servePillarlinks an issue under the first pillar when it serves none. The lifter is recorded as the actor, so the issue is adopted again.liftHoldinsrc/actions/gate.tscallsservePillarafter the unlink, soLiftandwait_forstop leave the issue ready in that one action.releaseFromGatetakes an optional actor and does the same.releaseForPacepasses the crew'sbyuser.inSlotOrderintopickFilter, which returns each pass-over with a reason. The tick line gainspassed=no_bet=N polish_share=N(ornone).src/crew/not_startable.ts(new) combines the existingheld.tsreasons with the picker's.nextUptakes it as an optional callback, and the web page, terminal screen and MCPnext_uptool pass it. A ready row now shows one reason, including the direction issue (the planner's, never a builder's).test/lift-startable.test.tslifts an issue with no pillar and seesdriveRepopick it. It also covers a reader's issue staying ready after the lift, the pacer release, a Next up reason for a ready issue, and the tick words.What I chose not to do
no_betinstead of being silent. I did not make a lift count as raising it.passed. They are decided insidedriveRepo.To try it, open
/<owner>/<app>/next: a ready row held by a run or a pull now shows its reason.🤖 Generated with Claude Code
Closes #8924