diff --git a/packages/agent-data/DECISIONS.md b/packages/agent-data/DECISIONS.md index f8a7d94d1..d367e4661 100644 --- a/packages/agent-data/DECISIONS.md +++ b/packages/agent-data/DECISIONS.md @@ -2,83 +2,75 @@ Non-obvious decisions only, grouped by business-logic flow. Anything not listed to the implementer's judgment. Flag conflicts instead of silently deviating. ## The package -- A **library, not a skill**: no `SKILL.md` and no command, because only code uses it. - Skills import this library; they never import each other. -- Package name = branch name: `@gemstack/agent-data` is the library for the `agent-data` - branch, but no function here hardcodes it: every one that touches a branch takes the - branch as an argument. -- `.branches/` holds a project's persistent checkouts, one directory per branch: the - agents' own, made by other packages, and the data branch's, made here. The directory - name is exported; the branch a caller names is checked out at `.branches/`. It - starts with a dot so a `*` glob skips it: every checkout inside is a full copy of the - project, and a type-checker or test runner that descends into N copies runs N times. - Hidden through `info/exclude`, never a committed `.gitignore`: the library must not - touch the project's tracked files. Writing the rule is best-effort: a git dir it cannot - write to still leaves the checkout standing. The rule goes in the common git dir: a - per-worktree `info/exclude` is never read, and one line there covers every checkout. A - checkout deleted by hand leaves git's registration behind: prune before adding, or the - add fails on the stale registration. -- Every git call has a time budget by subcommand: a read 10s, network and `worktree add` - 120s, everything else 30s. `worktree` goes by its second word (`add` slow, `list` a - read, the rest a write) and `branch` by its own words (bare or with a listing flag it - reads; `-D`, `-m` or a new branch name writes); an unlisted subcommand pays the write - budget rather than risk cutting a write short. A killed `push` may have half landed, so - a timeout is reported as a timeout, never as a rejected push. +- A **library, not a skill**: no `SKILL.md`, no command. Skills import it; they never import + each other. +- Named for the `agent-data` branch, but no function hardcodes it: every one that touches a + branch takes it as an argument. +- `.branches/` holds a project's persistent checkouts, one per branch: the agents' + own (made by other packages) and the data branch's (made here). The directory name is + exported. Dotted so a `*` glob skips it: each checkout is a full copy, so a tool that + descends does N times the work. +- Hidden through the common git dir's `info/exclude`, never a committed `.gitignore`: the + library must not touch tracked files; a per-worktree `info/exclude` is never read, and + one line there, written once, covers every checkout. Best-effort: the checkout stands + even when the rule could not be written. Prune before adding: a hand-deleted checkout + leaves a registration that fails the add. +- Every git call has a time budget: a listed read 10s, network and `worktree add` 120s, + other writes 30s. `worktree` goes by its second word (`add` slow, `list` a read, the rest + a write), `branch` by its flags (bare or a listing flag reads; `-D`, `-m` or a new name + writes); an unlisted subcommand gets the write budget, so a write is never cut short at + the read budget. A killed `push` may have half landed, so a git call that outruns its + budget fails as a timeout, its own error kind, never as a plain failure. git's output + buffer is 16 MB (a large checkout's file listing); an overrun is not a timeout. ## The branch -- A branch of the project's repository holds the agents' data — tickets, the queue — like - `gh-pages` holds a site; code branches hold only code. Pushed by every write, and pulled - independently of any write, so a machine that writes nothing still ends up with what the - others pushed. -- One branch for all skills, each with its own folder or file on it. Not one branch per - skill: every extra branch would need its own checkout on disk and its own sync failure - to report. -- A branch missing locally is adopted from origin's copy; one that exists nowhere is - created as an orphan branch, empty and with no parent commit, so no code commit is ever - in its history. -- The branch name is written once, in `names`, as `DATA_BRANCH`; every other package - imports it. `names` is its own entry point and has no node imports, so browser-side code - can import it without the git code. -- Read from anywhere in the repository, no checkout needed: a file comes from the - persistent checkout under `.branches/` when there is one, else the local branch, else - origin's copy; a directory listing always comes off a ref. A read can ask for a fresh - copy: fetch, then read origin's ref instead of the checkout, for a reader whose checkout - may trail what others pushed. A one-shot command opens the branch once: one fetch, then - every read off that one ref (origin's copy, or the local branch when origin has none); - it reads origin's copy because its own writes go straight to the remote and never move - the local branch. A read never fails: a missing file, a missing branch and a git that - could not run all read as absent, so a caller cannot tell an unreachable store from an - empty one. +- A branch of the project's repository holds the agents' data (tickets, the queue) the way + `gh-pages` holds a site; code branches hold only code. Every write that changes something + pushes, and a pull is a cycle of its own, so a machine that writes nothing still gets what + the others pushed. +- One branch for all skills, each with its own folder or file. Not one per skill: every + extra branch needs its own checkout and its own sync failure to report. +- Missing locally, it is adopted from origin's copy; missing there too, it is born an orphan + branch, so no code commit is ever in its history. +- The name is written once, in `names`, as `DATA_BRANCH`, and imported everywhere else. + `names` is its own entry point with no node imports, so browser code can import it. +- A read works from anywhere in the repository, an agent's worktree included: the checkout + is looked for beside the real `.git`. A file is looked for in that checkout when there is + one, then on the local branch, then on origin's copy; a directory listing always comes off + a ref. A read can ask for a fresh copy: fetch, then origin's ref instead of the checkout, + for a reader whose checkout may trail. A one-shot command opens the branch once: one fetch + when there is an origin, every read off origin's copy (the local branch when there is no + `origin/`), because its own writes go straight to the remote and never move the + local branch. A read never fails: a missing file, a missing branch and a git that could + not run all read as absent. ## Flow: a write Fetch what others pushed → make the change → commit → push. - Two writers. A long-lived process (a daemon) writes in its own checkout, - `.branches/`, one write at a time; the pull takes its turn in the same queue: it - is that same cycle with an empty change. A command an agent runs writes in a throwaway - worktree outside the project, at the remote's tip (parentless when origin has no such - branch yet), pushes, and deletes it whether or not the push landed; it never touches the - process's checkout: that checkout's next write commits everything it finds there, and a - failed write resets it, so a second writer's files would land in the wrong commit or be - wiped. A command's write races at the push like the process's, twice in all; a push that - still fails throws, and nothing is left behind to retry. The process's write never - throws: its callers are background ticks. -- A write is a re-runnable function, not a finished commit: a lost race just runs it again - on the new files. The commit message is the caller's too: fixed, or a function run after - the change, since a write that batches several edits only knows what it did once it is - done. The lost attempt's commit is wound back first, so the change lands once and not - twice. Never a force push. After two failed pushes the write reports the failure and the - commit stays local in the process's checkout. The next write or pull rebases it onto - what the remote has by then and pushes the stranded commit together with the new one; - when the rebase conflicts, the checkout is reset to origin's tip and every unpushed - commit goes with it: the remote wins, only the current change runs again, and what was - dropped is never reported. -- An op is handed a directory and writes into it as it likes; `BranchFileFs` is the file - seam an op can take instead of the disk (the type and its node implementation ship here, - the op does the injecting, so it is testable off disk), and it creates parent - directories: git keeps no empty directory, so a skill's folder is gone with its last - file and absent on a branch just born. -- The remote is always `origin`, and a repository without one counts as remote-less - whatever other remotes it has: the process's write commits locally and reports no error; - a command's write refuses, as an outcome it returns, not as a throw; the pull reports an - error: it has no remote to converge with. + `.branches/`, one write at a time per repository and branch, an in-memory lock: + two processes on one clone are not guarded. The pull is that same cycle with an empty + change, behind the same lock. A command an agent runs writes in a throwaway worktree + outside the project, at the remote's tip (parentless when origin has no such branch), + pushes, and deletes it whether or not the push landed. It never touches the process's + checkout: that checkout's next write commits everything it finds, and a failed write + resets it, new files included. Both try the push twice; the command's write then throws + with nothing left to retry, the process's never throws: its callers are background + ticks. +- A write is a re-runnable function, not a finished commit: a lost race winds the attempt's + commit back and runs the function again on the new files, so the change lands once. The + message is the caller's: fixed, or a function run after the change, since a batch only + knows what it did once done; the library's own are `create the branch` for the + birth and `sync` for the pull. Never a force push. After two failed pushes the process's + write reports the failure and the commit stays local in its checkout; the next write or + pull rebases it onto the remote and pushes it with the new one. When that rebase conflicts + the checkout is reset to origin's tip: the remote wins, every unpushed commit is dropped + unreported, only the current change runs again. +- An op is handed a directory and writes into it. `BranchFileFs` is the file seam an op can + take instead of the disk, for tests (type and node implementation ship here, the op + injects it); it creates parent directories: git keeps no empty directory, so a skill's + folder vanishes with its last file and is absent on a new branch. +- The remote is always `origin`; a repository without one is remote-less whatever other + remotes it has. Then the process's write commits locally and reports no error, a command's + write refuses (an outcome, not a throw), and the pull reports an error: nothing to + converge with. diff --git a/packages/skill-branches/DECISIONS.md b/packages/skill-branches/DECISIONS.md index bc1598cb0..3ed3684a0 100644 --- a/packages/skill-branches/DECISIONS.md +++ b/packages/skill-branches/DECISIONS.md @@ -2,92 +2,149 @@ Non-obvious decisions only, grouped by business-logic flow. Anything not listed to the implementer's judgment. Flag conflicts instead of silently deviating. ## The checkout -- One checkout per agent: a git worktree of the user's repository under `.branches/`. A - worktree, not a clone, so every checkout shares the repository's objects and refs. - Agents run in parallel, and the user's own copy is never an agent's workspace. -- `.branches/` is hidden from the project's git through the repository's own exclude file, - from the first checkout on: an untracked folder at the root would ride a sweeping `git - add -A` onto a code branch, and the hiding must not touch a tracked file, which rules - `.gitignore` out. -- A checkout starts as branch `agent-` in folder `.branches/agent-/`; `` is - what the program that starts the agent calls it, restricted to `[A-Za-z0-9_-]+` so no id - can build a path outside `.branches/`. When the agent names itself, through the command - (`npx branches name `), the branch is renamed to `agent-`; the folder keeps - the id. A rename, not a new branch, so no empty branch is left behind. The folder is not - renamed, because the agent is running inside it. -- After a rename, a link named as the new branch is put beside the folder, so - `.branches/agent-` reaches every checkout by its current branch; the package makes - the link and removes it when the checkout goes. -- Branch names are `agent-`, with no `/`: the folder, and the link beside it, are - named after a branch, a name on disk cannot hold a slash, and a slashed ref cannot be - handed to a hosted run as its starting revision. The package renames and deletes only - `agent-*` branches; the user's own branches are never touched. -- `agent-data` is not an agent's: it is the data branch of `@gemstack/agent-data`, checked - out beside the agent checkouts as `.branches/agent-data`. The package never lists, - renames or deletes it, `data` is refused as an agent id, and an agent naming itself - `data` gets `agent-data-2`. +- One checkout per agent, a git worktree of the user's repository under `.branches/`, + branched from the project's head unless the caller names a base. Agents run in parallel, + and the user's own copy is never an agent's workspace. A worktree, not a clone, so every + checkout shares the repository's objects and refs. +- The repository's exclude file hides `.branches/` from the first checkout on: an + untracked folder at the root would ride a sweeping `git add -A` onto a code branch, and + `.gitignore` is tracked. +- A checkout starts as branch `agent-` in folder `.branches/agent-/`; `` comes + from the program that starts the agent, and must match `[A-Za-z0-9_-]+`, so no id can + build a path outside `.branches/`. `npx branches name ` (`[a-z0-9-]+`; the skill + asks for a leading letter or digit: at the command line a leading `-` reads as a flag, a + usage error, and the name check itself does not mind it) renames the branch to + `agent-`: a rename, not a new branch, so nothing is left behind; the folder keeps + the id, since the agent is running inside it. A checkout on no branch is neither renamed + nor reclaimed; `status` answers it without a `branch`. +- After a checkout is made, named or removed, each checkout whose branch differs from its + folder name gets a sibling link `.branches/` to its folder, relative; a detached + checkout or a slashed branch gets none. A link whose target is an `agent-*` name, + `agent-data` aside, is the package's to remove, whatever it is called, and whether or + not the target exists; anything else at a link's path is left alone. `list` and `prune` + see directories only, so a link is never a checkout; a session name passes as an id, so + `remove ` follows the link `.branches/agent-` to the checkout. +- No name the package mints holds a `/`: a folder and a link are named after a branch, and + a cloud session (a hosted agent run, started on a branch) cannot start on a slashed ref. + The package renames and deletes only `agent-*` branches. +- `agent-data` is `@gemstack/agent-data`'s data branch, checked out as + `.branches/agent-data` by the program that keeps it, not by this package. Never listed, + renamed or deleted; `data` is refused as an id, and an agent naming itself `data` gets + `agent-data-2`; `attach` guards the id only, never the branch it is given. - A taken name gets `-2`, `-3`, … instead of a refusal: the agent asked for a name and - reads back the one it got. Taken means any local or remote-tracking branch of that name, - so the later push cannot land on someone else's branch; never the checkout's own branch, - so asking again for the name it already carries changes nothing. Two agents naming the - same thing at once race on the rename; the loser takes the next suffix. -- Continuing an agent puts it back on the branch its work is on; a branch that is gone is - recreated from the project's head, since the only branch the package deletes is one that - held nothing past what the remote already had. -- The user's installed dependencies are linked into the checkout, not copied and not - reinstalled. One link per entry of the folder, not one link to the whole folder, so an - install in the checkout writes into the checkout; a scope (`@acme`) is one entry, so a - scoped install still reaches the user's folder. Every dependency folder down to two - levels under the root is linked, not only the root's, so a workspace package's own - dependencies are there too. No dot-entry of the folder is linked except `.bin`: the - agent runs the project's tools. The package manager's own state (`.pnpm`, - `.modules.yaml`) is left out: it says the tree it sits in was installed there, which the - checkout's was not; the packages resolve without it, a link to a link resolving where - the target lives. + reads back the one it got. Taken means any local or remote-tracking branch, so the later + push cannot land on someone else's branch. The branch the checkout carries right now, + suffix included, is not counted: a checkout already on `agent--2` that asks for + `` again keeps `-2` while `agent-` is still taken, and takes `agent-` + once it is free. Two agents naming the same thing at once race on the rename; the loser + takes the next free suffix, in at most three tries, then `git-failed`. +- Continuing an agent puts it back on the branch its work is on, even one the package did + not make; a branch gone locally comes back from origin's copy, and one gone everywhere + is recreated from the project's head: every branch the package deletes held nothing the + remote lacked. +- The user's installed dependencies are linked into the checkout, not copied or + reinstalled: one link per entry of the folder, absolute, so an install in the checkout + writes into the checkout (a scope like `@acme` is one entry, so a scoped install still + writes into the user's folder: a known limit). Every dependency folder down to two + levels under the root (not under `node_modules`, `dist`, `build`, `coverage` or a + dot-directory) is linked, so a workspace package's own dependencies are there too; a + tree already in the checkout is left alone. Of the dot-entries only `.bin` is linked, so + the agent runs the project's tools; the others (`.pnpm`, `.modules.yaml`) would tell the + package manager the checkout's tree was installed there, which it was not. The packages + still resolve: a link to a link resolves where the target lives. +- Everything after the worktree is best-effort: a checkout missing any of it is a worse + run, not a failed one. +- `create` or `attach` for an id that already has a checkout fails as `git-failed` with + git's own error, and so does `create` when the branch exists without one: `attach` is + the way then. `create` and `attach` each answer the path and the branch; `list` answers + one row per checkout in directory order: the id, the path, the branch and, asked for, + the size. Only a directory named `agent-` counts as a checkout. ## Flow: reclaim -Deleting an agent's checkout to free the disk. It goes only once everything in it is on -the remote, so deleting it can lose nothing; the reclaim pushes the branch itself when the -program allows a push. +Deleting an agent's checkout to free disk, only after the remote has everything in it. It +pushes the branch the checkout ended on, the user's own included, when the caller allows a +push. -- Nothing is committed on the agent's behalf: a checkout with uncommitted work is kept - until a person commits or deletes it. -- An `agent-*` branch whose commits have already reached the remote through another - branch, as after a merge, holds nothing of its own and is deleted with its checkout. -- A checkout whose tip is inside a pushed commit the program names — the commit a cloud - session pushed on the agent's behalf — goes without a push and keeps its branch. +- Nothing is committed on the agent's behalf: a checkout with uncommitted work, untracked + files included, is kept until a person commits or deletes it, and nothing of it is + pushed. +- An `agent-*` branch whose tip is reachable from another name's remote-tracking ref, on + any remote, holds nothing of its own (the holds-nothing rule): it goes with its checkout + unpushed, deleted with `-D`: git's own merged test asks the wrong question. Its own + copy, under its current name or its upstream's, does not count. Pushed means on + `origin`, the only remote the package pushes to. Both reads take the local + remote-tracking refs, never a fetch: the push that put a tip there wrote them. +- The caller may name a pushed commit through the library, not from the command line: the + commit a cloud session pushed on the agent's behalf. A checkout whose tip is an ancestor + of it goes without a push and keeps its branch, even one the holds-nothing rule would + delete. - An agent that switched to another branch leaves `agent-` behind; it goes with the checkout once the branch the agent ended on contains it. -- A folder under `.branches/` that git no longer knows as a worktree is left alone: git - run inside it would act on the user's own checkout. -- The package does git and the filesystem, nothing else. Anything else it needs to know, - like whether it may push, the program using it passes in; the package never reads that - program's files. +- A folder under `.branches/` that git no longer knows as a worktree is left alone, and + `list` still shows it, without a branch: a git command run inside it would act on the + user's own checkout. +- A removal git refuses as unclean after the clean check passed is forced, and says so on + stderr: an ignored build artifact must not strand a checkout for good. +- `remove` and `prune` push by default; `--no-push` opts out. `remove` of a missing + checkout is a refusal, `no-checkout`; a removal judges the birth branch before anything + goes, then removes the checkout, then the branches, since git will not delete a branch a + worktree has out. `remove` names the branches that went with it, absent when none. + `prune` lists only the ids it removed, and the checkouts it kept, each with its reason, + in its result, nothing on stderr but the forced-removal line, and exits 0. +- The package reads no configuration and never asks whether an agent still runs: the + caller says whether it may push, and may pass a hook that runs just before the checkout + goes, to stop whatever serves the tree; the command line passes no hook. ## The skill - The agent commits and stops: it never pushes, opens a pull request, or merges. Whoever started it does that. -- The skill says `npm install`, then `npx branches`, never a bare `branches`: on a fresh - clone no such command exists yet. -- One JSON document on stdout for every command that runs: the result, or the refusal. A - refusal, a rule saying no, adds one line for a person on stderr and exits 1; an argument - that cannot be read never gets that far: the usage on stderr, nothing on stdout, exit 2. - A malformed command line (an unknown flag, the wrong argument count) is that usage - error; an id that parses but is not an agent id is an ordinary refusal, `invalid-id`. -- Anything a command throws is reported like a refusal, reason `git-failed`, with the - error's own line on stderr: a caller parsing stdout never has to handle a command that - printed nothing. -- `list` answers with a bare JSON array when it runs; every other result, and every - refusal, is an object whose `ok` tells the two apart. -- Run outside a repository, a command refuses with `not-a-repo`: only git's own "not a git - repository" reads as that, every other git failure stays `git-failed`. -- An agent reading the skill can be in one of two places: inside a checkout the program - that started it made for it, already on an `agent-*` branch; or in a plain clone of the - repository, on `main` or on someone's branch. The skill tells them apart by the branch - name alone: on `agent-*`, the checkout is the agent's; on anything else, the agent makes - its own `agent-` branch with git before its first change. -- Each agent tool (Claude Code, Codex) looks for skills in its own folder at the root of - the checkout: `.claude/skills`, `.agents/skills`. The package links its own folder, - where `SKILL.md` sits, into each of those as `branches` in every checkout it makes, and - hides the links from git. A caller may name further skills to be linked in beside it, - each under its own name; temporary, until the project commits its own skill files. +- Before its first change the agent names its session, saying what the work is, unless its + branch already differs from its folder name, as a continued agent's does: it is already + named. The agent finishes only when `npx branches status` reports the checkout clean, or + after saying what remains is not its own; an agent that needs anything outside its + checkout stops and says so. +- The skill says: when `node_modules` is missing, install with the lockfile's package + manager, then `npx branches`, never a bare `branches`: on a fresh clone no such command + exists yet. +- Every command that runs prints one JSON document on stdout: the result or the refusal. A + refusal (a rule saying no) adds one line for a person on stderr and exits 1. A malformed + command line (an unknown flag, the wrong argument count) never gets that far: the usage + on stderr, nothing on stdout, exit 2. An id the charset rejects is a refusal, + `invalid-id`, not a usage error; one starting with `-` reads as a flag, a usage error, + unless the arguments follow `--`. +- A command that throws is reported like a refusal, reason `git-failed`, the error's own + line as `detail` on stdout and on stderr. +- `create`, `attach`, `list`, `remove` and `prune` act on the project, found from the + `.branches/` layout even from inside a checkout; `name` and `status` act on the checkout + the command runs in, found from anywhere inside it. `status` also takes the path of a + checkout root. `status` answers the path, the branch, whether the tree is clean, and + whether the tip is on the remote. +- The keys: `agentId`, `path`, `branch`, `clean`, `onRemote`, `sizeBytes`, `detail`, + `removed`, `skipped` (each with `agentId`, `reason`, and the person's line as `detail`), + `branchesDeleted`. +- `list` answers with a bare JSON array; every other result and every refusal is an object + whose `ok` tells the two apart. +- Outside a repository, a command that needs one refuses with `not-a-repo`: only git's own + "not a git repository" reads as that; every other failure stays `git-failed`. An id is + checked before the repository, a session name after it, and `status ` skips the + repository check, so outside one it answers `not-a-worktree`. +- A refusal names its subject: `invalid-id` and `no-checkout` the id, `status`'s + `not-a-worktree` the path, `dirty` and `not-on-remote` the branch, `not-on-remote` also + git's reason when a push was tried; `name`'s refusals, `not-a-repo`, and + `not-a-worktree` and `no-branch` from `remove`, carry the reason alone. The refusals: + `invalid-id`, `invalid-name`, `not-a-worktree`, `no-branch`, `not-an-agent-branch`, + `no-checkout`, `dirty`, `not-on-remote`, `not-a-repo`, `git-failed`. +- The skill tells the agent where it is: on `agent-*` the checkout is its whole workspace, + and the dependency files and skill folders in it are links to the user's copies, never + edited; on any other branch under `.branches/` it was continued on that branch on + purpose and stays; anywhere else it is a plain clone, and the agent makes its + `agent-` branch with git before its first change, another name if that one exists + locally or on origin. `status` and `name` are the agent's commands; the rest are the + caller's. +- Each agent tool (Claude Code, Codex) looks for skills in its own folder at the checkout + root: `.claude/skills`, `.agents/skills`. In every checkout it makes, the package links + its own folder, which holds `SKILL.md`, into both as `branches`, hidden through the + repository's exclude, whose entry also hides an untracked project file at that path. An + entry already there, a committed skill say, is left alone. A caller may name further + skills to link in beside it, each under its own name, not from the command line; + temporary, until the project commits its own skill files. diff --git a/packages/skill-branches/SKILL.SPEC.md b/packages/skill-branches/SKILL.SPEC.md index 32b7db281..957f0b49b 100644 --- a/packages/skill-branches/SKILL.SPEC.md +++ b/packages/skill-branches/SKILL.SPEC.md @@ -6,13 +6,13 @@ The skill's instructions: what an agent is told about where its work goes, for e ## Business logic — TL;DR -- **One rule wherever the agent is** - the work goes on a branch named `agent-`, and whoever started the agent publishes it: the agent never pushes and never opens the pull request. -- **The command is installed, then run through npx** - `branches` ships with the `@gemstack/skill-branches` package the repository depends on; the agent installs the repository's dependencies once and runs `npx branches`, so every command the skill names runs as written on a fresh clone. -- **Where the agent is, read off its branch** - `npx branches status` prints the branch. One starting with `agent-` is the agent's own; any other means a plain clone, on a branch that is not the agent's. -- **On its own branch, the working directory is the whole workspace** - every file the agent reads or writes is under it, addressed relative to it; when the directory sits under `.branches/`, a checkout was made for the agent and the repository around it is the user's own working tree, never the agent's to edit; anything it genuinely needs from outside is a reason to say so and stop. Before the first change it names the session with `npx branches name ` — a rename of its branch to `agent-`, so the commits stay, suffixed when the name was taken. -- **In a plain clone, the agent makes its branch itself** - before the first change, `git switch -c agent-`. -- **Commit as you go** - only what the agent committed is ever published; nothing is committed on its behalf, and uncommitted work is neither published nor cleaned up. -- **Leave a clean tree** - before finishing, `npx branches status` must report a clean tree. +- **One rule wherever the agent is** - the work goes on a branch named `agent-`, unless the caller continued the agent on another, and whoever started the agent publishes it: the agent never pushes and never opens the pull request. +- **The command is installed, then run through npx** - `branches` ships with the `@gemstack/skill-branches` package the repository depends on; the agent installs the repository's dependencies once, when there is no `node_modules` yet, with the package manager the lockfile belongs to, and runs `npx branches` inside its checkout, so every command the skill names runs as written on a fresh clone. `status` and `name` are the agent's commands; the rest are the caller's. +- **Where the agent is, read off its branch** - `npx branches status` prints the branch. One starting with `agent-` is the agent's own; any other means a plain clone, on a branch that is not the agent's, unless the checkout sits under `.branches/`: then the agent was continued on that branch on purpose and stays on it. +- **On its own branch, the working directory is the whole workspace** - every file the agent reads or writes is under it; dependency files and the skill folders are the user's copies, linked in, and are never edited; anything it genuinely needs from outside is a reason to say so and stop. Before the first change it names the session with `npx branches name ` — a rename of its branch to `agent-`, suffixed when the name was taken, printed as `branch`, another name when refused as invalid — unless the branch already differs from the checkout's folder name, which a continued agent's does: then it is named and kept. +- **In a plain clone, the agent makes its branch itself** - before the first change, `git switch -c agent-`, another name if that one exists locally or on origin; the same workspace rules apply. +- **Commit as you go** - nothing is committed on the agent's behalf, and only what it committed is published. +- **Leave a clean tree** - before finishing, `npx branches status` must report a clean tree: nothing uncommitted and nothing untracked, so the agent commits or deletes what it added; what remains and is not its own it reports and finishes. ## Before modifying/creating SPEC.md files diff --git a/packages/skill-branches/SKILL.md b/packages/skill-branches/SKILL.md index 164093266..5a0c40505 100644 --- a/packages/skill-branches/SKILL.md +++ b/packages/skill-branches/SKILL.md @@ -5,11 +5,11 @@ description: Where your work goes (a branch named agent-), how to name it, # Branch management -Your work goes on a branch named `agent-`. Whoever started you publishes it — push, pull request, merge — so you never push and never open the pull request yourself. +Your work goes on a branch named `agent-`, unless whoever started you continued you on another. Whoever started you pushes, opens the pull request, and merges. You never do. ## The command -`branches` comes with the npm package `@gemstack/skill-branches`, a dependency of this repository. Install the repository's dependencies once — `npm install`, or the package manager its lockfile belongs to — then run it as `npx branches`. +`branches` is a dependency of this repository (`@gemstack/skill-branches`). If `node_modules` is missing, install with the lockfile's package manager (`npm install` for `package-lock.json`). Then run `npx branches` inside your checkout. `status` and `name` are yours; the rest are the caller's. ## Where you are @@ -19,25 +19,27 @@ npx branches status It prints JSON; `branch` is the branch you are on. -**A branch starting with `agent-`.** The branch is yours, and the checkout it is in is your whole workspace: every path you read or write is inside it. When the checkout sits under a `.branches/` folder, it was made for you and the repository around it is the user's own working tree, never yours to edit. If something you need is outside your checkout, say so and stop. +**A branch starting with `agent-`.** Its checkout is your whole workspace: read and write only there. Dependency files and skill folders are links to the user's copies: never edit them. If something you need is outside your checkout, say so and stop. -Before your first change, name the session — `[a-z0-9-]+`, saying succinctly what the work is: +Before your first change, name the session with `[a-z0-9-]+`, starting with a letter or digit, saying what the work is, unless your branch already differs from `path`'s last segment: then it is named, keep it. ``` npx branches name ``` -It renames your branch to `agent-` — a rename, so your commits stay — and prints the name the branch ended up with: `agent--2`, `-3`, … when `` was taken. +It renames your branch to `agent-` and prints it in `branch`: `agent--2`, `-3`, … when `` was taken; a name outside `[a-z0-9-]+` is refused as `invalid-name`. -**Any other branch.** You are in a plain clone, on a branch that is not yours. Before your first change, create yours and switch to it — `` is `[a-z0-9-]+`, saying succinctly what the work is: +**Any other branch.** If the checkout sits under `.branches/`, you were put on this branch on purpose: stay on it, do not name it. Otherwise you are in a plain clone on someone else's branch. Before your first change, create your own and switch to it, `` as above (another if it exists, locally or on origin): ``` git switch -c agent- ``` +Everything above applies. + ## Commit as you go -Only what you committed is ever published: nothing is committed on your behalf, and uncommitted work is neither published nor cleaned up. +Nothing is committed for you. ## Before you finish @@ -45,4 +47,4 @@ Only what you committed is ever published: nothing is committed on your behalf, npx branches status ``` -It must report `"clean": true`. +It must report `"clean": true`. `clean` is false while anything is uncommitted or untracked: commit or delete what you added; if what remains is not yours, say so and finish. diff --git a/packages/skill-tickets/DECISIONS.md b/packages/skill-tickets/DECISIONS.md index c7a91c0d7..f8b618783 100644 --- a/packages/skill-tickets/DECISIONS.md +++ b/packages/skill-tickets/DECISIONS.md @@ -2,108 +2,163 @@ Non-obvious decisions only, grouped by business-logic flow. Anything not listed to the implementer's judgment. Flag conflicts instead of silently deviating. ## The tickets -- A ticket is a markdown file in `tickets/`. Its plan, and its claim (who is working on - it), are two more files beside it: the ticket's name with `.md` swapped for `.plan.md` - and `.lock.md`. -- Tickets live on `agent-data`, the shared data branch named by `@gemstack/agent-data`, - never on a code branch: no code checkout carries them. The program that keeps the branch - checked out syncs it; that sync links `tickets` at the project root to - `.branches/agent-data/tickets` in the persistent checkout, and only where nothing of - that name already sits. The same sync creates an empty `TODO_AGENTS.md` when the branch - has none, so the queue is a file before the first entry is added. Git is told to ignore - the link with a pair of rules, `/tickets` then `!/tickets/`: the first hides any root - entry of that name, the second un-hides it again if it is a directory, which a symlink - never is. The pair is needed because `.git/info/exclude` is one file for every worktree - of the repository, the data branch's checkout included, whose own `tickets/` folder must - stay committable. -- `tickets/` holds only open tickets: closing one deletes it, with its plan and its claim. -- `list` answers newest ticket first, dated by the `_` its filename carries. A - filename with no date falls back to the file's modification time, which a read off git - does not have: on `list` such a ticket is dated the epoch and sorts last. -- The skill knows no issue tracker: a ticket may carry a `GitHub:` line with its issue, - but importing issues is the program's job. +- Two callers: the command an agent runs, and a long-lived program that keeps the branch + checked out, starts agents through the library, and imports issues with its own code, + stamping `meta.json`. The executable is `tickets`. The package ships `SKILL.md`, the + agent's instructions: where the files live, never to write through the root link, + install, then `npx tickets`. It gives the commands and what they answer; claim before + planning or working, release before stopping unless closed, close once the work is + merged; and the ticket, plan and queue formats, which parts of them the code parses, + with the filename convention `_.md`, the plan's optional sections, its + `Outdated:` key and its rubric for rating uncertainty. +- A ticket is a markdown file in `tickets/`. Its plan and its claim sit beside it: + `.plan.md` and `.lock.md`, `` the filename without `.md`. +- Tickets live on `agent-data`, the branch `@gemstack/agent-data` names, never on a code + branch. The program's sync runs three steps in order. It seeds an empty `TODO_AGENTS.md` + when the branch has none, so the queue exists before its first entry. It links `tickets` + at the project root to the relative target `.branches/agent-data/tickets`, only when + nothing of that name sits at the root; the target may not exist yet, so the link dangles + until the first ticket lands. Then it converges the checkout with origin. A seed that + cannot commit stops before the link; a link that cannot be made is ignored, and the sync + still returns the pull's result. The link is hidden by two rules in `.git/info/exclude`, + `/tickets` then `!/tickets/`, written on the run that creates the link even when the + symlink fails. `/tickets` hides the link, `!/tickets/` re-admits directories, which a + symlink is not, so the persistent checkout still commits its own `tickets/` under the + same repo-wide exclude. +- Closing a ticket deletes it, its plan and its claim, and nothing else; a queue entry + linking it stays until `queue done`. +- `list` sorts newest first by the filename's leading `yyyy-mm-dd_`, ties by filename + ascending; the row's `date` is that day at `T00:00:00.000Z`, unvalidated. A filename + with no date takes the file's modification time, or, when read from git, the epoch, + which sorts last. +- A ticket's row, the same fields in `list` and `show`: the title from its `# ` line, the + summary from the first prose line after `## TLDR`, else after the title, scanning past + headings to the end of the file (a `Source:` line is skipped in either scan, matched + case-sensitively unlike every key: a trailer on imported tickets, not a field), `Topics: + [a, b]` split into tags, `GitHub:` into label and url when it is a full markdown link, + `Priority:` verbatim. Plus whether a plan sits beside it, whether a lock does (`locked`, + present only when true), whom the lock names, and the plan's `Effort:` and + `Uncertainty:`, absent unless a whole number 0-10. Keys and headings match in any case, + `## TLDR` as the whole line; a ticket's keys are read only above the `# ` title, a + plan's above its title or, when it has none, anywhere in its first 4000 characters. + `list` reads the first 4000 characters of a ticket, `show` all of it, so the two can + differ on a long ticket. The library also reads a ticket's issue reference, scanning the + whole file for a `GitHub:` line: the number from the link's URL, `/issues/` or + `/pull/`, a bare `#42` as the fallback. The row's keys, `summary` always present, + empty when there is no prose: `file`, `title`, `summary`, `priority`, `topics`, `github` + (`label`, `url`), `date`, `planned`, `locked`, `lockedBy`, `effort`, `uncertainty`; + `show` nests the row as `ticket` with its text as `content`. + ## Flow: a claim -- A claim is a committed file holding one line, `CLAIMED: `, so that agents on other - machines see it too. +- A claim is a committed file holding one line, `CLAIMED: `, so agents on other + machines see it. - One claim per ticket; it never expires: it lifts when the ticket is released or closed, - and otherwise only by hand on the branch. The command releases and closes as the holder - it reads: it lifts only a lock naming that holder, and closes any ticket nobody else has - locked. The program that started an agent releases what the agent left claimed, naming - either the holder it expects or none; naming none frees whoever holds the lock. -- Releasing a ticket nobody holds is a refusal, not a no-op: a release that lifts nothing - means the claim is not where the caller thought it was. -- A lock is written only by a claim. -- A claim the program's write cycle committed but could not push still counts as claimed: - the commit already guards this machine's readers, and the gap is logged. A cycle that - could not commit at all claims nothing. -- A claim guards planning and working, not writing: `put` overwrites a ticket or its plan - no matter who holds it, so an import can refresh a ticket someone is working. -- A claim on a ticket the same holder already holds succeeds and writes nothing: a re-run - after a lost race must not read its own lock as someone else's. -- The holder's name is never typed; the command reads it from where it runs: `AGENT_ID` - from the environment when the program that started the agent set it, else the current - branch. The id wins because it outlives the branch: a program that renames an agent's - branch mid-session leaves a lock naming a branch no live agent answers to. A checkout on - no branch is refused rather than claiming as `HEAD`. Reading the id off the checkout's - folder name was dropped: that layout belongs to whichever program made the checkout, not - to this skill. -- The program says whether a claim is for planning or for implementing: a claim for - planning is skipped, no lock written, when the ticket already has a plan; a claim for - implementing is not skipped for having a plan: the plan is what it came to implement; - only someone else's lock stands in its way. The command always claims to implement. -- The lock's existence is the claim; the holder it names is only shown. A lock nobody can - read still holds the ticket, and no command lifts it: only a release naming no holder, - or a hand edit on the branch. + otherwise only by hand on the branch. The command lifts only its own lock, and closes + only when the ticket has no lock or its own, `not-holder` otherwise. The program that + started an agent releases what the agent left claimed, naming the holder it expects, or + none to free whoever holds the lock. +- Releasing an unclaimed ticket is a refusal. +- A lock is written only by a claim; someone else's `claim` is refused while it exists. + `put` ignores it, so an import can refresh a ticket someone holds. +- A claim the program committed but could not push still counts: the commit already guards + this machine's readers, and the gap is logged. A cycle that could not commit claims + nothing. The program's release is judged the same way: committed counts, pushed or not. + A queue edit is not: it counts only once pushed. +- Claiming a ticket you already hold succeeds and writes nothing, so a re-run does not + read its own lock as someone else's. +- The holder is never typed: `AGENT_ID` when non-blank, else the current branch. The id + outlives the branch: a rename mid-session would leave a lock naming a branch nobody + answers to. A checkout on no branch is refused, never claimed as `HEAD`. The checkout's + folder name is not read: the layout is the caller's. +- The program says whether a claim is for planning or implementing: a claim for planning + is skipped, no lock written, when the ticket already has a plan, unless the lock is + already this holder's: the lock is checked before the plan; a claim for implementing + ignores the plan; only someone else's lock stands in its way. The command always claims + to implement. The program's claim writes the lock without reading the ticket, so a + ticket closed under it gets an orphan lock. +- The lock's existence is the claim: the holder it names decides who may close or release, + not whether the ticket counts as locked. A lock whose line does not parse still holds + the ticket, and no command lifts it: only the program's release naming no holder, or a + hand edit on the branch. A lock file that cannot be read at all counts as no lock to + `claim`, `close` and `release`, though `list` still shows it locked, since `locked` + comes from the directory listing, not from reading the lock. ## The queue - The queue is one markdown file on the branch, `TODO_AGENTS.md`: sections `## Priority - 10` down to `## Priority 0`, an entry a list item under one of them. The file is kept - sorted high to low as entries are placed; nothing re-sorts on read, so a reader answers - in file order, top first. -- An entry is plain text: the task a future agent is started with. `--ticket` writes it as - a markdown link to the ticket; the ticket is read once as the entry is added, for its - priority and to refuse an entry pointing at no ticket. The program that starts agents - reads that link back, to claim the ticket for the agent it starts on the entry. -- An entry added with no priority is appended at the end of the file, so it lands in - whatever section ends it; one linked to a ticket takes the ticket's priority unless one - was given with it, and 5 when the ticket has none or names one that is not a whole 0-10: - clamping a typo would claim 10 or 0, both reserved ends of the scale. A priority the - file has no section for gets one, placed before the first lower section so the file - stays sorted high to low. A file with no priority section at all gets the new one above - its first `## ` section, so an unranked section cannot bury a deliberate one; a file - with no `## ` section at all gets it appended at the end. An entry joins the end of its + 10` down to `## Priority 0`, any `## Priority N` counts, in any case, N one or two + digits ending at a word boundary; any `-`, `*` or `N.` list item with text is an entry, + wherever it sits. Entries are placed to keep the file sorted high to low; nothing + re-sorts on read. +- An entry is plain trimmed text: the task a future agent is started with. `--ticket` + writes the entry as a markdown link to the ticket, the given text as its label, + unescaped. The ticket is read off the fetched branch before the write, for its priority + and to refuse an entry pointing at no ticket; the program reads the link back to claim + the ticket for the agent it starts. +- `queue add` creates the queue file when the branch has none. An entry with no priority + goes at the end of the file, in whatever section ends it; one linked to a ticket takes + the ticket's priority unless `--priority` was given; a ticket whose `Priority:` is + missing or unreadable counts as 5, never 10 or 0: those ends (act immediately, only if + capacity) are deliberate picks. A priority the file has no section for gets one, before + the first lower section, or after the last priority section when none is lower; a file + with no priority section gets it above its first `## ` section, so an unranked section + cannot bury a deliberate one; a file with no `## ` section gets it appended. An entry + joins its section at the end, before the blank lines; only a `## ` heading ends a section. -- Done means deleted, never checked off: a `- [x]` line is not an open entry, and `queue` - never lists it. +- Done means deleted, never checked off: a `- [x]` or `- [X]` line is not an open entry; a + `- [ ]` line is, printed without its box and deleted whole. ## Flow: the command - The command reads with `list`, `show` and `queue`, and writes with `put` (a ticket, a - plan, or `meta.json`; the bytes as given, unparsed), `close`, `claim`, `release`, `queue - add` and `queue done`. -- A ticket is named to any command by its bare filename or by its `tickets/` path, - so the link a queue entry carries can be pasted straight in. -- No command reads `meta.json`; `put` writes it like a ticket, bytes as given. The - importing program is its only reader, and only for the last-import stamp it keeps there. -- A read fetches the branch from origin once and reads everything from that copy: only - origin is sure to hold what every writer pushed, the command's own earlier writes - included. With no origin, the local branch is read instead: writes are refused there, so - nobody else can have moved it. -- One JSON document on stdout for every command that runs: the result, or the refusal. A - refusal, a rule saying no, adds one line for a person on stderr and exits 1; an argument - that cannot be read never gets that far: the usage on stderr, nothing on stdout, exit 2. - A malformed command line (an unknown flag, the wrong argument count, a `--priority` off - the 0-10 scale) is that usage error; an argument that parses but names a file no command - may touch is an ordinary refusal, `invalid-path`. -- Anything a command throws is reported like a refusal, reason `git-failed`, with the - error's own line on stderr: a caller parsing stdout never has to handle a command that - printed nothing. -- `list`, and `queue` with no sub-command, answer with a bare JSON array; every other - result, and every refusal, is an object whose `ok` tells the two apart. -- Run outside a repository, a command refuses with `not-a-repo`: only git's own "not a git - repository" reads as that, every other git failure stays `git-failed`. -- A write is one commit per command, pushed straight to origin through a throwaway - worktree at origin's tip; a push that loses a race is re-applied on the new tip by - `@gemstack/agent-data`. -- `queue done` takes the entry as `queue` printed it and refuses a line the queue does not - have. + plan, or `meta.json`, all inside `tickets/`; the bytes as given, unparsed), `close`, + `claim`, `release`, `queue add` and `queue done`. +- `show`, `claim` and `close` refuse a missing ticket with `no-ticket`. Every write + refuses `no-remote` without an origin. For `claim` and `close` the refusals come in this + order: `invalid-path`, `not-a-repo`, `no-identity` (a checkout on no branch), + `no-remote`, then `no-ticket`, since the ticket is checked inside the write; `release` + the same without `no-ticket`, never looking at the ticket; `queue add --ticket` checks + the ticket before the write but after the repository, so there `no-ticket` comes before + `no-remote` and `not-a-repo` before `invalid-path`. `release` looks only at the lock, so + an orphan lock naming you lifts. `put` checks only the name, before reading stdin, so a + plan can be written for a ticket that does not exist, and writes whatever stdin gives, + an empty file included. +- Every command names a ticket by its bare filename or its `tickets/` path, so a + queue entry's link target can be pasted in as is; a sibling's name (`.plan.md`, + `.lock.md`) is `invalid-path` to every command, `put` taking `.plan.md` the one + exception. +- No command reads `meta.json`: only the importing program does, for its one key + `lastImportedAt`; an unparsable file, or a value that is not a date string, reads as no + stamp, and only the first 10000 characters are parsed. +- A read fetches origin once and reads everything from that copy (the library's queue read + fetches only when asked): only origin has every writer's pushes, this command's own + included. With no origin the local branch is read: writes are refused there, so nobody + else can have moved it. +- Every command that runs prints one JSON document, the result or the refusal. A refusal + also puts one line on stderr and exits 1. A malformed command line (an unknown flag, the + wrong argument count, an empty `queue add` text, a `--priority` off the 0-10 scale) is + rejected first: the usage on stderr, nothing on stdout, exit 2. A file no command may + touch refuses with `invalid-path`. +- Anything a command throws refuses with `git-failed`. +- `list` and a bare `queue` answer with a JSON array; every other result and every refusal + is an object with `ok`. +- Outside a repository a command refuses `not-a-repo`; only git's own "not a git + repository" reads as that. +- A write's result echoes the file as `tickets/`, a row's `file` is bare; `claim` + and `release` also answer the holder, `queue add` and `queue done` the entry, `queue + add` its priority when placed, `show` the plan and the holder when there are any; a + refusal names the bare file or the entry it was given, `invalid-path` the argument as + typed (`put`'s with the `tickets/` prefix stripped), `git-failed` its detail, and + `no-remote`, `no-identity` and `not-a-repo` name nothing. The refusals: `no-ticket`, + `claimed` and `not-holder` (both with the holder, when readable), `no-lock`, `no-entry`, + `no-identity`, `no-remote`, `invalid-path`, `not-a-repo`, `git-failed`. +- The command's write is one commit per command (`put tickets/`, `close + tickets/`, `claim tickets/`, `release tickets/`, `queue add: `, + `queue done: `), pushed straight to origin through a throwaway worktree at + origin's tip; a push that loses a race is re-applied on the new tip by + `@gemstack/agent-data`. The program's writes go through its persistent checkout's cycle + instead, its seed as `seed the queue`, its batch claim as one commit, `claim + tickets` for the n it locked, or `claim tickets/` when n is one; its `queue done` + of an entry already gone succeeds, changing nothing. +- `queue done` takes the entry as `queue` printed it, trimmed, removes the first such + line, and refuses a line the queue does not have, an empty one included, decided inside + the write. diff --git a/packages/skill-tickets/SKILL.SPEC.md b/packages/skill-tickets/SKILL.SPEC.md index cd9790fc2..1f2bd7450 100644 --- a/packages/skill-tickets/SKILL.SPEC.md +++ b/packages/skill-tickets/SKILL.SPEC.md @@ -6,12 +6,12 @@ The skill's instructions: what an agent is told about the project's tickets and ## Business logic — TL;DR -- **The tickets are on a branch, not in your checkout** - the tickets (`tickets/_.md`, with their `.plan.md` and `.lock.md` siblings) and the queue (`TODO_AGENTS.md`) live on the `agent-data` branch, never on a code branch; the agent's own checkout does not contain them. A `tickets` link at the repository root may show them: read there if you like, never write there. -- **The `tickets` command is the only way to change them** - it ships with the `@gemstack/skill-tickets` package the repository depends on: the agent installs the repository's dependencies once and runs `npx tickets`, so every command the skill names runs as written on a fresh clone. Every change it makes is one commit pushed straight to the `agent-data` branch; nothing the agent commits on its own branch reaches them, and these files never belong on the agent's branch. -- **Read: `list`, `show`, `queue`** - every open ticket as JSON, one ticket with its plan and its holder, and the queue's open entries in order of work. -- **Change: `put`, `close`, `queue add`, `queue done`** - write a ticket, a plan or the import stamp from standard input; remove a ticket with its plan and claim, because `tickets/` holds only open tickets — refused while someone else holds the ticket; put an entry on the queue, optionally linked to a ticket and placed by that ticket's priority; take an entry off, which deletes it. -- **Claim before you plan or work a ticket** - `tickets claim` says the ticket is yours or names who holds it; someone else's claim means back off and pick another, and never remove or overwrite their lock. `tickets release` lifts the agent's own claim when the plan is finished or the work is published. -- **The formats** - a ticket (its optional `Priority:`, `Topics:` and `GitHub:` keys, its title, its `## TLDR` and `## Why it matters`); a claim (one line, `CLAIMED: `); a plan (its `Effort:` and `Uncertainty:` on a 0–10 scale, with the sections it may use and how to gauge uncertainty by listing each aspect and rating it); and the queue (`## Priority N` sections from 10, critical and to be acted on immediately, down to 0, only if capacity, first within a band first to be taken). +- **The tickets are on a branch, not in your checkout** - the tickets (`tickets/_.md`, with their `.plan.md` and `.lock.md` siblings) and the queue (`TODO_AGENTS.md`) live on the `agent-data` branch, never on a code branch; the agent's own checkout does not contain them. Where a long-lived process keeps that branch checked out, a `tickets` link at the repository root shows them: read there if you like, though it may trail what others pushed while the command reads fresh; never write there. +- **The `tickets` command is the only way to change them** - it ships with the `@gemstack/skill-tickets` package the repository depends on: the agent installs the repository's dependencies once, when there is no `node_modules` yet, and runs `npx tickets`, so every command the skill names runs as written on a fresh clone. Every change it makes is one commit pushed straight to the `agent-data` branch; a refusal exits 1 with a line on stderr, a wrong command line exits 2 with the usage; nothing the agent commits on its own branch reaches them, and these files never belong on the agent's branch. +- **Read: `list`, `show`, `queue`** - every open ticket as JSON, its optional fields (priority, topics, github, effort, uncertainty, locked, lockedBy) absent when unset; one ticket with its plan and its holder; and the queue's open entries in order of work. +- **Change: `put`, `close`, `queue add`, `queue done`** - write or create a ticket, a plan or the import stamp from standard input, the whole file, empty input included, a plan for a name no ticket has written without complaint; once the work is merged, remove the ticket with its plan and claim, because `tickets/` holds only open tickets — refused while someone else holds the ticket, and its queue entry stays until taken off; put an entry on the queue, optionally linked to a ticket and placed by that ticket's priority (5 when it has none); take an entry off, by its text as listed, which deletes it. +- **Claim before you plan or work a ticket** - `tickets claim` says the ticket is yours or names who holds it (no holder when the lock's line does not parse); someone else's claim means pick another, and never remove or overwrite their lock. A claim guards claim, close and release, never `put`, which overwrites whoever holds the ticket. `tickets release` lifts the agent's own claim once the plan is finished or the work is published, and before the agent stops, finished or not, unless it closed the ticket, since no timeout lifts it. The holder is `AGENT_ID` from the environment, else the current branch, so a rename or a branch switch between claim and release changes who the agent is, and a lock it can no longer lift stays until a person edits the branch. +- **The formats** - a ticket (its optional `Priority:`, `Topics:` and `GitHub:` keys above the title, `Priority:` a bare whole number or read as absent for queue placement, its title, its `## TLDR` and `## Why it matters`); a claim (one line, `CLAIMED: `); a plan (its `Effort:` and `Uncertainty:` on a 0–10 scale, with the sections it may use and how to gauge uncertainty by listing each aspect and rating it); and the queue (`## Priority N` sections from 10, critical and to be acted on immediately, down to 0, only if capacity, first within a band first to be taken). ## Before modifying/creating SPEC.md files diff --git a/packages/skill-tickets/SKILL.md b/packages/skill-tickets/SKILL.md index 5925ca388..c0c8a8e73 100644 --- a/packages/skill-tickets/SKILL.md +++ b/packages/skill-tickets/SKILL.md @@ -5,15 +5,17 @@ description: Where the project's tickets and its agent queue live, how to read a # Tickets and the agent queue -The tickets (`tickets/_.md`, with their `.plan.md` and `.lock.md` siblings) and the agent queue (`TODO_AGENTS.md`) live on the branch `agent-data`, never on a code branch. Your checkout does not carry them. A `tickets` link at the repository root may show the tickets, where a long-lived process keeps that branch checked out: read them there if you like, never write there. The queue is not under that link. +The tickets (`tickets/_.md`, with their `.plan.md` and `.lock.md` siblings) and the agent queue (`TODO_AGENTS.md`) live on the branch `agent-data`, never on a code branch. A `tickets` link at the repository root, if present, shows a possibly stale copy; never write there. The command reads fresh. The queue is not under that link. -Read and change them with the `tickets` command. It comes with the npm package `@gemstack/skill-tickets`, a dependency of this repository: install the repository's dependencies once — `npm install`, or the package manager its lockfile belongs to — then run it as `npx tickets`. Every change it makes is one commit pushed straight to the `agent-data` branch. +Read and change them with the `tickets` command, a dependency of this repository (`@gemstack/skill-tickets`). With no `node_modules`, install first with the lockfile's package manager (`npm install` for `package-lock.json`). Then run it as `npx tickets`. Every change it makes is one commit pushed straight to the `agent-data` branch. A refusal exits 1 with a line on stderr; a wrong command line exits 2 with the usage. ## Read ``` npx tickets list every open ticket, as JSON: file, title, summary, priority, topics, github, date, planned, effort, uncertainty, locked, lockedBy + (priority, topics, github, effort, uncertainty, locked, lockedBy + absent when unset) npx tickets show one ticket: its text, its plan, who holds it npx tickets queue the queue's open entries, in order of work ``` @@ -21,28 +23,34 @@ npx tickets queue the queue's open entries, in order of work ## Change ``` -npx tickets put write one file under tickets/ from stdin: a ticket, a plan, or meta.json - (the last-import stamp the program that imports issues keeps there) -npx tickets close remove a ticket with its plan and lock; refused while someone else - holds the ticket +npx tickets put write one file under tickets/ from stdin, the whole file, creating it if new; + empty stdin writes an empty file + (npx tickets put < draft.md): a ticket or a plan +npx tickets close once the work is merged: remove the ticket with its plan and lock; + refused while someone else holds it; its queue entry stays, + `queue done` it npx tickets queue add [--priority N] [--ticket ] put an entry on the queue; --priority places it in that section, --ticket links it to the ticket and places it by the ticket's - priority unless --priority says otherwise; with neither, it is - appended at the end of the file, under whatever section ends it -npx tickets queue done take an entry off the queue, as `npx tickets queue` printed it: done means deleted + priority (5 when it has none) unless --priority says otherwise; + with neither, it goes at the end of the file +npx tickets queue done remove an entry: one quoted argument, exactly as `npx tickets queue` + printed it; done means deleted ``` ## Claim before you plan or work a ticket ``` -npx tickets claim {"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 -npx tickets release lift your own claim — the plan is finished, or the work is published +npx tickets claim {"ok":true,"file":…,"holder":…} — the ticket is yours + {"ok":false,"reason":"claimed","holder":…,"file":…} — someone else's + (no holder when the lock's line does not parse): pick another; never remove + or overwrite their lock. A claim guards claim, close and release; + put overwrites whoever holds the ticket +npx tickets release lift your own claim when the plan or the work is done, and before you + stop unless you closed it; nothing lifts it on a timeout ``` -Every `` above names a ticket: its filename, `2042-01-01_some-ticket.md`, or the `tickets/…` path a queue entry links to; `put` also takes that ticket's `.plan.md` name, or `meta.json`. You claim as the value of `AGENT_ID` when the process that started you set it in your environment, else as your current branch name; nothing to type. +Every `` above takes a ticket's filename (`2042-01-01_some-ticket.md`) or the `tickets/…` path a queue entry links to; `put` also takes that ticket's `.plan.md` name, and writes a plan for a ticket that does not exist, without complaint, invisible to `show`. You claim as `AGENT_ID` when it is set, else as your current branch (so a rename or a branch switch between claim and release changes who you are: release from the branch you claimed on, or the lock stays until a person edits the branch). ## Formats @@ -68,11 +76,11 @@ GitHub: [#42](https://github.com/org/repo/issues/42) [optional] [optional: more info (any heading and format you want)] ``` -`tickets/` holds only open tickets: a closed ticket is removed, with its `.plan.md` and `.lock.md` (`npx tickets close`). +`Priority:`, `Effort:` and `Uncertainty:` are bare whole numbers above the `# ` title; anything else reads as absent for queue placement and the scales, and a ticket with no readable `Priority:` queues at 5. ### A claim: `tickets/_.lock.md` -Written by `npx tickets claim`, removed by `npx tickets release` or `npx tickets close`. One line: `CLAIMED: `. A ticket with a lock is being planned or worked by its holder — pick another. +Written by `npx tickets claim`, removed by `npx tickets release` or `npx tickets close`. One line: `CLAIMED: `. ### A plan: `tickets/_.plan.md`