The daemon reads no queue: it starts /work-queue when the agent-data branch moves - #1777
Conversation
…branch moves The drain half of Auto PM goes. The daemon no longer reads the agent queue, claims tickets for a run, pins an entry into the prompt, or deletes the entry at settle. It watches the head of the agent-data branch instead: a commit no daemon wrote starts one agent told /work-queue, the routine skill this package now ships and links into every checkout. The agent composes the tickets and queue skills itself. Its own commits move the branch again, so the daemon fires again as the run ends; a run that finds nothing queued moves nothing, and the chain stops. Then the rotation gets its turn. One heartbeat a day is the belt. Every commit the daemon writes on its own (a run's record, a routine lock, a plan claim) goes through one funnel that signs it with a `Daemon: <host>` trailer, so a record never starts the next run and two daemons sharing a branch never fire at each other. Gone with it: the drain preset and its recognition, todo-loop.ts and the merge gate that read the session's TODO file, the ticket start option, the ticket event and the card's ticket field, the (fix #N) title, the PATH entries and the three skill links in every checkout, AGENT_ID as an import from the tickets skill, and the tickets and queue sync calls, replaced by one pull of agent-data. The Overview's implementing lane goes with the ticket field; the dashboard half brings it back from the claim. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAh91ACHayiEsgT1k2i8Nf
The sweep's logic rewritten around the branch head, the chain and the heartbeat; the daemon's signed writes and the routine skill get their own; the drain, the backlog loop, the ticket lane and the (fix #N) title leave every file that described them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAh91ACHayiEsgT1k2i8Nf
Two kinds of skill: capability skills (branches, tickets, queue, logs), a SKILL.md and a command each; routine skills, a SKILL.md only, each a job that composes capabilities. A routine names the capabilities it uses and a capability never names another skill, so a routine cannot live in a capability package. The routines package holds them, one folder per routine under skills/, the skills-npm layout; the framework depends on it and links the file into every checkout it makes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAh91ACHayiEsgT1k2i8Nf
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAh91ACHayiEsgT1k2i8Nf
…em or no AI queue Rom's note on #1774: a command assumes no capability; where it would be broken without one, it tells the user in capability words and stops. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAh91ACHayiEsgT1k2i8Nf
|
👤 How this PR was driven. Everything I typed to the agent, verbatim, in order. What the agent did between my prompts is summarized under 🤖. One session, 2026-09-13 19:00 to 2026-09-14 01:30 IDT, Fable 5.1, effort high. 1. My brief that opened the session:
🤖 Read the memory, the plan, the facts file and the rig. Read the daemon code: 2.
🤖 Asked the same four questions again, one situation each, plainer. 3. My picks, one per question:
🤖 Argued the skill file's home: the routine names both skills, and Rom's rule is that skills do not know each other. Asked again with three options, and flagged the step-1 gap: the plan removes the checkout links assuming projects track the SKILL.md files, and gemstack does not yet. 4.
🤖 Read that as: the routine is a job the framework ships, not a skill package's file; the links go, the cutover tracks the files. Built the PR in a scratchpad worktree: the trigger and chain in 5.
🤖 Pushed the branch and opened #1777. 6.
🤖 Prepared the two edits and showed them, plus a third: the comment's first line said "Nothing is built yet". 7.
🤖 Edited the comment: headings Q6.1 to Q6.6, the exact drain prompt quoted in Q6.1, "Built as #1777" on top. 8.
🤖 Answered: the routine names tickets, so it cannot live in the queue package under Rom's rule, and the daemon would need a queue import for the path. 9.
🤖 Laid out two kinds of skill, capability and routine, and three homes for routines: the framework's own skills folder, one routines package, one package per routine. Recommended the routines package. 10.
🤖 Explained "routine": a job the daemon runs by itself, no click; a preset is the same shape started by a person. Asked whether to make the package now or in the next round. 11.
🤖 Created 12.
🤖 Drafted a shorter pick and showed it. 13.
🤖 Posted it. 14.
🤖 Answered: the sibling packages ship one skill each, this one ships several; and the name is Rom's to decide. 15.
🤖 Recommended keeping 16.
🤖 Answered: Auto PM is the scheduler, daemon code, not a routine; what is still not modular is that it knows the routines by name; the end state is triggers declared in each SKILL.md. 17.
🤖 Recommended yes, later: it is already one function with every reading injected, a library like agent-driver; after the triggers step, so no name moves with it. 18.
🤖 Added both under "What remains" in the PR body and listed the next steps. 19.
🤖 Added them to the comment's remains and order of work. 20. After Rom's review on Discord (2026-09-14, 13:15 to 14:32):
🤖 Added Rom's note to the work-queue command, put my picks as a 👤 block on top of the PR body, posted this comment. Every design pick above is mine. The agent wrote the code, the tests, the LOGIC.md files, the PR body and the comment edits, and posted only on my word. |
…1778) The daemon links nothing but branches and work-queue into a checkout since #1777. A project that wants its agents to know the tickets, the queue and the logs tracks the three SKILL.md files under .agents/skills with the .claude/skills links, the way the ldd skill is tracked. Copies of the packages' SKILL.md files at 06ea480. Claude-Session: https://claude.ai/code/session_01Rjdv16jjSyTUHZLXHpYjwG Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
👤 My picks for this PR, in my words as I gave them to the agent:
What to look at: the first section below, the "What remains" list, and the proof at the end. Every prompt I gave for this PR is in the 👤 comment on this PR, verbatim.
The daemon half of #1774, step 2 of Q6. Built as posted in the plan, six picks as picked.
🤖 automated · Fable 5.1, effort high
What the daemon does now
The daemon reads no queue and names no skill. It watches the head of the
agent-databranch, once a minute, right behind its own pull. When the branch moved by a commit no daemon wrote, it starts one agent told/work-queue. The agent reads the skills in its checkout and takes one queued task. Its claim, close and done are commits, so the branch has moved again by the time the run ends, and the daemon fires again on the next look. A run that finds nothing queued writes nothing, and the chain stops. Then the rotation gets its turn. One run a day is the belt when nothing moved.The routine is a skill file,
skills/work-queue/SKILL.mdin a new package,@gemstack/routines, the first preset converted for #1770. Its body is the prompt from the plan, "committed counts as published" included, plus Rom's note from the review: no ticketing system or no AI queue, show an error to the user and stop. Rom named this kind "command skills"; the rename to that name, and one package per command, wait for his name shape. It is markeddisable-model-invocation, so only a person or the daemon invokes it. The framework depends on the package and links the file into every checkout it makes, beside thebranchesskill. The routines list shows it as "Work the queue"; Run now on that row starts one agent on the queue in every project.Two kinds of skill from here. Capability skills,
branches,tickets,queue,logs: a SKILL.md and a command each, each saying how to do one thing. Routine skills: a SKILL.md only, no code, each a job that composes capabilities. A routine names the capabilities it uses; a capability never names another skill, so a routine cannot live in a capability package. The routines package holds them, one folder per routine underskills/, the skills-npm layout. A project can install them without the daemon and replace one with its own. The picks are inpackages/routines/DECISIONS.md.How the daemon tells its own commits apart
A run's record, a routine lock and a plan claim are commits the daemon writes. Counted as moves, every record would start the next run, forever. Two daemons sharing the branch would fire empty runs at each other's records without end. So every automatic write goes through one funnel,
src/daemon-writes.ts, that ends the commit message with a git trailer:The trigger counts the commits since its last look that carry no
Daemon:trailer. A person's write from the dashboard (queue an entry, release a claim) takes the skills' plain funnel and carries none: that is a move, as it should be.What stays as it was
The quota gate, the concurrency cap and the rotation's 30 minute cooldown. The cooldown paces the rotation only. A move of the branch, from a person or an agent, is never paced, so the chain runs back to back. The rotation fires after a run that moved nothing, and keeps its start-up turn on a daemon that boots with the setting on. The first look after boot only remembers the head: a restart starts no run on the queue.
What goes
prompts/presets/drain_queue.md, its catalog row, and the exact-text match that recognised a hand-fired drain.auto-pm.ts: the drain job, the pinned prompt, the drain mode of the decision, the drain batch and its claims, the settle half that deleted the entry and freed the claim, the "ended dry" memory for entries. The plan fan-out keeps its claims and its dead-claim release; that is the follow-up's design.daemon-services.ts: the queue read, the drain claim and release, the promote, the ticket named on the start.todo-loop.tswhole, with its 13 tests, and the merge-gate belt that read the session's own TODO file with the queue parser. The agent's ready-for-merge signal is the authorization now.daemon-runtime.ts: the PATH entries, the three skill links in every checkout, andAGENT_IDas an import from the tickets skill.AGENT_ID_ENVis the framework's own constant inagent-id.ts.cli.ts: theticketstart option, the(fix #N)read from the ticket for the PR title, the ticket event. With them the card'sticketfield, the meta's, and the Overview's "implementing" lane, which the dashboard half brings back from the claim.agent-datathrough the shared branch library. The rootticketslink and the queue seed leave the daemon; they belong to the skills' own setup.What remains
Named here so the follow-ups have a list.
The rotation's tickets imports in
daemon-services.ts: update tickets, triage quick, triage consensual, plan tickets. Plan tickets still reads and claims tickets in the daemon.The dashboard's tickets and queue imports.
The two package dependencies,
@gemstack/skill-ticketsand@gemstack/skill-queue; the queue one only for the dashboard now.Two imports the plan keeps until step 4: the checkout from the branches package, the record write from the logs package.
The scheduler,
auto-pm.ts, knows the routines by name: the list, its order, which one works the queue, which one fans out, which two take a lock. Adding a routine means editing the daemon.Four follow-up PRs, one rotation job each, each becoming a folder in the routines package the same way: update tickets, triage quick, triage consensual, then plan tickets last, since its fan-out claims tickets in the daemon and needs its own design.
Then two more, in this order:
agent-driverandagent-data. Another runner, or a cron job, can then fire routines without the dashboard. Its policy is already one function with every reading injected; the move waits for the trigger step so no name moves with it.Temporary
work-queueinto checkouts through the branches package's caller list. That list was marked temporary for the tickets, queue and logs skills; those three links are gone, and the list now carries the routines package's skill. The comment inskill-branchessays so. If Replace presets in favor of skills withdisable-model-invocation#1770 ends with projects tracking the routine skills too, the list goes.labelandtooltipof the queued-work routine are written inauto-pm.ts, since it has no preset. Once the launcher reads skill files (Replace presets in favor of skills withdisable-model-invocation#1770), the skill's owndescriptionis the sentence.Cutover
The daemon no longer links the tickets, queue and logs skills into checkouts, and puts nothing on an agent's PATH. After merging, each project must track the three SKILL.md files under
.agents/skills/<name>/(with the.claude/skills/<name>links), and list the four@gemstack/skill-*packages in its dependencies sonpx ticketsresolves. gemstack lists the packages already; the files are not tracked yet in any project. Until a project tracks them, its agents see no skills. Then restart the daemon.Preference files that opted out
drain-queuename a routine that no longer exists; the entry is inert.Tests
auto-pm.test.ts: the trigger and the chain with a fake branch and fake starts. The first look remembers and starts nothing on the queue. A commit no daemon wrote starts the queued work once. A daemon's commit is not a move. The chain over three tasks under a cap of one, ending on the empty run. The rotation after an empty run, and a move taking the turn back. A move is not paced by the cooldown, the rotation is. The heartbeat. Run now on the row. The routine switched off. An unreadable branch. The stand-down logged once until it changes.daemon-writes.test.ts: real git. The funnel's trailer, unsigned commits counted, signed ones not, prose mentioning the word not a trailer, a range git cannot walk.daemon-services.test.ts: real registry, real branch, only the spawn stubbed. A plain commit on the branch starts one agent told/work-queue, unattended, merging, one per move however high the concurrency; a commit through the daemon's funnel starts nothing; Run now with the schedule off; the model-week gate on a move.daemon-runtime.test.tsanddaemon-workspace.test.ts: the spawned agent's environment has itsAGENT_IDand the daemon's own PATH untouched; the checkout links exactly two skills,branchesandwork-queue; the skill file names itself and is not model-invocable.daemon.test.ts: the teardown's record commit ends with the trailer.Each of the trigger, the chain and the signing was broken on purpose once, and the tests failed: counting signed commits as moves failed 2 tests, never setting the move failed 9, dropping the trailer from the record write failed the teardown test.
Removed with the drain: 28 tests in
auto-pm.test.ts, 4 indaemon-services.test.ts, 13 intodo-loop.test.ts, 3 incontrol.test.ts, the ticket lane tests inoverview.test.tsandHotTickets.test.tsx, the drain story instory-tickets-and-queue.test.ts.pnpm build,pnpm typecheckandpnpm testpass: 1350 node tests, 865 dashboard tests.LOGIC.mdupdated beside every touched file;auto-pm.LOGIC.mdrewritten around the branch head, the chain and the heartbeat;daemon-writes.LOGIC.mdand the routines package'sLOGIC.md,DECISIONS.mdandSKILL.LOGIC.mdare new. The LOGIC changes are their own commits.Proof
This PR's own daemon, on a throwaway repository with the tickets, queue and logs SKILL.md tracked, origin on GitHub (
suleimansh/dogfood-uns), an isolated config: Opus, concurrency 1, every routine but the queued work switched off. Three tickets, an empty queue. The daemon started, took its first look, and stood down. Then one commit onagent-dataqueued three entries. Nobody touched anything after that.The daemon's log:
The queue was pushed at 19:00:01 UTC. The first run started at 19:00:52. Each next run started on the look after the previous run's record landed. The fourth run started at 19:04:53 and ended nine seconds later. Nothing fired after it.
agent-dataon origin afterwards, newest first, with the trailer where there is one:The queue is empty and
tickets/holds nothing. PRs #1, #2 and #3 are merged into the throwaway's main.npx logsin a fresh clone lists the four runs:/work-queue/work-queue/work-queue/work-queuenpx logs show <id>, what each agent said:Run 1: "I added the line
License: MITto the end ofREADME.md, which was the first ticket in the queue. Commitfcd0c94is on branchagent-2026-09-13T19-00-52-977Z. I didn't push it; the run that started me publishes the branch. I claimed it before starting and closed it after committing, so there's no claim left to release. The entry is marked done. Two tasks are still queued." Then ready-for-merge and anopen-prblock.Run 2: "I took the 'Add a CONTRIBUTING.md' ticket from the queue and finished it. Committed as
3bd97e7onagent-add-contributing. I didn't push. The ticket is closed and its queue entry is gone. The only entry left is 'Fix the README title', which I didn't touch." It also reported an error: the throwaway'spackage.jsondid not list@gemstack/skill-branches, sonpx branchesfetched a stranger package from the registry; it renamed the branch withgit branch -minstead.Run 3: "I fixed the one ticket in the queue: the README title now reads
# throwaway. Commit9c7b6ddon branchagent-fix-readme-title. I closed the ticket (which also removed my claim) and marked its queue entry done. The queue is now empty." The same branches error, handled the same way.Run 4: "The agent queue is empty (
npx queuereturned[]), so there was nothing to pick up. I didn't claim a ticket, commit anything or change the queue."The branches error in runs 2 and 3 is the rig's: I wired only the tickets, queue and logs packages into the throwaway. It is the cutover rule above, seen from the agent's side. gemstack lists all four packages.
Total: $1.05 on Opus for three tasks and one empty run.