Run now on the planning routine spends the concurrency, not one agent (#1204) - #1640
Conversation
…#1204) Two routines can fan out — a drain takes one entry off the queue, a plan agent writes one ticket's own sibling files — and until now only one of them did so from the card. The drain's Run now fires the sweep; [Plan tickets] fell through to a plain single start, so on the one rotation job where the concurrency setting is safe to spend, the click was the thing that ignored it. The daemon running the same routine unattended started up to `Concurrent agents`. The sweep's narrowing stops being drain-shaped. `drainOnly: true` becomes `only: 'drain' | 'plan'`, and a plan-only tick reaches the same claim-then-start path the rotation takes: candidates enumerated, ticket locks pushed before any agent starts, one agent per locked ticket. The locks are the point — without them two clicks a second apart plan the same ticket twice, which is the exact double-work this issue is about. Three things a narrowed tick must not do, each its own test: - work the queue instead. The queue-picked mode would send a plan click to the drain whenever entries are waiting; the click named a routine, so it plans. - override a switched-off routine. An unticked box stands the click down, and says so, rather than firing a routine the card shows as off. - cost the rotation its turn. A click borrows the tick for a routine it named, so the cycle stays where it was — the rule a due maintenance sweep follows. `projectId` scopes a sweep to one project. The drain sends none and visits them all, which is what its tooltip promises; planning is the picked project's own work and carries one. Both surfaces that quote a number now read `fansOut` rather than only `drains`: the tooltip says "Starts up to 2 agents in gemstack, one per open ticket" instead of "Starts one agent", and the menu item's "one agent, not the fan-out" warning — which was true of the drain and became true here the moment the button stopped being one start — now shows on both. Every new test was checked by breaking the code it guards. The first version of the plans-instead-of-draining test passed with the fix reverted: it asserted the job's name, which stays `plan` either way, when the difference is the batch — entries off the queue instead of tickets. It asserts the prompts now.
|
🤖 automated Dogfooded on a real daemon and a real repo — the fan-out is no longer a claim resting on unit tests. Rebuilt, restarted the daemon on this branch, and drove the dashboard through the browser. Three throwaway tickets in One click, three agentsThe card said the same thing: "started 3 agents: planning …; planning …; planning …". Each ticket was claimed before its agent started — three All three plans landed, and they are real work, not stubs — each read the repo (all three independently noted
Before this branch the same click started one agent and the other two tickets sat untouched. The tooltip tracks both the setting and the project
Read live off Project scoping, proven without spending anythingFour projects are registered. With the routine switched off — so no agent could start either way — two clicks produced exactly two log lines: One line per click, naming the project the picker showed. Unscoped, that would have been four lines each. A fix I had not claimedThat stand-down is also a behaviour change worth naming: the old Run now called Unrelated, confirmed in passingThe same daemon printed |
|
🤖 automated The whole chain, run end to end on a throwaway repo — three tickets in, two pull requests with real code out. Follow-up to the fan-out dogfood above. This one answers "where does planning fit, and who implements the plans?" by running every step for real on The chainPlanning never touches code. Implementation is the drain, and it works from the queue, not from plans directly — a triage routine is what promotes a ticket onto the queue, and the entry links back to its ticket so the agent gets the ticket and its plan. The two ends fan out; the middle cannot, because it rewrites the whole queue document. What actually ran
The other three registered projects stood down in the same sweep — "the queue is empty, so there is nothing to drain". The output is real work#8 — -console.log("hello")
+const name = process.argv[2] || "world"
+console.log(`hello ${name}`)#7 — Two problems this turned up, neither caused by this branchThe triage implemented a ticket instead of queueing it. A Run now can be silently eaten by the 30-minute cooldown. Clicking the drain right after the triage gave: Nothing started. It is reported on the card, but a user who clicks and looks away sees a button that did nothing. A daemon restart clears the cooldown, which is what unblocked this run. And a third instance of this branch's own theme. The first triage click aborted on |
Dogfooding the rotation (#1640) showed `Add quick-win work to AI Queue` queueing two of three tickets and implementing the third itself: a README change committed onto `tf-triage-quick` with its own pull request. Nothing in `auto-pm.ts` asked for that; the prompt said "choose tickets to work on" and "add tickets to TODO_AGENTS.md" and never said not to do them. Both triage presets now end with the rule: the only file a triage changes is `TODO_AGENTS.md`, and a ticket is never implemented inside one, however small its plan. The reason is stated in the prompt so the agent weighs it rather than obeys it — the queue is where a human can veto a ticket before an agent builds it. The rule lives once, as `prompts/triage_scope.md`, beside the queue format it protects; the catalog appends it to both triage rows, so the two preset files carry only what differs between them. The pair had already drifted once on a shared sentence (the collision guard is worded two ways), which is why it is not pasted into each. A test asserts each render ends with the shared file's text. The split matters for concurrency as much as for the veto: a triage rewrites the shared queue document and so runs as one agent, while the drain runs one agent per entry. A ticket built inside a triage takes the single-agent lane and skips the fan-out. `prompts/SPEC.md` and `preset-catalog.SPEC.md` record the rule and that rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1645) After any run on a project, the sweep's 30-minute cooldown stood every later pass down with "a run was started for this project a moment ago". Since #1210 a Run now is a sweep, so for half an hour the button could start nothing — the stand-down is reported in small text under the fold of the card, after the click, while the button stays enabled and its tooltip still promises agents. Restarting the daemon cleared it, which is how the #1640 dogfood got past it. The cooldown paces work nobody asked for. A click is someone asking, the same reading that already lets an on-demand sweep outrank the master switch. So `autoPmDecision` takes `onDemand` and skips the cooldown for it; the sweep passes the flag through. The concurrency cap is deliberately not waived with it. The #685 test names the cooldown as what stops a tick landing before the spawn registers from doubling up; for a click that guard is the cap alone, which holds because `start()` resolves after the spawn and ticks are serialized, so a second click counts the first. The new decision test pins both halves. `auto-pm.SPEC.md` said Run now skips only the master switch; it now names the cooldown too, and why. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sweep (#1643) (#1649) Run now on either triage routine was a plain start outside the sweep, so it never reached the release the sweep does before firing a pinned job. A leftover copy of the triage branch made every click abort in seconds as "triage already pending", until someone deleted the branch by hand. The card now asks the sweep for that one routine — `only: { pinned: <branch> }`, scoped to the picked project — the way the plan click does since #1640. The sweep finds the job by the branch it declares, never by name; a switched-off routine stands the click down with a reason; the named job takes the tick outright, so neither a full queue nor the rotation index borrows it, and the rotation keeps its turn.
🤖 curated
Draft: this is Option B from #1204, built so the choice can be seen rather than imagined. One word merges or closes it.
@brillout — you wrote "as long as there isn't any redundant work being done, I don't see a problem with concurrent agents." This is the one place that condition was met and the setting still did nothing.
What was wrong
Two routines can fan out safely — a drain takes one entry off the queue, a plan agent writes one ticket's own sibling files — so their agents do disjoint work. Only one of them did it from the card:
Concurrent agentsConcurrent agentsConcurrent agentsSame routine, two behaviours depending on who fired it. The tooltip said "Starts one agent" and was telling the truth about the click — which is why this reads as a missing feature rather than a bug.
What changed
The sweep's narrowing stops being drain-shaped:
drainOnly: truebecomesonly: 'drain' | 'plan'. A plan-only tick reaches the same claim-then-start path the rotation already takes — candidates enumerated, ticket locks pushed before any agent starts, one agent per locked ticket.The locks are the point. Without them two clicks a second apart plan the same ticket twice, which is the exact double-work this thread is about. This reuses the sweep's existing lock path rather than adding a second one.
projectIdscopes a sweep to one project. The drain sends none and visits them all — what its tooltip promises. Planning is the picked project's own work and carries one.Three things a narrowed click must not do
Each is its own test:
The tooltip follows the behaviour
Before → after, same
Concurrent agents: 2:Verified in the running dashboard, not only in tests.
The menu item moved too: "one agent, not the fan-out" was true of the drain and became true of planning the moment its button stopped being one start. It reads
fansOut, not a routine name.On testing
Every new test was checked by breaking the code it guards — five deliberate reversions in
auto-pm.ts, three inRoutineWork.tsx, each caught by exactly the test that should catch it.One test failed that check and had to be rewritten. The plans-instead-of-draining test passed with the fix reverted: it asserted the job's name, which stays
planeither way, when the real difference is the batch — entries off the queue instead of tickets. It asserts the prompts now, and fails as it should.pnpm typecheckon both packages; 1541 node tests + 796 vitest, all passing.If you'd rather have Option A
Close this. A is coherent — Run now means "start this routine once", and fan-out is what unattended running adds. The code even had a comment defending it. Nothing else in the branch depends on this.