Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FEATURES-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ happens while nobody is at the keyboard.
- CI watch: merge a PR once its checks pass
- CI watch: one fix agent per red head commit, max two attempts
- Reclaim the checkout of an agent whose work is on the remote β€” never by publishing what a `handoff: local` agent refused to
- Release a pinned routine branch left behind by a closed PR
- Release a pinned routine branch left behind by a closed PR β€” before the schedule fires the routine, and before its "Run now" does
- The agent drains its own TODO backlog, one entry per turn

## Spending
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ The Overview's Routine work card: the jobs fired by the scheduled sweep β€” the
## Flows

- The list is read straight from the definition the daemon runs, so screen and schedule cannot drift; Run now starts the work at once rather than asking the sweep to come sooner.
- The two routines that fan out β€” queue-draining and ticket-planning β€” have their Run now ask the sweep for that routine's work only, which is the one path that can spin up several agents, up to the concurrency setting. Draining visits every project; planning stays in the project the card has picked. Every other routine's Run now is one agent, because concurrent copies of it would undo each other. Card-fired routines run unattended, like the sweep's own.
- The two routines that fan out β€” queue-draining and ticket-planning β€” have their Run now ask the sweep for that routine's work only, which is the one path that can spin up several agents, up to the concurrency setting. Draining visits every project; planning stays in the project the card has picked. The two triage routines ask the sweep too, for a different reason: each works on one fixed branch, and only the sweep deletes a leftover copy of it before starting β€” started directly, the agent read the leftover as a triage already under way and gave up. Every other routine's Run now is one agent started directly, because concurrent copies of it would undo each other. Card-fired routines run unattended, like the sweep's own.
- Two checkbox tiers: the master switch turns the schedule on or off, a row's box takes that one routine in or out of it β€” recorded as opt-outs, so a routine added by a later version runs by default.
- "Trigger routine now" sweeps once even with auto-run off (the click is the consent), and the sweep answers on the card per project, so "ran and found nothing" never looks like "never ran".
- Hovering a Run now says what it is about to spend before it is spent: what that routine does, which model it will use, and where it runs β€” none of which the card can otherwise show, because all three come from the Global options on another page. It also says how many agents the click costs, which is one for most routines but the concurrency setting for the two that fan out. The queue-draining routine answers the model and place differently, since its Run now visits every project the daemon watches and each of those decides its own.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Covers the Routine work card: routines listed by label with Run now starting the prompt verbatim (unattended, then jumping to the agent), the drain firing a fan-out-capable sweep instead, per-routine opt-out boxes versus the master auto-agent switch, the on-demand trigger working with auto-run off, the sweep's per-project answers landing on the card, the concurrency setting's clamping, "Configure first, then run" carrying the prompt to the picked project's launcher instead of starting anything (and saying so on the drain), each Run now naming on hover what the routine does and the model and place its start would use (and the drain saying instead that every project decides its own), and the picker, empty, failure, and busy states.
Covers the Routine work card: routines listed by label with Run now starting the prompt verbatim (unattended, then jumping to the agent), the drain and the planning routine firing a fan-out-capable sweep instead and each triage routine asking the sweep for itself by its branch, per-routine opt-out boxes versus the master auto-agent switch, the on-demand trigger working with auto-run off, the sweep's per-project answers landing on the card, the concurrency setting's clamping, "Configure first, then run" carrying the prompt to the picked project's launcher instead of starting anything (and saying so on the drain), each Run now naming on hover what the routine does and the model and place its start would use (and the drain saying instead that every project decides its own), and the picker, empty, failure, and busy states.

## Before modifying/creating SPEC.md files

Expand Down
19 changes: 19 additions & 0 deletions packages/the-framework/dashboard/components/RoutineWork.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,25 @@ describe('RoutineWork (#1159)', () => {
expect((await hoverTooltip(await runNowOf(PLAN_JOB))).textContent).toMatch(/Starts up to 1 agent in gemstack/)
})

// #1643: a routine pinned to a branch goes through the sweep too β€” one agent still, but the
// sweep releases a stale copy of its branch before the start, and a plain start never did: the
// agent read the leftover as a triage already pending and aborted, on every click.
test("a pinned routine's Run now asks the sweep for it by its branch, not a plain start (#1643)", async () => {
const pinned = AUTO_PM_ROUTINES.filter(job => job.pinnedBranch !== undefined)
expect(pinned.length).toBeGreaterThan(0)
renderCard()
for (const job of pinned) {
fireEvent.click(await runNowOf(job))
// By the branch the job declares, never its name, and scoped to the picked project like
// the plan click: the routine is that project's own work.
await waitFor(() =>
expect(sendAutoPmSweep).toHaveBeenCalledWith({ only: { pinned: job.pinnedBranch }, projectId: 'p1' }),
)
}
expect(sendAutoPmSweep).toHaveBeenCalledTimes(pinned.length)
expect(start).not.toHaveBeenCalled()
})

/** Open one row's secondary half β€” the chevron beside its Run now. */
const openRunMenu = async (job: AutoPmJob) => {
await waitFor(() => expect(screen.getAllByText('Run now').length).toBe(AUTO_PM_ROUTINES.length))
Expand Down
42 changes: 30 additions & 12 deletions packages/the-framework/dashboard/components/RoutineWork.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import type { AutoPmJob, AutoPmOutcome, ProjectSummary } from '../../src/index.js'
import type { AutoPmJob, AutoPmOnly, AutoPmOutcome, ProjectSummary } from '../../src/index.js'
import {
AUTO_PM_ROUTINES,
DEFAULT_AUTO_PM_CONCURRENCY,
Expand Down Expand Up @@ -34,8 +34,10 @@ import {
//
// The list is `AUTO_PM_ROUTINES` itself, straight off the browser-safe client entry, so what is on
// screen is what the daemon runs rather than a second copy of it: no read of its own, and no way
// for the two to drift. Run now takes the same path the launcher does (`sendStart` with the job's
// prompt verbatim), so it starts the work now instead of asking the sweep to come round sooner.
// for the two to drift. Run now on an ordinary routine takes the same path the launcher does
// (`sendStart` with the job's prompt verbatim), so it starts the work now instead of asking the
// sweep to come round sooner; a routine that needs the sweep's own preparation asks the sweep for
// that one routine's work instead (see `narrowedSweep`).
//
// Two tiers of checkbox, and they control different things (#1209). The one at the foot is the
// `autoPm` preference (#685): whether the schedule runs at all. The one on each row is that
Expand All @@ -48,6 +50,28 @@ import {
/** Captured once: `useLoaded` treats a fresh `[]` literal as a new value on every render. */
const NO_PROJECTS: ProjectSummary[] = []

/**
* The sweep a routine's Run now asks for, when it is a sweep rather than a plain start β€” or
* nothing, for the routine a plain start serves exactly. Decided by what the job declares about
* itself (it drains, fans out, or pins a branch), never by its name, so a renamed routine keeps
* its path.
*
* The two that fan out (#1204) go because only the sweep can: it claims the work before each
* agent starts β€” a queue entry for a drain, a ticket lock for planning β€” and a plain start could
* only ever be one agent. A routine pinned to a branch (#1643) goes for the sweep's other
* preparation: it releases a stale copy of that branch before the start, and without it the
* agent read the leftover as a triage already pending and aborted, on every click.
*
* The drain visits every project, which is what its tooltip says and why it sends no id. The
* rest are the picked project's own work, so they carry one.
*/
function narrowedSweep(job: AutoPmJob, projectId: string): { only: AutoPmOnly; projectId?: string } | undefined {
if (job.drains) return { only: 'drain' }
if (job.fansOut) return { only: 'plan', projectId }
if (job.pinnedBranch !== undefined) return { only: { pinned: job.pinnedBranch }, projectId }
return undefined
}

/**
* The triggered sweep's answer as one card line (#1433): a single project speaks its message
* plainly, several are prefixed with the folder name so the reader can tell whose line is whose.
Expand Down Expand Up @@ -156,19 +180,13 @@ export function RoutineWork({

const runNow = async (job: AutoPmJob) => {
if (!projectId || busy) return
// The two routines that fan out go through the sweep (#1204), because only the sweep can:
// it claims the work before each agent starts β€” a queue entry for a drain, a ticket lock for
// planning β€” and a plain start could only ever be one agent, reading whatever is first.
// Narrowed to the one routine the click named, so having nothing to work is reported on the
// Narrowed to the one routine the click named, so having nothing to do is reported on the
// card rather than the click quietly borrowing a different rotation job. No navigation on
// purpose: the agents land in the Agents card, which is where a batch is watchable.
//
// The drain visits every project, which is what its tooltip says and why it sends no id.
// Planning is the picked project's own work, so it carries one.
if (job.drains || job.fansOut) {
const narrowed = narrowedSweep(job, projectId)
if (narrowed) {
setStarting(job.name)
setSweepNote(null)
const narrowed = job.drains ? { only: 'drain' as const } : { only: 'plan' as const, projectId }
const result = await sendAutoPmSweep(narrowed).catch(() => ({ ok: false as const }))
setStarting(null)
if (!result.ok) setSweepNote('This dashboard is not running the sweep, so there is nothing to trigger here.')
Expand Down
2 changes: 2 additions & 0 deletions packages/the-framework/src/auto-pm.SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Auto PM spends leftover subscription quota on the product's own roadmap: while t
- Both phases claim their ticket with a pushed lock file before the agent starts, so agents on other machines cannot double-book it: planning locks the ticket it will plan, and draining locks the ticket its queue entry links back to. An entry claimed elsewhere is dropped from the batch, and an entry with no ticket behind it keeps the queue itself as the coordination point.
- A claim whose agent settled with nothing to hand off is released by the sweep: the pull request that normally lifts the lock is never coming, and without the release the queue would jam forever on a dead claim. The freed work is not respawned by this daemon β€” one commitless run is evidence for a human, not an invitation to repeat it every cooldown. A claim whose agent never even started (a refused spawn, a stop mid-batch) is freed the same way.
- The queue coordinates a ticketless entry only once its check-off is on the data branch, and that leaves a window: an agent handed off to a cloud session settles locally before its published work is adopted, so until the check-off lands the entry still reads open, and past the cooldown it can be fanned out to a second agent. The same window opens when the daemon restarts, since only its in-memory pin covered the wait.
- The two triage routines each work on one fixed branch, and their agent refuses to start when that branch already exists, so two triages never run at once. Before firing either β€” from the schedule or from its "Run now" β€” the daemon deletes a leftover copy of that branch whose pull request is closed or merged; a branch with an open pull request, or with no pull request history at all, is kept, since that may be a triage still under way.
- Each routine can be switched off individually, and every stand-down is reported with its reason: a wedged sweep must not look like a healthy idle one.
- Switching the draining routine off means "do not *work* the queue", not "do nothing": the pass falls through to the rotation, which puts entries *on* the queue rather than taking them off. The one exception is a click that asked for the queue by name: a drain-only sweep says why it cannot, rather than borrowing the click.

Expand All @@ -24,6 +25,7 @@ Auto PM spends leftover subscription quota on the product's own roadmap: while t
- Where the account stands is asked per project rather than once per pass, because the model a project's work would run on is a project setting and each model's own weekly allowance binds alongside the account's. Two projects on two models can therefore stand at two different places against the same reading.
- "Run now" skips the master switch and the cooldown: the click is the consent the preference exists to record, and the cooldown paces work nobody asked for β€” without skipping it, the button could start nothing for half an hour after any run. Every other stand-down holds: a click cannot exceed the concurrency cap, and that cap is what keeps a second click from doubling up.
- A "Run now" can ask for one routine's work in one project, rather than a whole pass: it never falls through to work the click did not name, a switched-off routine stands it down instead of being overridden, and it leaves the rotation on whichever turn it was on.
- A triage's "Run now" goes through the pass rather than starting its agent directly, so it gets the same branch release the schedule gets: started directly, the click died on the branch the previous triage left behind, and kept dying on every click until someone deleted it by hand.
- A switched-off draining routine falls through to the rotation rather than standing the pass down, because a stand-down would make every inventing routine unreachable whenever the queue holds anything β€” and the queue is auto-populated, so it usually does.
- The ticketless hand-off window is accepted rather than closed: closing it would take a durable per-entry claim β€” a second claim shape beside the pushed ticket lock that already covers the queue's normal case β€” for a race whose cost is a duplicated attempt, never lost work.

Expand Down
2 changes: 1 addition & 1 deletion packages/the-framework/src/auto-pm.test.SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Covers auto PM's decision policy β€” quota and queue both failing closed when unreadable, cooldowns, concurrency caps β€” the drain-before-refill cycle, rotation order and per-routine opt-outs (an unticked drain routine falls through to the rotation; a drain-only click still stands down), the calendar-paced maintenance sweep, fan-out with pinned entries and locked tickets, drains claiming their entry's ticket before starting (ticketless entries exempt; a lost claim drops the entry, not the batch), durable claims surviving restarts and hand-offs, a claim released when its agent settles with nothing to hand off and only then (the freed work not respawned; refused-spawn claims freed too; failed releases retried under a bound), on-demand and drain-only passes, and the report the dashboard shows.
Covers auto PM's decision policy β€” quota and queue both failing closed when unreadable, cooldowns, concurrency caps β€” the drain-before-refill cycle, rotation order and per-routine opt-outs (an unticked drain routine falls through to the rotation; a drain-only click still stands down), the calendar-paced maintenance sweep, fan-out with pinned entries and locked tickets, drains claiming their entry's ticket before starting (ticketless entries exempt; a lost claim drops the entry, not the batch), durable claims surviving restarts and hand-offs, a claim released when its agent settles with nothing to hand off and only then (the freed work not respawned; refused-spawn claims freed too; failed releases retried under a bound), on-demand and drain-only passes, a pass narrowed to a triage routine releasing its leftover branch before its one start (standing down when the routine is off, and never falling through to the queue or another routine), and the report the dashboard shows.

## Before modifying/creating SPEC.md files

Expand Down
Loading
Loading