Skip to content

Second standalone skill: tickets and the agent queue (@gemstack/skill-tickets) #1748

Description

@suleimansh

Next standalone skill: tickets + the agent queue. Plan below; points 3 and 5 are heads-ups, the rest is what I'm building unless you object.

  1. Branch. Your .branches/tickets + .branches/agent-logs, one branch per kind: the skill owns tickets (tickets + queue), the framework owns agent-logs (archives + routine locks); agents-data goes. The sync/commit/push code becomes one function with a branch argument, in skill-branches. Inside the branch the layout stays as today: a tickets/ folder (tickets, plans, locks, meta.json) with TODO_AGENTS.md beside it — on disk .branches/tickets/tickets/<ticket>.md.
  2. Names. @gemstack/skill-tickets, command tickets, skill tickets, branch tickets. One package for tickets and the queue — the queue is made of tickets.
  3. Done queue entry = deleted (heads-up). Your todo_format.md says so; my All framework data moves to the tf-data branch (fix #1582) #1595 code marks - [x] instead, and the queue on origin is nine [x] lines. The code will follow the format.
  4. What goes inside a lock file. A .lock.md says which agent holds a ticket. Today it is written three different ways: your ticketing_format.md asks for CLAIMED: <SESSION_ID> (<SESSION_NAME>), but nothing ever tells an agent its session id, so agents improvise (a real lock on origin holds a claude.ai session id); the daemon writes a throwaway token like plan-<timestamp>-0 that matches no agent; the dashboard just prints whatever string it finds, so "who holds this ticket" is never answerable. Proposal: one line, CLAIMED: <agent id> — the same id that names the agent's checkout (.branches/agent-<id>). The tickets claim command reads the id from the directory it runs in, so the agent has nothing to type or know; the daemon creates the agent id first and claims with it, instead of the token; the dashboard can then look the id up and show the agent's name with a link to its page. The (<SESSION_NAME>) part goes: the name is the branch, and it changes when the agent renames it.
  5. Agents outside a daemon-made checkout (heads-up). Cloud, Actions and terminal runs get no linked skill and no command; today branches covers this with branch_yourself.md (three lines appended only for them). Tickets get the same: tickets_yourself.md + the format text. A stopgap, labelled temporary, deleted when use-npm-skills commits the skill into the repo.

One PR (skill + skill-branches primitive + the framework adopting it), dogfooded on an isolated rig before you see it.


🤖 curated · Fable 5, effort high — the plan, as the agent wrote it after reading the code; I read it and I stand behind the recommendations above.

The plan

What it is. The second standalone skill after branches: tickets (tickets/<DATE>_<SLUG>.md with their .plan.md / .lock.md siblings), the agent queue (TODO_AGENTS.md), and the branch both live on (tickets; today agents-data) — as one npm package with a SKILL.md, a tickets command, and the library the daemon and dashboard call. Same shape as packages/skill-branches: the package knows git, files and the two formats; nothing else. The embedding product is "the caller".

What the code says today (facts the plan rests on):

  • The three formats ride in every agent's system prompt as text (CONTEXT_FORMATS in system-prompt.ts:93: ticketing_format.md, todo_format.md, data_branch_protocol.md). No agent has a command; each improvises its data-branch write with git. 33 agent-authored commits exist on origin/agents-data ("Claim ticket … for planning", "Import GitHub issues …").
  • The lock format disagrees with itself three ways: the prompt says CLAIMED: <SESSION_ID> (<SESSION_NAME>), and <SESSION_ID> is defined nowhere in the prompts; the daemon writes CLAIMED: plan-<ts>-<i> / drain-<ts>-<i> — a sweep token, not an agent id (auto-pm.ts:1002,1060); a real lock on origin reads CLAIMED: session_01CYcH… (unified-session-timeline-plan) (a claude.ai session id). The dashboard shows the token verbatim (lockedBy, TicketsPanel.tsx:166), which resolves to no agent.
  • The queue format says a done entry is deleted (todo_format.md, its SPEC: "Done means deleted"); the code checks it off (checkOffEntry, todo-loop.ts), and origin/agents-data:TODO_AGENTS.md is nine - [x] lines and no open entry.
  • Everything data-branch is already one module (data-branch.ts, 396 lines: ensure, sync, the write funnel, the eager pull, readDataFile, dataProjectRoot) that the locks, the queue, the archives and the routine locks all write through.
  • The daemon puts branches on every agent's PATH (daemon-runtime.ts:114, CLI_BIN_DIR) and skill-branches links its own SKILL.md into every checkout it creates (skill-links.ts, called from checkout.ts:settle). Non-owned agents (terminal, Actions, cloud) get branch_yourself.md instead.

1. Package shape

Name. @gemstack/skill-tickets, directory packages/skill-tickets, command tickets, skill name tickets (frontmatter name: tickets, so harnesses list it as tickets beside branches). Depends on @gemstack/skill-branches (git runner, BRANCHES_DIR, excludeFromGit, agentIdFromWorktreeDir) — the branches package is the git substrate, the tickets package builds on it. Later one line, "dependencies": {"use-npm-skills": …}, makes it a committed skill.

One package for tickets and the queue, not two. The queue is made of tickets: an entry links tickets/<file>, the plan ask names a ticket file, a ticket's Priority: decides its ## Priority N section (todoPriorityForTicket, ticketFromQueueEntry are that seam), a drain claims the ticket its entry names, and every preset that touches one touches the other. Split, an agent loads two skills for one task and both packages need a shared third. The queue does hold non-ticket entries (quality follow-ups, resume notes), but on the same branch through the same command. queue.ts is its own file: a later split is a mechanical move, not a design decision now.

Layout (mirrors skill-branches):

packages/skill-tickets/
  SKILL.md                what the agent is told (below)
  SPEC.md
  bin/tickets             hands argv/cwd/streams to src/cli, exits with its code
  src/
    names.ts              TICKETS_BRANCH='tickets', checkout '.branches/tickets', TICKETS_DIR='tickets' (the folder on the branch, and the repo-root link), QUEUE_FILE='TODO_AGENTS.md', isTicketPath, lock/plan sibling names (pure, browser-safe)
    (the branch primitive — ensure, sync, the write funnel, eager pull, read — moves to skill-branches as `file-branch.ts`, parameterised by branch; see §2a)
    detached-write.ts     NEW: the command's write path — fetch, throwaway worktree on origin's tip, op, commit, push, retry, remove (§1 "how the command writes")
    tickets.ts            read side: readTickets(dataDir), readTicket, Ticket type (title/TLDR/priority/topics/github/date/planned/effort/uncertainty/locked/lockedBy), ticketIssueRef, todoPriorityForTicket, ticketFromQueueEntry
    locks.ts              lockContent(holder), lockHolder(md), claimTickets (batch, plan|drain), releaseTicket (heldBy)
    queue.ts              parseQueueEntries, insertQueueEntry (priority sections), removeQueueEntry, appendQueueEntry
    holder.ts             who the command claims as (§2b)
    cli.ts, bin-dir.ts, index.ts

Every file gets its SPEC.md per sdd.md; the framework's tickets.SPEC.md and ticket-locks.SPEC.md move here, data-branch.SPEC.md to skill-branches, all losing their product framing.

SKILL.md (the agent's text; product-free; formats copied from the three prompt files minus "The framework's own data"):

---
name: tickets
description: The project's tickets and its agent queue: where they live, how to read and change them, how to claim a ticket so no two agents work the same one, and the formats.
---

# Tickets and the agent queue

The tickets (`tickets/<DATE>_<SLUG>.md`, with their `.plan.md` and `.lock.md` siblings) and the
agent queue (`TODO_AGENTS.md`) live on the branch `tickets`, never on a code branch. Your
checkout does not contain them. A `tickets` link at the repository root may show them: read
there if you like, never write there.

Read and change them with the `tickets` command. Every change it makes is one commit pushed
straight to the `tickets` branch (a rejected push is rebased and retried for you); nothing you commit
on your own branch reaches them, and these files never belong on your branch.

## Read
  tickets list                 every open ticket, JSON: file, title, summary, priority, topics,
                               github, planned, effort, uncertainty, locked, lockedBy
  tickets show <file>          one ticket: its text, its plan, who holds it
  tickets queue                the queue's open entries, in order of work

## Change
  tickets put <file>           write one file under tickets/ from stdin (a ticket, a plan, meta.json)
  tickets close <file>         remove a ticket with its plan and lock — tickets/ holds only open tickets
  tickets queue add <text> [--priority N] [--ticket <file>]
  tickets queue done <text>

## Claim before you plan or work a ticket
  tickets claim <file>         {"ok":true,"holder":…} — the ticket is yours
                               {"ok":false,"reason":"claimed","holder":…} — someone else's: back off,
                               pick another; never remove or overwrite their lock
  tickets release <file>       lift your own claim (a finished plan, work that is published)

## Formats
  [ticket file — as ticketing_format.md]
  [plan file — as ticketing_format.md]
  [queue file — as todo_format.md]

No git show/git commit instructions in SKILL.md: the skill is the command, like branches. Where the command is not on the PATH is the caller's fallback (§3).

How the command writes — the one design point. Two processes must never share the daemon's checkout: the daemon's funnel git add -As and, on failure, reset --hards its checkout (data-branch.ts today, file-branch.ts in skill-branches after), so a command writing into the same directory would be committed under the daemon's message or wiped. data-branch.SPEC.md already states the rule: "an agent that writes data is a remote writer, like another machine: it commits onto the branch in its own checkout and pushes, and the race is settled by the push itself." The command does exactly that: git fetch origin ticketsgit worktree add --detach <tmp> origin/tickets → run the op → git add -A && git commitgit push origin HEAD:refs/heads/tickets → on rejection re-fetch, reset, re-run the op (twice, like the funnel) → git worktree remove --force. The local tickets ref is never moved (the daemon's checkout would see its index go stale); the daemon's eager pull brings the change in within a minute. Works in a plain clone (cloud, Actions) with no .branches/ created. The ops (claim, release, put, close, queue add/done) are the same functions the funnel runs for the daemon — one op per write kind, two runners (persistent checkout with sync; throwaway worktree on origin's tip). No remote → writes refuse with no-remote (a repository nothing can reach is already the caller's error state); reads still work off the local branch.

Reads: tickets list/show/queue fetch first and read origin/tickets via git show/ls-tree (the branch's own fresh read), falling back to the local branch when there is no remote.

CLI contract = the branches contract (cli.ts): JSON on stdout, one line on stderr, exit 0 result / 1 refusal or git failure / 2 usage. Refusals: no-ticket, claimed (+ holder), not-holder (+ holder), no-identity, no-remote, not-a-repo, invalid-path (put/close accept a bare <name> inside tickets/ only, never a .lock.md — claims go through claim). Commit messages verb-first, no product prefix: claim tickets/<stem>, release tickets/<stem>, put tickets/<file>, close tickets/<stem>, queue add: <text>, queue done: <text>, seed the queue, sync. The caller's own writes (archives, routine locks) pass their own message and may keep [The Framework].

Library surface the caller needs (what framework imports from the package; everything below exists today under a framework name):

today (framework) callers in the package
TICKETS_DIR, FLAT_TODO_FILE, DATA_BRANCH, DATA_CHECKOUT_DIR layout.ts, framework-dir.ts, daemon-services, auto-pm, control.ts, overview.ts, docs.ts names.ts (TICKETS_BRANCH, QUEUE_FILE); the caller's agent-logs name stays in framework-dir.ts
isTicketPath, ticketFromQueueEntry, todoPriorityForTicket, ticketIssueRef cli.ts (--ticket, PR title), daemon-runtime, auto-pm, control.ts tickets.ts
readTickets, readTicket, isTicketFile, WorkspaceTicket(Detail) reads.ts RPCs, overview.ts (hot tickets), daemon-services (planCandidates), dashboard.ts tickets.ts (Ticket, readTickets(dataDir))
acquireTicketLocks(cwd, assignments, deps, phase), releaseTicketLock(cwd, ticket, deps, {heldBy}), ticketLockHolder daemon-services (lockPlans/lockDrains/releaseLock), control.ts (release button) locks.ts (claimTickets, releaseTicket)
withDataBranch, pullDataBranch, readDataFile, dataProjectRoot, dataWorktreePath, resolveDataFileDeps worktrees.ts, daemon-runtime (archive), archived-agent-patch, routine-locks, todo-loop, docs.ts, cli.ts skill-branches/src/file-branch.ts, a branch argument (tickets for the skill, agent-logs for the caller's archives and routine locks)
parseTodoEntries, insertTodoEntry, checkOffEntry, appendFlatTodoEntry, appendTodoEntry, findTodoBacklog, nextQueuedTicket todo-loop's runTodoLoop, daemon-services (promote), control.ts (queue buttons) queue.ts
CLI_BIN_DIR daemon-runtime childEnv (prepend both packages') bin-dir.ts

Stays in the framework: planTicketPrompt + planAgentFor (the plan-ask sentence and the record lookup, §3), readTicketsMeta (tickets/meta.json, the GitHub-import stamp), runTodoLoop, ticketForPrompt/drainsQueue, routine-locks.ts, agent-archive.ts.

2. The two decisions

2a. The data branch name — recommendation: a branch named after what it holds, tickets

agents-data was a "for now" name; the convention idea (.branches/tickets, .branches/agent-logs) names a branch after what it holds. The skill's PR is where it gets settled, because SKILL.md names the branch to every agent.

  • The skill's branch is tickets, checkout .branches/tickets — a constant in names.ts, like .branches is BRANCHES_DIR; not a parameter, not config (an agent in a cloud clone has no config to read; no caller wants another name). A user of the skill is told "your tickets live on the tickets branch": self-explaining, no product-wide name inside a standalone skill.
  • The caller's archives go on the caller's branch (agent-logs, your word; the routine locks go with it). Nothing of the framework's rides in the skill's branch.
  • The funnel becomes a branch-parameterised primitive in skill-branches — "a branch used as a file store": ensure, sync, write, eager pull, read, for a branch argument. Git knowledge with no ticket knowledge; both packages need it. skill-tickets calls it with tickets, the framework with agent-logs. (data-branch.ts therefore moves to packages/skill-branches/src/file-branch.ts, not into the tickets skill — this replaces the §1 layout line.)
  • Layout inside the branch: unchanged. A tickets/ folder (tickets, plans, locks, meta.json) with TODO_AGENTS.md beside it, as today — Move tickets/TODO.md to TODO-AGENTS.md #682's "tickets/ holds only tickets" stays true. On disk the checkout reads .branches/tickets/tickets/<DATE>_<SLUG>.md (the branch is named tickets and holds a tickets/ folder); the repo-root link tickets/.branches/tickets/tickets hides the doubled path from humans. Repo-root paths stay tickets/<file>.
  • Cost: two branches per project in the eager pull (one more fetch a minute). Cutover by hand, in the PR body: agents-datatickets minus agents/; agents/agent-logs; delete agents-data; every machine re-ensures.

2b. What a lock records — recommendation: one token, the agent id, minted before the agent starts

CLAIMED: <holder>, one line, no parenthesised name. Holder = the agent id.

  • Why the id and not the branch. The branch changes under a claim (branches name renames agent-<id> to agent-<name> after the lock was written), so a lock naming a branch goes stale at the first rename. The checkout directory does not change: it stays .branches/agent-<id> for its life (branch-links adds a link per name beside it). The id is already the branches skill's own concept (branches create <id>, agentIdFromWorktreeDir), not the caller's.
  • What the command writes. holder.ts: inside a .branches/agent-<id> checkout → <id>, read off the checkout root's name; anywhere else → the current branch name (a cloud session on claude/…, a person on a feature branch); detached HEAD → refuse no-identity. Nothing for the agent to type or know — this retires the undefined <SESSION_ID>. The library takes holder explicitly (the daemon passes what it knows).
  • The daemon's own claims. Today the sweep locks with a token and starts the agent afterwards, because "no agent id exists before the agent starts" (auto-pm.ts:243). It can exist: ids are agentIdFromStartedAt(now) (daemon-runtime.ts:756) and the store already takes a caller-given id (agent-store.ts:401). The sweep mints the id, claims with it, starts the agent with it; heldBy release compares against the same id; the pinned prompts' "your claim holds CLAIMED: <token>" becomes "the ticket is claimed for you — tickets show names you". To confirm in implementation: that startUnattended threads an id through to allocateWorkspace.
  • How the dashboard shows who holds a lock. lockedBy is now an agent id → the caller resolves it against its records (findAgent) to the session name (the branch) and links the ticket row to the agent's page; an id it does not know (another machine's agent, a cloud session's branch name) shows verbatim, as today. This is the first time the holder is resolvable at all.
  • Drop (SESSION_NAME). The name is read from the branch whenever it is needed, and it changes after the claim; keeping it in the file is a second copy that goes stale. The parser today already treats the whole rest of the line as the holder, so a parenthesised name would break heldBy equality anyway.

Hard switch: the one lock currently on origin (2026-07-27_unified-session-timeline.lock.md, a claude.ai session id) is released by hand.

3. What moves out of packages/framework, what stays

Moves (deleted from the framework, no re-exports):

  • src/data-branch.ts (+ SPEC, test 241 lines) → skill-branches/src/file-branch.ts, branch-parameterised; src/tickets.ts (+ SPEC, test 123) minus planTicketPrompt/planAgentFor, src/ticket-locks.ts (+ SPEC, test 255), the pure half of src/todo-loop.ts (parseTodoEntriesnextQueuedTicket, with their tests out of the 552), the parse half of src/dashboard/tickets.ts (describe, planMeta, readTickets, readTicket, isTicketFile, the WorkspaceTicket types; test 356 split).
  • prompts/ticketing_format.md, todo_format.md, data_branch_protocol.md (+ SPECs, README rows) → their content lives in SKILL.md only. CONTEXT_FORMATS and the headings it names go from system-prompt.ts; the two CONTEXT_DOCS bullets become tickets/**.md / TODO_AGENTS.md … "on the tickets branch; read and change them with the tickets skill".
  • framework-dir.ts: DATA_BRANCH/DATA_CHECKOUT_DIR become LOGS_BRANCH = 'agent-logs' and its checkout — the caller's branch for archives and routine locks; .the-framework/LAYOUT prints both branches.

Stays (caller's, product knowledge):

  • Dashboard pages and RPCs (TicketsPanel, TicketDetailPage, TicketPlanPage, TicketsPage, onTickets/onTicket/onAllTickets/onHotTickets, queue/release/plan buttons) — they call the package's readTickets(dataWorktreePath(cwd)) instead of reading the root tickets link (a real tickets/ directory of the user's would shadow the link today).
  • planTicketPrompt and planAgentFor: the sentence "Create tickets/.plan.md" is the caller's queue convention (its presets write it, its dashboard dedupes on it), and the author lookup needs AgentMeta records.
  • ticketIssueRef use in cli.ts:1068 — the (fix #N) PR title; the parser moves, the title rule stays.
  • readTicketsMeta (tickets/meta.json, the GitHub-import feature's stamp, not part of the format).
  • runTodoLoop, ticketForPrompt, the sweep (auto-pm.ts), routine-locks.ts, agent-archive.ts, archived-agent-patch.ts, worktrees.ts' delete-session write — all through skill-branches' file branch, on agent-logs.
  • prompts/triage_scope.md: a rule about the caller's triage presets ("you only queue work"), not about the format; its one file name becomes "the queue".
  • branch_yourself.md untouched.

Loading the skill into agents — today's link and tomorrow's commit:

  • Owned checkouts (daemon-started, today): createCheckout/attachCheckout in skill-branches take skills?: { name, dir }[] and linkSkill links each beside its own (.claude/skills/tickets, .agents/skills/tickets → the tickets package's SKILL_DIR, excluded from git). The daemon passes [{ name: 'tickets', dir: SKILL_DIR }] and prepends both CLI_BIN_DIRs to the child's PATH. ~15 lines in skill-branches, labelled temporary: dies when the skill is committed into the repo by use-npm-skills (a committed .claude/skills/npm-gemstack-skill-tickets/ is in every worktree because it is a tracked file).
  • Non-owned agents (terminal, Actions, cloud): the counterpart of branch_yourself.md: a tickets_yourself.md (~6 lines: the branch, git fetch + git show tickets:<file> to read, commit on the branch pushed straight with rebase-retry to write, a claim is tickets/<stem>.lock.md holding CLAIMED: <your branch>, conflict = back off) plus the package's SKILL.md injected verbatim for the formats — generated from the package at build like the old BRANCHES_SKILL was, so the formats exist in one file. Appended only for !ownedCheckout, exactly where BRANCH_YOURSELF is (system-prompt.ts:242). Labelled temporary, dies with the committed skill and a PATH answer for the command (shared open point with branches: npx tickets … needs the package on npm — publish is HELD).
  • prompts/README.md, FEATURES-SPEC.md (lines 94–107 Tickets, 125, 131–143 Autonomy: add the tickets skill line beside the branches one at 112–113; "Cross-machine ticket claims" line names the holder rule).

Also changed by the switch: auto-pm.ts pinned prompts (:284, :326) — "see 'The data branch'" → "use the tickets skill"; "remove x.md, x.plan.md, x.lock.md" → tickets close x.md; "write the plan and delete the lock in the same commit" → tickets put x.plan.md then tickets release x.md (two commits; the lock lifts a second later). daemon-services.ts:321 check-off → removeQueueEntry (see queue decision). layout.ts imports. scripts/gen-prompts.mjs unchanged (it globs).

Queue done = delete (the format). queue done and the daemon's promote remove the line; checkOffEntry goes; parseQueueEntries keeps accepting - [ ]/- [x] on read (the file on origin is all [x]; one hand-clean of TODO_AGENTS.md on origin after the merge). The archive is the record of what ran.

4. Presets that change text

file today after
drain_queue.md "Open TODO_AGENTS.md … check the entry off" "tickets queue, work the first entry only; when published, tickets queue done "<entry>"" (the daemon's pinned drain still says: do not, the daemon retires it)
plan_tickets.md tickets/<TICKET>.md without .plan.md and .lock.md, "add to TODO_AGENTS.md" tickets list → unplanned, unlocked; tickets queue add "Create tickets/<TICKET>.plan.md" --priority N
update_tickets.md reads tickets/meta.json, writes files, "the whole update as one commit" tickets list, tickets put <file> per ticket, tickets close, tickets put meta.json; "one commit" dropped — one commit per file (cost: N pushes on a first import)
suggest_new_tickets.md "Suggest new tickets" + "write each with tickets put, in the ticket format the tickets skill gives"
suggest_new_features.md "existing tickets/", "new ticket under tickets/, following the ticket format" tickets list, tickets put
suggest_tickets_to_work_on.md "Add approved tickets to TODO_AGENTS.md" tickets queue add "<title>" --ticket <file> (the link + priority placement the dashboard writes)
triage_quick.md, triage_consensual.md "Look at tickets/*", "Add tickets to TODO_AGENTS.md" tickets list (effort/uncertainty are in the JSON), tickets queue add … --ticket
triage_scope.md "the only file you change is TODO_AGENTS.md" "the only thing you change is the queue, through tickets queue add"
maintenance.md, market_research.md, on_before_mergeable_prompt.md, system_prompt.md (TODO_FILE) "add … to TODO_AGENTS.md" tickets queue add "<entry>" --priority N
preset-catalog.ts tooltips "queue (TODO_AGENTS.md)" unchanged words are fine; "TODO_AGENTS.md" stays the file's name

The presets keep naming the skill explicitly ("use the tickets skill") because a skill's body only loads when invoked — a triage that never invokes it would write the old format from memory.

5. Risks, and the dogfood

Risks

  1. The formats leave every agent's context. Today they are always in the system prompt; after, only agents that invoke the skill see them. Mitigation: every ticket/queue preset names the skill (§4); the dogfood checks that a plan agent invoked Skill tickets before writing. If Opus skips the skill, the fallback is the caller's decision, not a silent inline.
  2. Cloud agents until the skill is committed. Auto-PM's default target is Claude web; cloud plan agents write plans today. The tickets_yourself.md + injected SKILL.md bridge keeps them working; it is the same kind of text branch_yourself.md already is, and it is deleted when use-npm-skills commits the skill into the repo. Labelled temporary in code and PR body so it never reads as the design.
  3. The detached write checks out the whole branch. With the archives on agent-logs the tickets branch is small (tickets + queue); measure once in the dogfood anyway.
  4. One commit per put on a first GitHub import (50 pushes). Acceptable for a routine; if not, put grows a multi-file form later — not in this PR.
  5. skill-branches API grows a skills list to link a second skill — temporary, but it is the branches package learning about other skills. Alternative: the daemon links the tickets skill itself after createCheckout (a copy of linkSkill, 30 lines, in the caller). Recommendation: the list; one mechanism, labelled temporary.
  6. Two runners in file-branch.ts (persistent funnel for the daemon, detached write for the command). The obvious question is "why two". Answer in the SPEC: the daemon has a checkout the dashboard reads from and a serial in-process writer; a command is a one-shot remote writer in any clone; the ops are shared, only the checkout differs.
  7. The queue file on origin is all [x], one lock names a cloud session, and agents-data has to become tickets + agent-logs — all by hand at cutover (the PR body's "on every machine": git push origin origin/agents-data:refs/heads/tickets, a commit removing agents/, agent-logs born from the archives, delete agents-data; per machine remove the old checkout and root link, restart the daemon) — on every machine that ran the daemon, since an old daemon would resurrect agents-data.

Dogfood (the rig from #1739):

  • Rig in the session scratchpad: remote.git (bare) + dogfood-repo clone with a couple of tickets pushed on tickets; XDG_CONFIG_HOME=<rig> with ln -sfn ~/.config/gh <rig>/gh; daemon on :4201 started from inside the rig repo, from the PR worktree's dist, unsandboxed; RPCs need an origin: header; sendAddProject, prefs target: local, model: opus, handoff: local.
  • Checks, each with a positive signal (a rig of zeros proves nothing):
    1. ls .branches/agent-<id>/.claude/skills/ shows branches and tickets; which tickets inside the agent's shell resolves.
    2. A hand-started agent with the [Plan tickets] preset: the transcript shows Skill {"skill":"tickets"}, then tickets list, then tickets queue add … --priority N; git log remote.git tickets shows queue add: … authored by the agent's git identity; TODO_AGENTS.md on the remote has the entry in its ## Priority N section.
    3. The sweep's fan-out: two plan agents, two claim … commits whose holders are the two agent ids; onTickets RPC shows lockedBy = those ids and the dashboard rows link to the agents' pages; each agent's transcript shows tickets put <stem>.plan.md then tickets release; the locks are gone on the remote.
    4. A race: run tickets claim for the same ticket from two clones back to back — the second exits 1 with {"ok":false,"reason":"claimed","holder":…}; the remote has exactly one lock commit.
    5. A drain: the daemon's promote removes the line (not [x]); the queue on the remote is shorter by one.
    6. Non-owned path: claude -p in a plain clone of the rig repo (no daemon) with the [Suggest new tickets] preset — the injected bridge is visible in the system prompt, the agent writes a ticket with git and pushes it to tickets (the branch ends with a new tickets/<date>_<slug>.md).
    7. pnpm test in packages/skill-tickets (moved tests, plus the detached write against a bare remote and a claimed refusal), packages/framework (the remaining suites), root typecheck; then the live :4200 daemon rebuilt and restarted, one real plan routine on gemstack.
  • Cutover on every machine (PR body, hand-run): the branch split from risk 7, release the stale lock, empty the [x] lines from TODO_AGENTS.md, restart the daemon on the new build.
  • One more check: an archive lands on agent-logs after a run settles (git log remote.git agent-logs), and the routine lock file appears there during a triage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions