diff --git a/SPEC.md b/SPEC.md index f8bef6a90..30a1659ac 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,8 +3,9 @@ The Framework: autonomous AI programming. Humans make the important decisions; c Six top-level pieces, one product: - `packages/framework` — the product itself, published as the npm package `framework`: one CLI (`the-framework`) that runs a foreground daemon, the agent lifecycle it orchestrates, and the browser dashboard it serves — the product's only user interface. -- `packages/skill-branches` — the git conventions and operations behind an agent's own checkout, as an API, as the `branches` command every agent the daemon starts on its machine gets on its PATH, and as the skill (`SKILL.md`) every agent's built-in system prompt carries, published as `@gemstack/skill-branches`: the first of the skills the product is being split into. The product depends on it; it depends on nothing of the product. -- `packages/skill-tickets` — the project's tickets and its agent queue, on the `tickets` branch of the project's own repository, published as `@gemstack/skill-tickets`: the same three faces — an API, the `tickets` command every agent the daemon starts gets on its PATH, and the skill (`SKILL.md`) every such agent finds in its checkout — over reading tickets, writing them, claiming one so two agents never work the same, and keeping the queue. The second of the skills. The product depends on it, and it depends on skill-branches for the branch it stores everything on. +- `packages/agent-data` — a branch of the project's repository used as a file store, published as `@gemstack/agent-data`: the shared `agent-data` branch every skill keeps its files on, written through one sync → commit → push cycle that re-applies the change when the push loses a race, from a long-lived process through a persistent checkout or one-shot from any clone. A library, not a skill — read by code, never by an agent. Every skill depends on it, and so does the product; it depends on nothing. +- `packages/skill-branches` — the git conventions and operations behind an agent's own checkout, as an API, as the `branches` command every agent the daemon starts on its machine gets on its PATH, and as the skill (`SKILL.md`) every agent's built-in system prompt carries, published as `@gemstack/skill-branches`: the first of the skills the product is being split into. The product depends on it; it depends on agent-data and on nothing of the product. +- `packages/skill-tickets` — the project's tickets and its agent queue, on the `tickets` branch of the project's own repository, published as `@gemstack/skill-tickets`: the same three faces — an API, the `tickets` command every agent the daemon starts gets on its PATH, and the skill (`SKILL.md`) every such agent finds in its checkout — over reading tickets, writing them, claiming one so two agents never work the same, and keeping the queue. The second of the skills. The product depends on it, and it depends on agent-data for the branch it stores everything on. - `packages/agent-driver` — the driver seam as its own package, published as `agent-driver`: one contract for driving a coding-agent CLI as a black box — a session in a directory, one full turn per prompt, a stream of what the agent did — and the implementations for Claude Code and Codex on this device, Claude Code on a GitHub Actions runner, and a scripted fake. The product depends on it and adds the one implementation that needs the product, the hand-off to a Claude Code cloud session; it depends on nothing of the product. - `packages/chrome-extension` — the Claude web bridge, a companion Chrome extension: when an agent's task was handed to a Claude Code cloud session on claude.ai, it carries the question that session is parked on into the local dashboard, and types the answer picked there back into the session. - `packages/the-framework.ai` — the marketing website at https://the-framework.ai. diff --git a/package.json b/package.json index 0d66f42cf..ee30640ea 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "preview": "pnpm -C packages/framework preview", "dev": "pnpm -C packages/framework dev", "typecheck": "pnpm -C packages/framework typecheck && pnpm -C packages/the-framework.ai typecheck", - "clean": "pnpm -C packages/skill-branches clean && pnpm -C packages/skill-tickets clean && pnpm -C packages/agent-driver clean && pnpm -C packages/framework clean", - "test": "pnpm -C packages/skill-branches test && pnpm -C packages/skill-tickets test && pnpm -C packages/agent-driver test && pnpm -C packages/framework test", + "clean": "pnpm -C packages/agent-data clean && pnpm -C packages/skill-branches clean && pnpm -C packages/skill-tickets clean && pnpm -C packages/agent-driver clean && pnpm -C packages/framework clean", + "test": "pnpm -C packages/agent-data test && pnpm -C packages/skill-branches test && pnpm -C packages/skill-tickets test && pnpm -C packages/agent-driver test && pnpm -C packages/framework test", "========= Website": "", "// Develop website https://the-framework.ai locally": "", "website": "cd packages/the-framework.ai/ && pnpm run dev", diff --git a/packages/SPEC.md b/packages/SPEC.md index 977643441..00dad9557 100644 --- a/packages/SPEC.md +++ b/packages/SPEC.md @@ -1,13 +1,14 @@ The deliverables The Framework ships, one directory each: - `framework/` — the product: the `framework` npm package (CLI, daemon, agent lifecycle, dashboard). -- `skill-branches/` — the `@gemstack/skill-branches` npm package: the git conventions and operations behind an agent's own checkout, with the skill's instructions (`SKILL.md`) and the `branches` command an agent follows them with: the first skill of the skills-plus architecture (#1725). The product depends on it, and so does the skill-tickets package. -- `skill-tickets/` — the `@gemstack/skill-tickets` npm package: the project's tickets and its agent queue on the `tickets` branch of its own repository, with the skill's instructions (`SKILL.md`) and the `tickets` command an agent reads, writes and claims them with: the second skill of the skills-plus architecture (#1748). The product depends on it; it depends on skill-branches for the branch it keeps everything on. +- `agent-data/` — the `@gemstack/agent-data` npm package: a branch of the project's repository used as a file store — the shared `agent-data` branch every skill keeps its files on — with the git runner and the exclude rule it is built on. A library, not a skill: read by code, never by an agent, so no `SKILL.md` and no command. Every skill depends on it; no skill depends on another (#1750). +- `skill-branches/` — the `@gemstack/skill-branches` npm package: the git conventions and operations behind an agent's own checkout, with the skill's instructions (`SKILL.md`) and the `branches` command an agent follows them with: the first skill of the skills-plus architecture (#1725). The product depends on it, and so does the skill-tickets package; it depends on agent-data. +- `skill-tickets/` — the `@gemstack/skill-tickets` npm package: the project's tickets and its agent queue on the `tickets` branch of its own repository, with the skill's instructions (`SKILL.md`) and the `tickets` command an agent reads, writes and claims them with: the second skill of the skills-plus architecture (#1748). The product depends on it; it depends on agent-data for the branch it keeps everything on, and on skill-branches for who an agent is. - `agent-driver/` — the `agent-driver` npm package: the driver seam — start a coding-agent CLI in a directory, prompt it for one full turn, stream what it does, resume it later — with the Claude Code, Codex, GitHub Actions and fake implementations. The product depends on it and adds its own cloud-session implementation behind the same contract. - `chrome-extension/` — the Claude web bridge, a companion Chrome extension that connects Claude Code cloud sessions on claude.ai back to the local dashboard. - `the-framework.ai/` — the marketing website. -The product depends on the skill-branches, skill-tickets and agent-driver packages in code; the extension and the website depend on the product only in what they present. See the root `SPEC.md` for how they relate as a product. +The product depends on the agent-data, skill-branches, skill-tickets and agent-driver packages in code; the extension and the website depend on the product only in what they present. See the root `SPEC.md` for how they relate as a product. ## Before modifying/creating SPEC.md files diff --git a/packages/agent-data/DECISIONS.md b/packages/agent-data/DECISIONS.md new file mode 100644 index 000000000..ee7b16462 --- /dev/null +++ b/packages/agent-data/DECISIONS.md @@ -0,0 +1,41 @@ +Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left +to the implementer's judgment. Flag conflicts instead of silently deviating. + +## The package +- A **library, not a skill**. A skill is a `SKILL.md` an agent reads, plus a command it + runs; this package has neither, because only code uses it. Skills never import each + other's code; this library is what every skill imports. +- Package name = branch name: `@gemstack/agent-data` manages the `agent-data` branch, so it + is called the same. +- `.branches/` holds every extra checkout of the project — each agent's, and the data + branch's — so its name is defined here. It starts with a dot to keep tools' `*` patterns + out of it: every checkout inside is a full working copy of the project. Hidden through + git's own exclude file, never a committed `.gitignore`: the project's files are not the + library's to change. + +## 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 and pulled on every + change, so every machine sees the same files. +- 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 that does not exist yet is born empty, with no parent commit, so no code commit + is ever in its history. +- The branch name is written once, here, as `DATA_BRANCH`; every other package imports it. + +## Flow: a write +Fetch what others pushed → make the change → commit → push. If the push is rejected +because someone pushed in between, start over on top of their changes. + +- A write is handed over as a small function ("add this line"), not as a finished + commit, so starting over is just running it again on the new files. Never a force + push. After two failed pushes the write reports the failure and the commit stays local. + The next write's fetch carries it when it still applies on top of the remote, and drops + it when it does not: the remote wins, only the current change runs again, and nobody is + told. +- Two writers. A long-running process, the program that starts agents, writes in its own + checkout, `.branches/agent-data`, one write at a time. A command an agent runs writes in + a temporary copy of the branch, pushes, and deletes the copy; it never touches the + process's checkout, whose next write commits everything it finds there and resets it + when the write fails. A command's write that cannot be pushed fails; nothing of it waits. diff --git a/packages/agent-data/SPEC.md b/packages/agent-data/SPEC.md new file mode 100644 index 000000000..76a10119a --- /dev/null +++ b/packages/agent-data/SPEC.md @@ -0,0 +1,15 @@ +A branch of a project's repository used as a file store, as an npm library: the one branch every skill a coding agent uses keeps its files on — `agent-data` — checked out once under the project's `.branches/`, written through one cycle every write goes through (sync with origin, apply the change, commit, push, and re-apply the change when the push loses a race), written one-shot from any clone, pulled eagerly, and readable from anywhere in the repository without holding a copy. The git runner and the exclude rule that machinery is built on ship with it, and so do the two names every consumer needs: the checkouts directory and the data branch. + +A library, not a skill: it is read by code, never by an agent, so it ships no `SKILL.md` and no command. It knows git and the filesystem, nothing else — the caller names the branch and decides what the files mean. Every skill that keeps files on the branch depends on it; no skill depends on another. + +## Business logic — TL;DR + +- **The two names** (`names`) - `.branches`, the directory every checkout of a project lives under, and `agent-data`, the shared data branch: defined once, importable without pulling in git. +- **Running git** (`git`) - one runner with a time budget per subcommand, a timeout told apart from a git failure, the checkout a directory is in, and a push whose failure is git's own reason. +- **Hidden from the project's git** (`git-exclude`) - a rule in git's own exclude list, never a tracked file, covering every worktree of the repository at once. +- **A branch used as a file store** (`file-branch`) - a branch the caller names, holding files nobody edits in a working tree: born parentless or adopted from origin, checked out under `.branches/` for a long-lived process that funnels every write through one serialized cycle, written one-shot from any clone through a throwaway worktree, pulled eagerly, and readable from anywhere in the repository without holding a copy. +- **The entry point** (`index`) - everything above in one place for a caller to import. + +## Before modifying/creating SPEC.md files + +You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/agent-data/package.json b/packages/agent-data/package.json new file mode 100644 index 000000000..d57a14419 --- /dev/null +++ b/packages/agent-data/package.json @@ -0,0 +1,41 @@ +{ + "name": "@gemstack/agent-data", + "version": "0.0.1", + "description": "A branch of the project's repository used as a file store for coding-agent skills: the agent-data branch, checked out under .branches/, every write one sync-commit-push cycle that re-applies on a lost race — from a long-lived process through its checkout, or one-shot from any clone.", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/framework/the-framework.git", + "directory": "packages/agent-data" + }, + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./names": { + "types": "./dist/names.d.ts", + "default": "./dist/names.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "pnpm build && tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'", + "clean": "rm -rf dist dist-test" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/agent-data/src/SPEC.md b/packages/agent-data/src/SPEC.md new file mode 100644 index 000000000..cb1bc3381 --- /dev/null +++ b/packages/agent-data/src/SPEC.md @@ -0,0 +1,5 @@ +The package's source: a branch used as a file store, and the git it is built on. See the package `SPEC.md` for how the modules relate; each module's `*.SPEC.md` holds its business logic. + +## Before modifying/creating SPEC.md files + +You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/skill-branches/src/file-branch.SPEC.md b/packages/agent-data/src/file-branch.SPEC.md similarity index 100% rename from packages/skill-branches/src/file-branch.SPEC.md rename to packages/agent-data/src/file-branch.SPEC.md diff --git a/packages/skill-branches/src/file-branch.test.SPEC.md b/packages/agent-data/src/file-branch.test.SPEC.md similarity index 100% rename from packages/skill-branches/src/file-branch.test.SPEC.md rename to packages/agent-data/src/file-branch.test.SPEC.md diff --git a/packages/skill-branches/src/file-branch.test.ts b/packages/agent-data/src/file-branch.test.ts similarity index 100% rename from packages/skill-branches/src/file-branch.test.ts rename to packages/agent-data/src/file-branch.test.ts diff --git a/packages/skill-branches/src/file-branch.ts b/packages/agent-data/src/file-branch.ts similarity index 99% rename from packages/skill-branches/src/file-branch.ts rename to packages/agent-data/src/file-branch.ts index f8f20772f..4cfce3285 100644 --- a/packages/skill-branches/src/file-branch.ts +++ b/packages/agent-data/src/file-branch.ts @@ -1,5 +1,5 @@ import { dirname, join } from 'node:path' -import { BRANCHES_DIR } from './branch-names.js' +import { BRANCHES_DIR } from './names.js' import { nodeGitRunner, type GitRunner } from './git.js' import { excludeFromGit } from './git-exclude.js' diff --git a/packages/skill-branches/src/git-exclude.BUG-ANALYSIS.md b/packages/agent-data/src/git-exclude.BUG-ANALYSIS.md similarity index 100% rename from packages/skill-branches/src/git-exclude.BUG-ANALYSIS.md rename to packages/agent-data/src/git-exclude.BUG-ANALYSIS.md diff --git a/packages/skill-branches/src/git-exclude.SPEC.md b/packages/agent-data/src/git-exclude.SPEC.md similarity index 100% rename from packages/skill-branches/src/git-exclude.SPEC.md rename to packages/agent-data/src/git-exclude.SPEC.md diff --git a/packages/skill-branches/src/git-exclude.ts b/packages/agent-data/src/git-exclude.ts similarity index 100% rename from packages/skill-branches/src/git-exclude.ts rename to packages/agent-data/src/git-exclude.ts diff --git a/packages/skill-branches/src/git.SPEC.md b/packages/agent-data/src/git.SPEC.md similarity index 100% rename from packages/skill-branches/src/git.SPEC.md rename to packages/agent-data/src/git.SPEC.md diff --git a/packages/skill-branches/src/git.test.SPEC.md b/packages/agent-data/src/git.test.SPEC.md similarity index 100% rename from packages/skill-branches/src/git.test.SPEC.md rename to packages/agent-data/src/git.test.SPEC.md diff --git a/packages/skill-branches/src/git.test.ts b/packages/agent-data/src/git.test.ts similarity index 100% rename from packages/skill-branches/src/git.test.ts rename to packages/agent-data/src/git.test.ts diff --git a/packages/skill-branches/src/git.ts b/packages/agent-data/src/git.ts similarity index 100% rename from packages/skill-branches/src/git.ts rename to packages/agent-data/src/git.ts diff --git a/packages/agent-data/src/index.SPEC.md b/packages/agent-data/src/index.SPEC.md new file mode 100644 index 000000000..3b5ccdc2f --- /dev/null +++ b/packages/agent-data/src/index.SPEC.md @@ -0,0 +1,5 @@ +The package's main entry point: it gathers the two names, the git runner, the exclude rule and the branch used as a file store into one place for a caller to import. No business logic of its own. The two names are also reachable on their own (`names`), for code that runs in a browser and must not pull in git. + +## Before modifying/creating SPEC.md files + +You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/agent-data/src/index.ts b/packages/agent-data/src/index.ts new file mode 100644 index 000000000..69a85e5d4 --- /dev/null +++ b/packages/agent-data/src/index.ts @@ -0,0 +1,31 @@ +export { BRANCHES_DIR, DATA_BRANCH } from './names.js' +export { + type GitRunner, + GitTimeoutError, + isGitTimeout, + nodeGitRunner, + isGitRepo, + checkoutRoot, + gitReason, + pushBranch, +} from './git.js' +export { excludeFromGit, type ExcludeFs } from './git-exclude.js' +export { + fileBranchPath, + fileBranchRepo, + ensureFileBranch, + withFileBranch, + pullFileBranch, + readBranchFile, + listBranchDir, + openBranchReader, + writeFileBranchDetached, + nodeBranchFileFs, + type FileBranchDeps, + type FileBranchWrite, + type FileBranchSync, + type CommitMessage, + type DetachedWrite, + type BranchFileFs, + type BranchReader, +} from './file-branch.js' diff --git a/packages/agent-data/src/names.SPEC.md b/packages/agent-data/src/names.SPEC.md new file mode 100644 index 000000000..a999a4406 --- /dev/null +++ b/packages/agent-data/src/names.SPEC.md @@ -0,0 +1,11 @@ +The two names every consumer of the package builds on, defined once: `.branches`, the directory at a project's root where every checkout lives — the data branch's persistent checkout beside the agents' own — and `agent-data`, the shared data branch every skill keeps its files on, a path per skill. Both are reachable without pulling in git, so code that runs in a browser can name them too. + +## Rationale + +The directory is dotted so that tools run in the project — a type-checker's `**/*`, a test runner's glob, a formatter — never descend into the checkouts: a leading dot is what `*` does not match, and it is the one thing that keeps N copies of the repository out of every tool that does not read git's ignore rules. + +The branch is named here and nowhere else: every skill already depends on this package, so one exported constant is what keeps four packages from each spelling the name. + +## Before modifying/creating SPEC.md files + +You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/agent-data/src/names.ts b/packages/agent-data/src/names.ts new file mode 100644 index 000000000..279b87c91 --- /dev/null +++ b/packages/agent-data/src/names.ts @@ -0,0 +1,19 @@ +/** + * The two names every consumer of the package hangs off. Pure: no node imports, so browser-side + * code can name them too. + */ + +/** + * Where a project's checkouts live: `/.branches/`, one directory per checkout, each named + * as the branch it is on — the data branch's persistent checkout beside the agents' own. Dotted + * on purpose: a `*` glob does not match a leading dot, so type-checkers, test runners and + * formatters run in the project never descend into N copies of the repository. + */ +export const BRANCHES_DIR = '.branches' + +/** + * The shared data branch of a project: the one branch every skill keeps its files on, a path per + * skill, checked out once at `.branches/agent-data`. Named here, once, for every consumer to + * import — never spelled out again. + */ +export const DATA_BRANCH = 'agent-data' diff --git a/packages/agent-data/tsconfig.build.json b/packages/agent-data/tsconfig.build.json new file mode 100644 index 000000000..e57806493 --- /dev/null +++ b/packages/agent-data/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist", "rootDir": "src" }, + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/agent-data/tsconfig.json b/packages/agent-data/tsconfig.json new file mode 100644 index 000000000..404aab404 --- /dev/null +++ b/packages/agent-data/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "noEmit": true, "rootDir": "src" }, + "include": ["src"] +} diff --git a/packages/agent-data/tsconfig.test.json b/packages/agent-data/tsconfig.test.json new file mode 100644 index 000000000..eebda2fc6 --- /dev/null +++ b/packages/agent-data/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "dist-test", "rootDir": "src" }, + "include": ["src"] +} diff --git a/packages/framework/SPEC.md b/packages/framework/SPEC.md index 4cd622e1b..9c1fe2771 100644 --- a/packages/framework/SPEC.md +++ b/packages/framework/SPEC.md @@ -2,7 +2,7 @@ The product: the `framework` npm package. One CLI, `the-framework`, runs a foreg How the package's pieces relate: -- `src/` — everything that runs in Node: the CLI, the daemon, the agent lifecycle (checkouts through the `skill-branches` package, gates, handoff), the drivers (the agent-driver package's, plus the product's own hand-off to a Claude Code cloud session), the `agents-logs` branch its own records live on (the tickets and the queue are the `skill-tickets` package's), autonomy (Auto PM, sweeps, CI watch), and the server side of the dashboard. +- `src/` — everything that runs in Node: the CLI, the daemon, the agent lifecycle (checkouts through the `skill-branches` package, gates, handoff), the drivers (the agent-driver package's, plus the product's own hand-off to a Claude Code cloud session), the `agents-logs` branch its own records live on (a branch used as a file store through the `@gemstack/agent-data` package; the tickets and the queue are the `skill-tickets` package's), autonomy (Auto PM, sweeps, CI watch), and the server side of the dashboard. - `dashboard/` — the browser app the daemon serves: a single-page app that is a pure projection of the files the daemon writes, reading over `POST /_rpc/` and a live event stream, and steering agents back through the same daemon. - `prompts/` — every prompt The Framework sends an agent, as markdown: the built-in system prompt, the protocols agents answer through, and the presets. The markdown is the only source of truth for agent-facing text. - `scripts/` — the build steps that compile the prompts into importable strings and run the package's test suites. diff --git a/packages/framework/package.json b/packages/framework/package.json index 5bf7bc2bc..90230e963 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@base-ui-components/react": "1.0.0-rc.0", + "@gemstack/agent-data": "workspace:*", "@gemstack/skill-branches": "workspace:*", "@gemstack/skill-tickets": "workspace:*", "agent-driver": "workspace:*", diff --git a/packages/framework/src/SPEC.md b/packages/framework/src/SPEC.md index 94df5d4e0..b1466981f 100644 --- a/packages/framework/src/SPEC.md +++ b/packages/framework/src/SPEC.md @@ -6,7 +6,7 @@ Everything of The Framework that runs in Node: the CLI entry, the daemon, the ag - **The agent lifecycle** - an agent gets a worktree and a branch, is framed with the built-in system prompt, works turn by turn through the driver, parks on gates when it needs the user, works the agent queue when its main task settles, and publishes itself per its handoff level when done. - **The driver seam** - the wrapped coding-agent CLI is a black box (the `agent-driver` package; `driver/` holds only the product's own cloud-session implementation): prompt in, full turn out, everything learned by parsing the turn's final message. Which CLI (Claude Code, Codex) and where it runs (this device, a GitHub Actions runner, a Claude Code cloud session) are two separate axes. - **Files are the seam** - an agent appends events to `.the-framework/events.jsonl`; steering flows back through `.the-framework/control.jsonl`; the agent's status lives in `agent.json`. The daemon and every surface are projections of these files (`store/`) — there is no process-to-process IPC. -- **Two branches carry the records** - the tickets and the agent queue belong to the `tickets` skill (the `@gemstack/skill-tickets` package) and live on the project's `tickets` branch; The Framework's own records — the agent archives and the routine locks — live on its `agents-logs` branch. Both are branches used as file stores: nobody edits them in a working tree, and every change goes through one serialized sync → apply → commit → push cycle, so every machine and cloud session sees the same thing while the default branch stays 100% code. +- **Two branches carry the records** - the tickets and the agent queue belong to the `tickets` skill (the `@gemstack/skill-tickets` package) and live on the project's `tickets` branch; The Framework's own records — the agent archives and the routine locks — live on its `agents-logs` branch. Both are branches used as file stores (the `@gemstack/agent-data` package's): nobody edits them in a working tree, and every change goes through one serialized sync → apply → commit → push cycle, so every machine and cloud session sees the same thing while the default branch stays 100% code. - **Autonomy, bounded** - on one shared background clock the daemon runs Auto PM (drain the agent queue, triage, plan — at most one firing per routine at a time, guarded by a routine lock on the `agents-logs` branch), the CI watch (merge on green, fix on red), and the sweeps (reclaim pushed checkouts, adopt cloud work, expire dead refs) — each start gated by the quota boundary. - **The dashboard's server side** - `dashboard/` (in this directory) serves the built browser app and implements its reads, live event stream, actions, and daemon-to-daemon relay; `dashboard-rpc/` is the RPC surface itself. diff --git a/packages/framework/src/agent-archive.test.ts b/packages/framework/src/agent-archive.test.ts index bc519da70..f4056c7b0 100644 --- a/packages/framework/src/agent-archive.test.ts +++ b/packages/framework/src/agent-archive.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { join } from 'node:path' -import type { GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' import { userDirName, resolveUserDir, forgetUserDirs, ANONYMOUS_USER_DIR } from './agent-archive.js' import { frameworkGitignore } from './framework-gitignore.js' diff --git a/packages/framework/src/agent-archive.ts b/packages/framework/src/agent-archive.ts index 3647764ab..054e70993 100644 --- a/packages/framework/src/agent-archive.ts +++ b/packages/framework/src/agent-archive.ts @@ -1,5 +1,5 @@ import { ARCHIVE_DIR } from './store/index.js' -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' /** * Committed session history (#1179): where a project's finished agents are archived so they survive * the repo being cleaned. diff --git a/packages/framework/src/agent.test.ts b/packages/framework/src/agent.test.ts index 80e82d7d6..7fc3a23c2 100644 --- a/packages/framework/src/agent.test.ts +++ b/packages/framework/src/agent.test.ts @@ -608,7 +608,7 @@ test('runAgent runs the backlog loop after the build when opted in (#323)', asyn const { mkdtemp, realpath, rm, writeFile } = await import('node:fs/promises') const { tmpdir } = await import('node:os') const { join } = await import('node:path') - const { nodeGitRunner, withFileBranch } = await import('@gemstack/skill-branches') + const { nodeGitRunner, withFileBranch } = await import('@gemstack/agent-data') // The queue lives on the tickets branch (#1582/#1748), so the fixture is a real repo. const git = nodeGitRunner() const cwd = await realpath(await mkdtemp(join(tmpdir(), 'framework-run-todo-'))) diff --git a/packages/framework/src/archived-agent-patch.test.ts b/packages/framework/src/archived-agent-patch.test.ts index 62c90fe9c..998fde1de 100644 --- a/packages/framework/src/archived-agent-patch.test.ts +++ b/packages/framework/src/archived-agent-patch.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { execFile } from 'node:child_process' import { promisify } from 'node:util' -import { fileBranchPath, withFileBranch } from '@gemstack/skill-branches' +import { fileBranchPath, withFileBranch } from '@gemstack/agent-data' import { LOGS_BRANCH } from './framework-dir.js' import { patchArchivedAgentOnDataBranch } from './archived-agent-patch.js' diff --git a/packages/framework/src/archived-agent-patch.ts b/packages/framework/src/archived-agent-patch.ts index 579ed8d78..e6e562820 100644 --- a/packages/framework/src/archived-agent-patch.ts +++ b/packages/framework/src/archived-agent-patch.ts @@ -1,4 +1,4 @@ -import { withFileBranch } from '@gemstack/skill-branches' +import { withFileBranch } from '@gemstack/agent-data' import { LOGS_BRANCH } from './framework-dir.js' import { patchArchivedAgent, type ArchivePatch } from './store/index.js' diff --git a/packages/framework/src/cli-exec.SPEC.md b/packages/framework/src/cli-exec.SPEC.md index 3f6426026..8dffd2e6b 100644 --- a/packages/framework/src/cli-exec.SPEC.md +++ b/packages/framework/src/cli-exec.SPEC.md @@ -1,4 +1,4 @@ -One configurable runner for the CLI binaries The Framework shells out to itself (`gh`; git runs through the `skill-branches` package's own runner): run the binary with arguments in a directory, resolve its output, reject on a non-zero exit. Each binary is configured with a time budget (`gh` runs its reads and its writes on two budgets) and with whether a failure is reported with the tool's own error text rather than a generic failure line (`gh` puts the useful part — "not logged in" — there, and that is what the dashboard should show). +One configurable runner for the CLI binaries The Framework shells out to itself (`gh`; git runs through the `@gemstack/agent-data` package's own runner): run the binary with arguments in a directory, resolve its output, reject on a non-zero exit. Each binary is configured with a time budget (`gh` runs its reads and its writes on two budgets) and with whether a failure is reported with the tool's own error text rather than a generic failure line (`gh` puts the useful part — "not logged in" — there, and that is what the dashboard should show). A process killed for outrunning its budget rejects as a recognizable timeout that names the command and the budget it outran, distinct from a failure the tool itself reported: a killed process usually writes no error of its own, and without the distinction it reads as the tool refusing. The timeout recognition still works on a value that crossed a module boundary. diff --git a/packages/framework/src/cli.test.ts b/packages/framework/src/cli.test.ts index 540ceef0e..d3f879d3d 100644 --- a/packages/framework/src/cli.test.ts +++ b/packages/framework/src/cli.test.ts @@ -6,7 +6,7 @@ import { join, dirname } from 'node:path' import { appendControl } from './control.js' import { BROWSER_MCP_SERVERS, withBrowser } from './browser.js' import { EVENTS_FILE, ARCHIVE_DIR, type StoreFs } from './store/index.js' -import { nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { layoutMarker, layoutMarkerPath } from './layout.js' import { diff --git a/packages/framework/src/cli.ts b/packages/framework/src/cli.ts index 65eef0fdb..2e08076a6 100644 --- a/packages/framework/src/cli.ts +++ b/packages/framework/src/cli.ts @@ -44,7 +44,8 @@ import { import { loadUserSystemPrompt, SYSTEM_PROMPT_FILE } from './system-prompt-file.js' import { checkForUpdate, formatUpdateStatus, nodeVersionFetcher, type VersionFetcher } from './update-check.js' import { AgentStore, nodeStoreFs, type StoreFs } from './store/index.js' -import { currentBranch, agentBranchName, nodeGitRunner, readBranchFile, sessionNameOf } from '@gemstack/skill-branches' +import { nodeGitRunner, readBranchFile } from '@gemstack/agent-data' +import { currentBranch, agentBranchName, sessionNameOf } from '@gemstack/skill-branches' import { materializePresets } from './presets.js' import { isLoopbackHost, registerHomeProject, runDaemon, DEFAULT_DAEMON_HOST, DEFAULT_DAEMON_PORT } from './daemon.js' import { appendControl, resetControl, watchControl, type ControlWatcher } from './control.js' diff --git a/packages/framework/src/cloud-scratch-refs.test.ts b/packages/framework/src/cloud-scratch-refs.test.ts index 326d17412..856694efb 100644 --- a/packages/framework/src/cloud-scratch-refs.test.ts +++ b/packages/framework/src/cloud-scratch-refs.test.ts @@ -9,7 +9,7 @@ import { type ScratchFs, type ScratchSweepResult, } from './cloud-scratch-refs.js' -import type { GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' import type { LinkedPr } from './dashboard/gh.js' const DAY = 24 * 60 * 60 * 1000 diff --git a/packages/framework/src/cloud-scratch-refs.ts b/packages/framework/src/cloud-scratch-refs.ts index fdf3659a5..6dbafbeda 100644 --- a/packages/framework/src/cloud-scratch-refs.ts +++ b/packages/framework/src/cloud-scratch-refs.ts @@ -1,5 +1,6 @@ import { join } from 'node:path' -import { nodeGitRunner, type GitRunner, AGENT_BRANCH_PREFIX } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' +import { AGENT_BRANCH_PREFIX } from '@gemstack/skill-branches' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { ghPrsForBranch, type LinkedPr } from './dashboard/gh.js' import { startedAtFromAgentId } from './store/index.js' diff --git a/packages/framework/src/cloud-work.ts b/packages/framework/src/cloud-work.ts index f4bd9f0d5..b0f254707 100644 --- a/packages/framework/src/cloud-work.ts +++ b/packages/framework/src/cloud-work.ts @@ -1,4 +1,5 @@ -import { nodeGitRunner, type GitRunner, agentBranchName } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' +import { agentBranchName } from '@gemstack/skill-branches' import { ghPrsForBranchOrThrow, pickAgentPr, type LinkedPr } from './dashboard/gh.js' import { openRemoteBranchPullRequest, type HandoffResult } from './dashboard/agent-handoff.js' import { listAgents, nodeStoreFs, startedAtFromAgentId, type AgentMeta, type ArchivePatch } from './store/index.js' diff --git a/packages/framework/src/daemon-runtime.ts b/packages/framework/src/daemon-runtime.ts index 7335b118d..597292964 100644 --- a/packages/framework/src/daemon-runtime.ts +++ b/packages/framework/src/daemon-runtime.ts @@ -3,7 +3,8 @@ import { closeSync, mkdirSync, openSync } from 'node:fs' import { basename, delimiter, dirname, join, resolve } from 'node:path' import { appendFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' import { agentIdFromStartedAt, startedAtFromAgentId, archiveWorktreeAgent, restoreArchivedAgent, listAgents, findAgent, archivedAgentPaths, readLiveMetas, readLiveMeta, resolveAgentEventsPath, EVENTS_FILE, META_FILE, isPidAlive, type AgentMeta } from './store/index.js' -import { createCheckout, attachCheckout, agentBranchName, worktreePath, worktreeBranch, removeWorktree, pruneWorktrees, agentIdFromWorktreeDir, isGitRepo, nodeGitRunner, isGitTimeout, withFileBranch, CLI_BIN_DIR as BRANCHES_BIN_DIR } from '@gemstack/skill-branches' +import { isGitRepo, nodeGitRunner, isGitTimeout, withFileBranch } from '@gemstack/agent-data' +import { createCheckout, attachCheckout, agentBranchName, worktreePath, worktreeBranch, removeWorktree, pruneWorktrees, agentIdFromWorktreeDir, CLI_BIN_DIR as BRANCHES_BIN_DIR } from '@gemstack/skill-branches' import { isTicketPath, CLI_BIN_DIR as TICKETS_BIN_DIR, SKILL_DIR as TICKETS_SKILL_DIR, SKILL_NAME as TICKETS_SKILL_NAME } from '@gemstack/skill-tickets' import { LOGS_BRANCH, THE_FRAMEWORK_DIR } from './framework-dir.js' import type { FrameworkEvent } from './events.js' diff --git a/packages/framework/src/daemon-services.test.ts b/packages/framework/src/daemon-services.test.ts index ceda14d27..cc283a1ad 100644 --- a/packages/framework/src/daemon-services.test.ts +++ b/packages/framework/src/daemon-services.test.ts @@ -7,7 +7,7 @@ import { execFile } from 'node:child_process' import { promisify } from 'node:util' import { startBackgroundServices, syncProjectData } from './daemon-services.js' import { projectErrorStore } from './project-errors.js' -import { fileBranchPath, withFileBranch } from '@gemstack/skill-branches' +import { fileBranchPath, withFileBranch } from '@gemstack/agent-data' import { TICKETS_BRANCH } from '@gemstack/skill-tickets' import { pollerQuotaSource, type QuotaSource } from './dashboard/quota.js' import { QuotaPoller } from './quota-poller.js' diff --git a/packages/framework/src/daemon-services.ts b/packages/framework/src/daemon-services.ts index a0cb25d1d..c0999dc13 100644 --- a/packages/framework/src/daemon-services.ts +++ b/packages/framework/src/daemon-services.ts @@ -22,7 +22,8 @@ import { LOGS_BRANCH } from './framework-dir.js' import type { ProjectErrors } from './project-errors.js' import { readFile, writeFile } from 'node:fs/promises' import { startMergedWorktreeSweep, type MergedSweepOptions } from './merged-worktrees.js' -import { pullFileBranch, reconcileBranchLinks } from '@gemstack/skill-branches' +import { pullFileBranch } from '@gemstack/agent-data' +import { reconcileBranchLinks } from '@gemstack/skill-branches' import { startProjectPass } from './project-pass.js' import { startCloudScratchSweep } from './cloud-scratch-refs.js' import { startCloudWorkAdoption } from './cloud-work.js' diff --git a/packages/framework/src/daemon-workspace.test.ts b/packages/framework/src/daemon-workspace.test.ts index b4b7a4e08..a75038da4 100644 --- a/packages/framework/src/daemon-workspace.test.ts +++ b/packages/framework/src/daemon-workspace.test.ts @@ -13,9 +13,9 @@ import type { PreflightResult } from './preflight.js' * machine running them happens to have `claude` installed and logged in. */ const agentReady = (): Promise => Promise.resolve({ ok: true, checks: [] }) - import { EVENTS_FILE, META_FILE, startedAtFromAgentId, type AgentMeta } from './store/index.js' -import { BRANCHES_DIR, worktreePath, agentBranchName, nodeGitRunner, GitTimeoutError, CLI_BIN_DIR } from '@gemstack/skill-branches' +import { BRANCHES_DIR, nodeGitRunner, GitTimeoutError } from '@gemstack/agent-data' +import { worktreePath, agentBranchName, CLI_BIN_DIR } from '@gemstack/skill-branches' import { CLI_BIN_DIR as TICKETS_BIN_DIR } from '@gemstack/skill-tickets' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { addProject, projectId } from './registry.js' diff --git a/packages/framework/src/daemon.test.ts b/packages/framework/src/daemon.test.ts index 5e35054e4..c1e17e50c 100644 --- a/packages/framework/src/daemon.test.ts +++ b/packages/framework/src/daemon.test.ts @@ -48,7 +48,8 @@ async function startDaemon(cwd: string, opts: RunDaemonOptions): Promise<{ done: } import { listAgents } from './store/index.js' import { EVENTS_FILE } from './store/index.js' -import { addWorktree, worktreePath, nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' +import { addWorktree, worktreePath } from '@gemstack/skill-branches' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { controlPath } from './control.js' import { projectId, listProjects, addProject } from './registry.js' diff --git a/packages/framework/src/dashboard-rpc/agent-addressing.test.ts b/packages/framework/src/dashboard-rpc/agent-addressing.test.ts index c8328bebe..0158ed238 100644 --- a/packages/framework/src/dashboard-rpc/agent-addressing.test.ts +++ b/packages/framework/src/dashboard-rpc/agent-addressing.test.ts @@ -6,7 +6,8 @@ import { tmpdir } from 'node:os' import { sendStop, sendMessage, sendChoice, sendRemoveWorktree } from './control.js' import { onRetainedWorktrees, onAgents } from './reads.js' import { addProject, projectId as idFor } from '../registry.js' -import { worktreePath, addWorktree, agentBranchName, nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' +import { worktreePath, addWorktree, agentBranchName } from '@gemstack/skill-branches' import { THE_FRAMEWORK_DIR } from '../framework-dir.js' import { CONTROL_FILE } from '../control.js' import { provideTestContext } from './test-context.js' diff --git a/packages/framework/src/dashboard-rpc/control.test.ts b/packages/framework/src/dashboard-rpc/control.test.ts index 185ceacc7..df891a028 100644 --- a/packages/framework/src/dashboard-rpc/control.test.ts +++ b/packages/framework/src/dashboard-rpc/control.test.ts @@ -7,9 +7,8 @@ import { provideTestContext } from './test-context.js' import { sendStart, sendReleaseTicketLock } from './control.js' import { presets } from '../preset-catalog.js' import { addProject, projectId } from '../registry.js' -import { withFileBranch } from '@gemstack/skill-branches' +import { withFileBranch, nodeGitRunner } from '@gemstack/agent-data' import { TICKETS_BRANCH } from '@gemstack/skill-tickets' -import { nodeGitRunner } from '@gemstack/skill-branches' import type { StartAgentOptions } from '../dashboard/types.js' /** A committed real repo whose tickets and queue sit on the tickets branch (#1582/#1748). */ diff --git a/packages/framework/src/dashboard-rpc/control.ts b/packages/framework/src/dashboard-rpc/control.ts index 8e5699503..ee2c41d98 100644 --- a/packages/framework/src/dashboard-rpc/control.ts +++ b/packages/framework/src/dashboard-rpc/control.ts @@ -8,7 +8,8 @@ import { ticketForPrompt } from '../todo-loop.js' import { planTicketPrompt } from '../tickets.js' import { isTicketFile, QUEUE_FILE, queueAdd, queuePriorityForTicket, releaseTicket, TICKETS_DIR } from '@gemstack/skill-tickets' import { findAgent, type AgentMeta } from '../store/index.js' -import { isSafeAgentId, worktreePath, pushBranch } from '@gemstack/skill-branches' +import { pushBranch } from '@gemstack/agent-data' +import { isSafeAgentId, worktreePath } from '@gemstack/skill-branches' import { withAgentLock } from '../agent-locks.js' import { removeProjectWorktree, deleteProjectAgent } from '../worktrees.js' import { patchArchivedAgentOnDataBranch } from '../archived-agent-patch.js' diff --git a/packages/framework/src/dashboard/agent-handoff.test.ts b/packages/framework/src/dashboard/agent-handoff.test.ts index 039a4d192..500f1702b 100644 --- a/packages/framework/src/dashboard/agent-handoff.test.ts +++ b/packages/framework/src/dashboard/agent-handoff.test.ts @@ -8,7 +8,7 @@ import { promisify } from 'node:util' import { isAgentBranch } from '@gemstack/skill-branches' import { readAgentHandoff, resolveAgentPr, mergeAgentPr, agentBranchFor, openBranchPullRequest, openRemoteBranchPullRequest, openAgentPullRequest, agentAutoHandoff, prBaseName, withheldMerge } from './agent-handoff.js' import { pickAgentPr } from './gh.js' -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' const exec = promisify(execFile) const SEP = String.fromCharCode(31) diff --git a/packages/framework/src/dashboard/agent-handoff.ts b/packages/framework/src/dashboard/agent-handoff.ts index d835f3b90..818a8e9cb 100644 --- a/packages/framework/src/dashboard/agent-handoff.ts +++ b/packages/framework/src/dashboard/agent-handoff.ts @@ -1,4 +1,5 @@ -import { nodeGitRunner, type GitRunner, agentBranchName, sessionNameOf, currentBranch, repoHasRemote, pushBranch, gitReason } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner, pushBranch, gitReason } from '@gemstack/agent-data' +import { agentBranchName, sessionNameOf, currentBranch, repoHasRemote } from '@gemstack/skill-branches' import { THE_FRAMEWORK_DIR } from '../framework-dir.js' import { cachedPrView, diff --git a/packages/framework/src/dashboard/docs.test.ts b/packages/framework/src/dashboard/docs.test.ts index 4cc08b0e4..1d427bb5d 100644 --- a/packages/framework/src/dashboard/docs.test.ts +++ b/packages/framework/src/dashboard/docs.test.ts @@ -4,9 +4,9 @@ import { mkdtemp, realpath, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { readDocs, DOC_CATEGORIES } from './docs.js' -import { withFileBranch } from '@gemstack/skill-branches' +import { withFileBranch, nodeGitRunner } from '@gemstack/agent-data' import { TICKETS_BRANCH } from '@gemstack/skill-tickets' -import { nodeGitRunner } from '@gemstack/skill-branches' + /** A real repo whose queue lives on the tickets branch (#1582/#1748), the way readDocs reads it. */ async function repoWithQueue(md: string): Promise { const git = nodeGitRunner() diff --git a/packages/framework/src/dashboard/file-diff.ts b/packages/framework/src/dashboard/file-diff.ts index 9ae12846c..32193e2cb 100644 --- a/packages/framework/src/dashboard/file-diff.ts +++ b/packages/framework/src/dashboard/file-diff.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import type { FileGitStatus } from './file-status.js' import { cutToPreview, readConfinedFile, safeRepoPath } from './file-read.js' diff --git a/packages/framework/src/dashboard/file-status.ts b/packages/framework/src/dashboard/file-status.ts index f5172273f..7956f2faa 100644 --- a/packages/framework/src/dashboard/file-status.ts +++ b/packages/framework/src/dashboard/file-status.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' // Per-file git status for the panel's file tree (#492): the working-tree state of each changed // file, so the tree can dot untracked/modified/deleted entries. A single `git status --porcelain` // read, mapped to repo-relative path -> state. Forgiving: a non-repo / failed git yields `{}`. diff --git a/packages/framework/src/dashboard/git-status.test.ts b/packages/framework/src/dashboard/git-status.test.ts index fc9d2e894..b15a181cc 100644 --- a/packages/framework/src/dashboard/git-status.test.ts +++ b/packages/framework/src/dashboard/git-status.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { readGitStatus } from './git-status.js' -import type { GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' const gitWith = (branch: string, porcelain: string): GitRunner => async args => { diff --git a/packages/framework/src/dashboard/git-status.ts b/packages/framework/src/dashboard/git-status.ts index ca472e43c..ffe4deaec 100644 --- a/packages/framework/src/dashboard/git-status.ts +++ b/packages/framework/src/dashboard/git-status.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import { cachedPrView, cachedPrsForBranch, pickAgentPr, type LinkedPr, type PrLookup } from './gh.js' // The project panel's git status (#491, part of #488): the active branch, whether the tree is diff --git a/packages/framework/src/dashboard/github.ts b/packages/framework/src/dashboard/github.ts index 99121d6e9..9f2e3189e 100644 --- a/packages/framework/src/dashboard/github.ts +++ b/packages/framework/src/dashboard/github.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' // The project panel's "Open on GitHub" (#489, part of #488). Derives the repo's github.com // URL from its `origin` remote so the panel can link straight to it. A read of git state, // safe anywhere — the relay has no local checkout, so it resolves to nothing there. diff --git a/packages/framework/src/driver/SPEC.md b/packages/framework/src/driver/SPEC.md index 0956332e8..cf21fe444 100644 --- a/packages/framework/src/driver/SPEC.md +++ b/packages/framework/src/driver/SPEC.md @@ -1,4 +1,4 @@ -The Framework's own driver implementation, behind the `agent-driver` package's contract: the `web` run target, which hands an agent's whole task to a Claude Code cloud session on claude.ai. It lives in the product rather than in the package because it needs the product — the daemon that queues the session request, the browser bridge that carries it, and the `skill-branches` package that pushes the starting point. Every other driver (Claude Code and Codex locally, a GitHub Actions runner, the scripted fake) is the package's. +The Framework's own driver implementation, behind the `agent-driver` package's contract: the `web` run target, which hands an agent's whole task to a Claude Code cloud session on claude.ai. It lives in the product rather than in the package because it needs the product — the daemon that queues the session request, the browser bridge that carries it, and the `@gemstack/agent-data` package's git runner that pushes the starting point. Every other driver (Claude Code and Codex locally, a GitHub Actions runner, the scripted fake) is the package's. ## Before modifying/creating SPEC.md files diff --git a/packages/framework/src/driver/cloud.ts b/packages/framework/src/driver/cloud.ts index 1d4b4162d..9d00a4373 100644 --- a/packages/framework/src/driver/cloud.ts +++ b/packages/framework/src/driver/cloud.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto' -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import { errorMessage } from '../error-message.js' import { githubSlugFor } from '../dashboard/github.js' import { WEB_START_PREFIX } from '../dashboard/web-start-endpoints.js' diff --git a/packages/framework/src/e2e/harness.ts b/packages/framework/src/e2e/harness.ts index 693f86922..e20983c60 100644 --- a/packages/framework/src/e2e/harness.ts +++ b/packages/framework/src/e2e/harness.ts @@ -13,7 +13,8 @@ import { createProjectRuntime, type ProjectRuntime } from '../daemon-runtime.js' import { registryPreferencesStore, projectId } from '../registry.js' import { registryDiscordCredentialsStore } from '../discord-credentials-store.js' import { resolveAgentEventsPath, type AgentMeta, type AgentStatus } from '../store/index.js' -import { withFileBranch, worktreePath } from '@gemstack/skill-branches' +import { withFileBranch } from '@gemstack/agent-data' +import { worktreePath } from '@gemstack/skill-branches' import { QUEUE_FILE, TICKETS_BRANCH, TICKETS_DIR } from '@gemstack/skill-tickets' import { withAgentLock } from '../agent-locks.js' import { tailAgentEvents } from '../dashboard-rpc/events-tail.js' diff --git a/packages/framework/src/framework-dir.ts b/packages/framework/src/framework-dir.ts index 4ba1803c5..c76e32cda 100644 --- a/packages/framework/src/framework-dir.ts +++ b/packages/framework/src/framework-dir.ts @@ -1,4 +1,4 @@ -import { BRANCHES_DIR } from '@gemstack/skill-branches/branch-names' +import { BRANCHES_DIR } from '@gemstack/agent-data/names' /** * The directory, under a project root, that holds The Framework's own files. diff --git a/packages/framework/src/install.test.ts b/packages/framework/src/install.test.ts index 00fc52c1b..dac06ebab 100644 --- a/packages/framework/src/install.test.ts +++ b/packages/framework/src/install.test.ts @@ -5,7 +5,7 @@ import { installProject } from './install.js' import { PRESETS, PRESET_DIR } from './presets.js' import { frameworkGitignore, gitignorePath } from './framework-gitignore.js' import { layoutMarker, layoutMarkerPath } from './layout.js' -import type { GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' import type { StoreFs } from './store/index.js' /** An in-memory {@link StoreFs} so the install logic is tested without touching disk. */ diff --git a/packages/framework/src/install.ts b/packages/framework/src/install.ts index 2ecc6414a..19a5d8bb8 100644 --- a/packages/framework/src/install.ts +++ b/packages/framework/src/install.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { frameworkGitignore, gitignorePath } from './framework-gitignore.js' import { layoutMarker, layoutMarkerPath } from './layout.js' diff --git a/packages/framework/src/maintenance.test.ts b/packages/framework/src/maintenance.test.ts index 43f531629..83d2a8a3c 100644 --- a/packages/framework/src/maintenance.test.ts +++ b/packages/framework/src/maintenance.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' -import type { GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' import { assessRepo, planMaintenanceSweep, diff --git a/packages/framework/src/maintenance.ts b/packages/framework/src/maintenance.ts index c7f90a6bf..085919918 100644 --- a/packages/framework/src/maintenance.ts +++ b/packages/framework/src/maintenance.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import { THE_FRAMEWORK_DIR } from './framework-dir.js' import { nodeFs } from './node-fs.js' /** diff --git a/packages/framework/src/merged-worktrees.test.ts b/packages/framework/src/merged-worktrees.test.ts index 444ddd8bb..470428f6e 100644 --- a/packages/framework/src/merged-worktrees.test.ts +++ b/packages/framework/src/merged-worktrees.test.ts @@ -4,7 +4,8 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { readFile, mkdtemp, realpath, rm, stat, writeFile } from 'node:fs/promises' import { removeMergedWorktrees, startMergedWorktreeSweep, type MergedSweepResult } from './merged-worktrees.js' -import { addWorktree, agentBranchName, nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' +import { addWorktree, agentBranchName } from '@gemstack/skill-branches' import type { WorktreeRow } from './worktrees.js' // E5: one rule — a checkout may go once its work is on the remote, and not before. Every deletion diff --git a/packages/framework/src/project.test.ts b/packages/framework/src/project.test.ts index 41d1586a0..8101c4612 100644 --- a/packages/framework/src/project.test.ts +++ b/packages/framework/src/project.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { crawlRepoFiles, isActivated, type ProjectFs } from './project.js' -import { type GitRunner } from '@gemstack/skill-branches' +import type { GitRunner } from '@gemstack/agent-data' import { gitignorePath } from './framework-gitignore.js' const CWD = '/proj' diff --git a/packages/framework/src/project.ts b/packages/framework/src/project.ts index 791ca24b5..768b400e2 100644 --- a/packages/framework/src/project.ts +++ b/packages/framework/src/project.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner, type GitRunner } from '@gemstack/agent-data' import { nodeFs } from './node-fs.js' import { gitignorePath } from './framework-gitignore.js' diff --git a/packages/framework/src/routine-locks.test.ts b/packages/framework/src/routine-locks.test.ts index 59e50a417..342ebd792 100644 --- a/packages/framework/src/routine-locks.test.ts +++ b/packages/framework/src/routine-locks.test.ts @@ -14,7 +14,7 @@ import { type RoutineLockDeps, } from './routine-locks.js' import { LOGS_BRANCH, LOGS_CHECKOUT_DIR } from './framework-dir.js' -import { fileBranchPath, nodeGitRunner } from '@gemstack/skill-branches' +import { fileBranchPath, nodeGitRunner } from '@gemstack/agent-data' const CWD = '/repo' const DATA = join(CWD, LOGS_CHECKOUT_DIR) const T0 = Date.parse('2026-08-23T10:00:00.000Z') diff --git a/packages/framework/src/routine-locks.ts b/packages/framework/src/routine-locks.ts index fe2c5f4dc..141d05fa8 100644 --- a/packages/framework/src/routine-locks.ts +++ b/packages/framework/src/routine-locks.ts @@ -1,6 +1,6 @@ import { hostname } from 'node:os' import { join } from 'node:path' -import { nodeBranchFileFs, withFileBranch, type BranchFileFs, type CommitMessage, type FileBranchWrite } from '@gemstack/skill-branches' +import { nodeBranchFileFs, withFileBranch, type BranchFileFs, type CommitMessage, type FileBranchWrite } from '@gemstack/agent-data' import { LOGS_BRANCH } from './framework-dir.js' // The "one triage at a time" guard (#1659): a `routines/.lock.md` on the logs branch. diff --git a/packages/framework/src/todo-loop.test.ts b/packages/framework/src/todo-loop.test.ts index 7a9458513..c65691901 100644 --- a/packages/framework/src/todo-loop.test.ts +++ b/packages/framework/src/todo-loop.test.ts @@ -8,7 +8,7 @@ import type { ChoicePick, ChoiceRequest, FrameworkEvent } from './events.js' import { nextQueuedTicket, runTodoLoop, agentTodoPending, ticketForPrompt } from './todo-loop.js' import { drainsQueue, presets } from './preset-catalog.js' import { AUTO_PM_DRAIN_JOB, AUTO_PM_JOBS } from './auto-pm.js' -import { fileBranchPath, nodeGitRunner, withFileBranch } from '@gemstack/skill-branches' +import { fileBranchPath, nodeGitRunner, withFileBranch } from '@gemstack/agent-data' import { TICKETS_BRANCH } from '@gemstack/skill-tickets' const git = nodeGitRunner() diff --git a/packages/framework/src/worktrees.test.ts b/packages/framework/src/worktrees.test.ts index 23b8d975f..2b06c58fe 100644 --- a/packages/framework/src/worktrees.test.ts +++ b/packages/framework/src/worktrees.test.ts @@ -5,7 +5,8 @@ import { tmpdir } from 'node:os' import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises' import { deleteProjectAgent, removeProjectWorktree } from './worktrees.js' import { listAgents } from './store/index.js' -import { addWorktree, agentBranchName, nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' +import { addWorktree, agentBranchName } from '@gemstack/skill-branches' // The agent's side of reclaiming a checkout: what its record allows the git rule to do, and how a // refusal is said. The rule itself is tested where it lives, in the `skill-branches` package. // Against real git, because "was the diff actually destroyed" is not a question a fake answers. diff --git a/packages/framework/src/worktrees.ts b/packages/framework/src/worktrees.ts index 7e88301b6..da5e8a16d 100644 --- a/packages/framework/src/worktrees.ts +++ b/packages/framework/src/worktrees.ts @@ -2,19 +2,8 @@ import { readFile } from 'node:fs/promises' import { join, sep } from 'node:path' import { errorMessage } from './error-message.js' import { listAgents, readLiveMetas, archivedAgentPaths, META_FILE, type AgentMeta, type AgentStatus } from './store/index.js' -import { - agentBranchName, - listWorktreeDirs, - isSafeAgentId, - reclaimWorktree, - removeWorktree, - pruneWorktrees, - worktreePath, - worktreeSize, - fileBranchPath, - withFileBranch, - type ReclaimOutcome, -} from '@gemstack/skill-branches' +import { fileBranchPath, withFileBranch } from '@gemstack/agent-data' +import { agentBranchName, listWorktreeDirs, isSafeAgentId, reclaimWorktree, removeWorktree, pruneWorktrees, worktreePath, worktreeSize, type ReclaimOutcome } from '@gemstack/skill-branches' import { LOGS_BRANCH, THE_FRAMEWORK_DIR } from './framework-dir.js' /** A retained worktree and the agent that left it behind (#752). */ diff --git a/packages/skill-branches/DECISIONS.md b/packages/skill-branches/DECISIONS.md new file mode 100644 index 000000000..3fa97991e --- /dev/null +++ b/packages/skill-branches/DECISIONS.md @@ -0,0 +1,52 @@ +Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left +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/`, in a + folder named after the agent's branch. 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. +- A checkout starts as branch `agent-` in folder `.branches/agent-/`, `` being + what the program that starts the agent calls it. 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 is named after the branch, + and a folder name cannot hold a slash. The package renames and deletes only `agent-*` + branches; the user's own branches are never touched. +- A taken name gets `-2`, `-3`, … instead of a refusal: the agent asked for a name and + reads back the one it got. Two agents naming the same thing at once race on the rename; + the loser takes the next suffix. +- The user's installed dependencies are linked into the checkout, not copied and not + reinstalled. One link per package, not one link to the whole folder, so a package the + agent installs lands in its own 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. + +- 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. +- The package only does git. 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. + +## 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. +- 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. Temporary, until the project commits the skill files itself. diff --git a/packages/skill-branches/SKILL.SPEC.md b/packages/skill-branches/SKILL.SPEC.md index d29229045..32b7db281 100644 --- a/packages/skill-branches/SKILL.SPEC.md +++ b/packages/skill-branches/SKILL.SPEC.md @@ -1,16 +1,18 @@ -The skill's instructions: what an agent is told about living inside its own checkout, under the package's conventions. Found by every agent a caller starts in a checkout the package created, as a skill of its harness (`src/skill-links`). +The skill's instructions: what an agent is told about where its work goes, for every place it can wake up in. Found by an agent as a skill of its harness — linked into every checkout the package creates (`src/skill-links`), or committed into the repository. ## User story -- The user starts an agent from the caller's UI and expects its work on a branch named after what it did, reviewable as a pull request, with nothing touched in the user's own checkout. +- The user starts an agent — from a caller's UI that made it a checkout of its own, or in a plain clone of the repository — and expects its work on a branch named after what it did, publishable as a pull request, with nothing touched on the user's own branch. ## Business logic — TL;DR -- **The workspace is the checkout** - the agent was started inside its own checkout under `.branches/`; every file it reads or writes is under its working directory, the repository around it is the user's own working tree and never the agent's to edit, and anything it genuinely needs from outside is a reason to say so and stop. -- **Name the session with the command** - before the first change the agent picks an `[a-z0-9-]+` name and runs `branches name `; the command renames the branch to `agent-` — a rename, so the commits stay — and prints the name the branch got, suffixed when the wanted one was taken. The printed name is the session name from then on; there is nothing else to report. -- **Commit as you go** - only what the agent committed is ever published; nothing is committed on its behalf, and uncommitted work stays in the checkout. -- **Leave a clean tree** - before finishing, `branches status` must report a clean tree: uncommitted work blocks the checkout from being reclaimed and is not part of what gets published. -- **Never publish yourself** - the agent neither pushes nor opens the pull request; push, pull request and merge are done for it, as the user configured. +- **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. ## Before modifying/creating SPEC.md files diff --git a/packages/skill-branches/SKILL.md b/packages/skill-branches/SKILL.md index 06a8e465f..29b54ad14 100644 --- a/packages/skill-branches/SKILL.md +++ b/packages/skill-branches/SKILL.md @@ -1,40 +1,48 @@ --- name: branches -description: One git checkout per agent, named as its branch. How to name your session, where your work lives, and what must be true before you finish. +description: Where your work goes (a branch named agent-), how to name it, and what must be true before you finish. --- # Branch management -You were started inside your own checkout of the repository: a git worktree at `.branches/agent-/`, on a branch of the same name. Your working directory is the whole of your workspace. +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. -## Workspace +## The command -- Every file you read or write is under your working directory. Address files relative to it — an absolute path is how you leave it without noticing. -- The repository around your checkout is the user's own working tree. It is not another view of your files, and it is never yours to edit: the same file exists twice, and only the copy under your working directory is on your branch. Editing the other one puts your work where your commits cannot reach it. -- If something you genuinely need is outside your working directory, say so and stop — do not reach for it. +`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`. -## Name the session +## Where you are -Before your first change, pick a name for the session — `[a-z0-9-]+`, saying succinctly what the work is — and run: +``` +npx branches status +``` + +It prints JSON; `branch` is the branch you are on. + +**It starts with `agent-`.** The branch is yours, and your working directory is your whole workspace: every file you read or write is under it, so address files relative to it — an absolute path is how you leave it without noticing. When it sits under a `.branches/` folder, a checkout was made for you and the repository around it is the user's own working tree, never yours to edit. If something you genuinely need is outside your working directory, say so and stop. + +Before your first change, name the session — `[a-z0-9-]+`, saying succinctly what the work is: ``` -branches name +npx branches name ``` -It renames your branch to `agent-` (a rename: your commits stay where they are) and prints the name the branch got, as JSON: `{"ok":true,"branch":"agent-"}`. When `` was already taken, the branch is `agent--2`, `-3`, … — use the name it printed, not the one you asked for. The session name is read from your branch; there is nothing else to report. +It renames your branch to `agent-` — a rename, so your commits stay — and prints the name the branch got: `agent--2`, `-3`, … when `` was taken. + +**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: + +``` +git switch -c agent- +``` ## Commit as you go -Work on that branch and commit to it as you go. Only what you committed is ever published — nothing is committed on your behalf, and work left uncommitted stays in your checkout. +Commit to your branch as you go. Only what you committed is ever published: nothing is committed on your behalf, and uncommitted work stays where it is — neither published nor cleaned up. ## Before you finish -Run: - ``` -branches status +npx branches status ``` -It must report `"clean": true`. Uncommitted work blocks the checkout from being reclaimed, and is not part of what gets published. - -Do not push, and do not open the pull request yourself: publishing — push, pull request, merge — is done for you, as the user configured it. +It must report `"clean": true`. diff --git a/packages/skill-branches/SPEC.md b/packages/skill-branches/SPEC.md index 10d59f7d6..d53bc7088 100644 --- a/packages/skill-branches/SPEC.md +++ b/packages/skill-branches/SPEC.md @@ -1,18 +1,16 @@ -Branch management for coding agents, as an npm package: one git checkout per agent under a project's `.branches/`, named as its branch; the parent checkout's dependencies shared into it; a navigable link per branch name; one retention rule under which a checkout is reclaimed — only once everything it holds is on the remote; the instructions an agent follows to live inside that rule (`SKILL.md`); and a branch used as a file store, for the files a caller keeps in the repository that are not code. +Branch management for coding agents, as an npm package: one git checkout per agent under a project's `.branches/`, named as its branch; the parent checkout's dependencies shared into it; a navigable link per branch name; one retention rule under which a checkout is reclaimed — only once everything it holds is on the remote; and the instructions an agent follows to live inside that rule (`SKILL.md`). -The package knows git and the filesystem, nothing else. The same functions serve every caller: a daemon that starts agents, a dashboard that lists and reclaims checkouts, and an agent's own shell, through the `branches` command a daemon puts on the PATH of every agent it starts on its machine. What a caller knows beyond git — whether an agent is still running, whether the caller allows a push, which pushed commit already holds its work — is passed in; the package never reads an agent's record. +The package knows git and the filesystem, nothing else. Running git, the exclude that hides `.branches/` from the project's git, and the `.branches` name itself are the `@gemstack/agent-data` package's, which this package depends on. The same functions serve every caller: a daemon that starts agents, a dashboard that lists and reclaims checkouts, and an agent's own shell, through the `branches` command a daemon puts on the PATH of every agent it starts on its machine. What a caller knows beyond git — whether an agent is still running, whether the caller allows a push, which pushed commit already holds its work — is passed in; the package never reads an agent's record. ## Business logic — TL;DR - **The conventions** (`branch-names`) - branch names, the checkout directory layout under `.branches/`, and the agent-id charset every path is built from. -- **Running git** (`git`) - one runner with a time budget per subcommand, and a timeout told apart from a git failure. - **A checkout's lifecycle** (`worktree`) - create, attach, list, name, remove, prune; the reads every retention decision is built on; the project a directory belongs to. - **A checkout as an agent gets it** (`checkout`) - the worktree, `.branches/` hidden from git, the dependencies linked in, the skill linked in, the links refreshed: one sequence for a daemon and the command line. - **The skill where the harness looks** (`skill-links`) - a link per harness in every checkout the package creates — `.claude/skills/branches`, `.agents/skills/branches` — to the package's own `SKILL.md`, hidden from git; one mechanism for every harness. A caller may name further skills to be linked in beside it, which it may do temporarily, until skills are committed into the repository. - **Dependencies shared, not copied** (`worktree-deps`) - a fresh checkout gets the parent's dependency trees as directories of links. -- **Reachable by branch name** (`branch-links`, `git-exclude`) - a symlink per current branch name beside the checkouts; the exclude that hides `.branches/` from the project's git. +- **Reachable by branch name** (`branch-links`) - a symlink per current branch name beside the checkouts. - **Reclaiming a checkout** (`reclaim`) - the one rule: keep a dirty tree, push the branch when allowed, remove only once the remote has it, and delete an agent branch that holds nothing. -- **A branch used as a file store** (`file-branch`) - a branch the caller names, holding files nobody edits in a working tree: born parentless or adopted from origin, checked out under `.branches/` for a long-lived process that funnels every write through one serialized cycle, written one-shot from any clone through a throwaway worktree, pulled eagerly, and readable from anywhere in the repository without holding a copy. - **The command line** (`cli`, `bin/`) - the same operations as commands for a shell: JSON on stdout, a reason on stderr, an exit code that tells a refusal from a usage error; the executable's directory is exported (`bin-dir`) for a caller that spawns agents. - **The skill** (`SKILL.md`) - what the agent is told: its checkout is its whole workspace, it names its session with `branches name` before its first change and uses the name the command prints, it commits as it goes, leaves a clean tree, and never publishes itself. An agent finds it as a skill of its harness in every checkout the package creates; nothing has to be put in a prompt. diff --git a/packages/skill-branches/package.json b/packages/skill-branches/package.json index 887e41cf5..0ec70eb39 100644 --- a/packages/skill-branches/package.json +++ b/packages/skill-branches/package.json @@ -34,11 +34,14 @@ } }, "scripts": { - "build": "tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", + "build": "pnpm -C ../agent-data build && tsc -p tsconfig.build.json", + "typecheck": "pnpm -C ../agent-data build && tsc --noEmit", "test": "pnpm build && tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'", "clean": "rm -rf dist dist-test" }, + "dependencies": { + "@gemstack/agent-data": "workspace:*" + }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^7.0.2" diff --git a/packages/skill-branches/src/branch-links.test.ts b/packages/skill-branches/src/branch-links.test.ts index cc842d045..9d1622e28 100644 --- a/packages/skill-branches/src/branch-links.test.ts +++ b/packages/skill-branches/src/branch-links.test.ts @@ -3,9 +3,9 @@ import { test } from 'node:test' import { join } from 'node:path' import { tmpdir } from 'node:os' import { mkdir, mkdtemp, readdir, realpath, rm, writeFile } from 'node:fs/promises' -import { nodeGitRunner } from './git.js' +import { nodeGitRunner, BRANCHES_DIR } from '@gemstack/agent-data' import { reconcileBranchLinks, type LinksFs } from './branch-links.js' -import { BRANCHES_DIR, addWorktree, worktreePath, type WorktreeDirEntry } from './index.js' +import { addWorktree, worktreePath, type WorktreeDirEntry } from './index.js' const CWD = '/repo' const LINKS = join(CWD, BRANCHES_DIR) diff --git a/packages/skill-branches/src/branch-links.ts b/packages/skill-branches/src/branch-links.ts index 32018c36a..4d1669c37 100644 --- a/packages/skill-branches/src/branch-links.ts +++ b/packages/skill-branches/src/branch-links.ts @@ -1,6 +1,6 @@ import { basename, join } from 'node:path' -import { nodeGitRunner, type GitRunner } from './git.js' -import { BRANCHES_DIR, isAgentBranch } from './branch-names.js' +import { nodeGitRunner, type GitRunner, BRANCHES_DIR } from '@gemstack/agent-data' +import { isAgentBranch } from './branch-names.js' import { worktreeDirEntries, worktreeBranch, type WorktreeDirEntry } from './worktree.js' // The branches view (#1580): every checkout under `.branches/` is a directory named as the branch diff --git a/packages/skill-branches/src/branch-names.SPEC.md b/packages/skill-branches/src/branch-names.SPEC.md index d872c5a6a..a9c2c7bc4 100644 --- a/packages/skill-branches/src/branch-names.SPEC.md +++ b/packages/skill-branches/src/branch-names.SPEC.md @@ -1,13 +1,9 @@ -The naming rules for everything the package mints in git, and the layout they imply on disk — the `.branches` directory at a project's root where every checkout lives, and the charset an agent id may use, so no id can build a path outside that directory — kept in one place so every surface names branches identically. Every branch the package mints starts with `agent-` — slash-free on purpose: a `/` in a ref name never resolves as a cloud session's revision, and slash-free names are what let each checkout directory under `.branches/` be named exactly as its branch. An agent's checkout is created on `agent-` — from the agent id, because the id exists before the session name does — and the branch is renamed to `agent-` once the agent picks a name. The session name is read off the branch, never recorded beside it: `agent-` minus the prefix. A branch carries no name while it is still the one the checkout was created on — the agent is unnamed until it renames its branch — and neither does a branch the package did not mint. +The naming rules for everything the package mints in git, and the layout they imply on disk under `.branches/`, the directory at a project's root where every checkout lives (the `@gemstack/agent-data` package's convention) — and the charset an agent id may use, so no id can build a path outside that directory — kept in one place so every surface names branches identically. Every branch the package mints starts with `agent-` — slash-free on purpose: a `/` in a ref name never resolves as a cloud session's revision, and slash-free names are what let each checkout directory under `.branches/` be named exactly as its branch. An agent's checkout is created on `agent-` — from the agent id, because the id exists before the session name does — and the branch is renamed to `agent-` once the agent picks a name. The session name is read off the branch, never recorded beside it: `agent-` minus the prefix. A branch carries no name while it is still the one the checkout was created on — the agent is unnamed until it renames its branch — and neither does a branch the package did not mint. The same rules answer the filesystem questions around `.branches/`: an agent's checkout directory carries the name of the branch it was created on, so the flat listing reads as branch names, and the agent id is recoverable from a directory name. They also answer which branches the package may ever rename or delete on its own: those it minted for an agent, `agent-` or `agent-`. A branch of the user's own is out of scope by name alone, so no cleanup can reach it however empty it looks. -## Rationale - -The directory is dotted so that tools run in the project — a type-checker's `**/*`, a test runner's glob, a formatter — never descend into the checkouts: a leading dot is what `*` does not match, and it is the one thing that keeps N copies of the repository out of every tool that does not read git's ignore rules. - ## Before modifying/creating SPEC.md files You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/skill-branches/src/branch-names.ts b/packages/skill-branches/src/branch-names.ts index 8cdfe3af7..b30965542 100644 --- a/packages/skill-branches/src/branch-names.ts +++ b/packages/skill-branches/src/branch-names.ts @@ -1,16 +1,9 @@ /** - * The naming rules for everything the package mints in git, and the layout they imply on disk. + * The naming rules for everything the package mints in git, and the layout they imply under + * `.branches/` (the directory is `@gemstack/agent-data`'s convention). * Pure: no node imports, so browser-safe code can name branches too. */ -/** - * Where a project's agent checkouts live: `/.branches/`, one directory per checkout, each - * named as the branch it was created on. Dotted on purpose: a `*` glob does not match a leading - * dot, so type-checkers, test runners and formatters run in the project never descend into N - * copies of the repository. - */ -export const BRANCHES_DIR = '.branches' - /** An agent id is path-safe: no separators or traversal, only our own charset. */ export function isSafeAgentId(id: string): boolean { return /^[A-Za-z0-9_-]+$/.test(id) diff --git a/packages/skill-branches/src/checkout.ts b/packages/skill-branches/src/checkout.ts index 8ed5c29e9..7ea35761d 100644 --- a/packages/skill-branches/src/checkout.ts +++ b/packages/skill-branches/src/checkout.ts @@ -1,9 +1,8 @@ -import { nodeGitRunner, type GitRunner } from './git.js' -import { agentBranchName, BRANCHES_DIR } from './branch-names.js' +import { nodeGitRunner, type GitRunner, BRANCHES_DIR, excludeFromGit } from '@gemstack/agent-data' +import { agentBranchName } from './branch-names.js' import { addWorktree, attachWorktree, type AddedWorktree } from './worktree.js' import { linkDependencies } from './worktree-deps.js' import { reconcileBranchLinks } from './branch-links.js' -import { excludeFromGit } from './git-exclude.js' import { linkSkill, type SkillLink } from './skill-links.js' /** diff --git a/packages/skill-branches/src/cli.test.ts b/packages/skill-branches/src/cli.test.ts index 0a6a6f311..f387344bd 100644 --- a/packages/skill-branches/src/cli.test.ts +++ b/packages/skill-branches/src/cli.test.ts @@ -4,7 +4,8 @@ import { execFile } from 'node:child_process' import { join } from 'node:path' import { tmpdir } from 'node:os' import { lstat, mkdir, mkdtemp, readFile, readlink, realpath, rm, stat, writeFile } from 'node:fs/promises' -import { CLI_BIN_DIR, agentBranchName, nodeGitRunner, runCli, worktreePath } from './index.js' +import { nodeGitRunner } from '@gemstack/agent-data' +import { CLI_BIN_DIR, agentBranchName, runCli, worktreePath } from './index.js' // #1725: the command line is the package's functions for an agent in a shell, so every command // is checked against real git the way the functions are — and the contract on top of them: JSON diff --git a/packages/skill-branches/src/cli.ts b/packages/skill-branches/src/cli.ts index f15e803e2..9ecabbe10 100644 --- a/packages/skill-branches/src/cli.ts +++ b/packages/skill-branches/src/cli.ts @@ -1,7 +1,7 @@ import { parseArgs } from 'node:util' import { resolve } from 'node:path' import { stat } from 'node:fs/promises' -import { nodeGitRunner, checkoutRoot, gitReason, type GitRunner } from './git.js' +import { nodeGitRunner, checkoutRoot, gitReason, type GitRunner } from '@gemstack/agent-data' import { agentBranchName, isSafeAgentId } from './branch-names.js' import { branchPushed, diff --git a/packages/skill-branches/src/index.SPEC.md b/packages/skill-branches/src/index.SPEC.md index 62cf0b3d5..f8469ce5c 100644 --- a/packages/skill-branches/src/index.SPEC.md +++ b/packages/skill-branches/src/index.SPEC.md @@ -1,4 +1,4 @@ -The package's main entry point: it gathers the git runner, the naming and layout conventions, the checkout lifecycle, the checkout-as-an-agent-gets-it sequence, the dependency linking, the branch-name links, the skill links, the branch used as a file store, the reclaim rule, the command line and the executable's directory into one place for a caller to import. No business logic of its own. The naming conventions are also reachable on their own (`branch-names`), for code that runs in a browser and must not pull in git. +The package's main entry point: it gathers the naming conventions, the checkout lifecycle, the checkout-as-an-agent-gets-it sequence, the dependency linking, the branch-name links, the skill links, the reclaim rule, the command line and the executable's directory into one place for a caller to import. What the package builds on — running git, the exclude rule, the `.branches` name — is imported from `@gemstack/agent-data`, never passed through. No business logic of its own. The naming conventions are also reachable on their own (`branch-names`), for code that runs in a browser and must not pull in git. ## Before modifying/creating SPEC.md files diff --git a/packages/skill-branches/src/index.ts b/packages/skill-branches/src/index.ts index 2226d7458..f0252a02c 100644 --- a/packages/skill-branches/src/index.ts +++ b/packages/skill-branches/src/index.ts @@ -1,14 +1,4 @@ export { - type GitRunner, - GitTimeoutError, - isGitTimeout, - nodeGitRunner, - isGitRepo, - gitReason, - pushBranch, -} from './git.js' -export { - BRANCHES_DIR, AGENT_BRANCH_PREFIX, isSafeAgentId, agentBranchName, @@ -40,31 +30,10 @@ export { type DirReader, } from './worktree.js' export { linkDependencies, findDependencyDirs, nodeLinkFs, type LinkFs } from './worktree-deps.js' -export { excludeFromGit, type ExcludeFs } from './git-exclude.js' export { reconcileBranchLinks, type LinksFs, type BranchLinksDeps } from './branch-links.js' export { reclaimWorktree, type ReclaimOptions, type ReclaimOutcome, type ReclaimRefusal } from './reclaim.js' -export { checkoutRoot } from './git.js' export { projectRoot, nameBranch, isSessionName, type NameBranchOutcome, type NameBranchRefusal } from './worktree.js' export { createCheckout, attachCheckout, type CheckoutSkills } from './checkout.js' export { runCli, USAGE, type CliIo, type CliRefusal } from './cli.js' export { CLI_BIN_DIR } from './bin-dir.js' export { linkSkill, HARNESS_SKILL_DIRS, SKILL_DIR, SKILL_NAME, OWN_SKILL, type SkillLink } from './skill-links.js' -export { - fileBranchPath, - fileBranchRepo, - ensureFileBranch, - withFileBranch, - pullFileBranch, - readBranchFile, - listBranchDir, - openBranchReader, - writeFileBranchDetached, - nodeBranchFileFs, - type FileBranchDeps, - type FileBranchWrite, - type FileBranchSync, - type CommitMessage, - type DetachedWrite, - type BranchFileFs, - type BranchReader, -} from './file-branch.js' diff --git a/packages/skill-branches/src/reclaim.test.ts b/packages/skill-branches/src/reclaim.test.ts index b97c71730..28e2728f5 100644 --- a/packages/skill-branches/src/reclaim.test.ts +++ b/packages/skill-branches/src/reclaim.test.ts @@ -3,7 +3,8 @@ import { test } from 'node:test' import { join } from 'node:path' import { tmpdir } from 'node:os' import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises' -import { addWorktree, agentBranchName, nodeGitRunner, reclaimWorktree, type ReclaimOptions } from './index.js' +import { nodeGitRunner } from '@gemstack/agent-data' +import { addWorktree, agentBranchName, reclaimWorktree, type ReclaimOptions } from './index.js' // #982: one rule decides every removal — the checkout goes only once the remote has it. So // nothing local is ever the last copy of anything, and the one failure mode is legible: the push diff --git a/packages/skill-branches/src/reclaim.ts b/packages/skill-branches/src/reclaim.ts index acc5c823a..ccd63dfd1 100644 --- a/packages/skill-branches/src/reclaim.ts +++ b/packages/skill-branches/src/reclaim.ts @@ -1,4 +1,4 @@ -import { nodeGitRunner, pushBranch, type GitRunner } from './git.js' +import { nodeGitRunner, pushBranch, type GitRunner } from '@gemstack/agent-data' import { isAgentBranch } from './branch-names.js' import { branchPushed, diff --git a/packages/skill-branches/src/skill-links.test.ts b/packages/skill-branches/src/skill-links.test.ts index 606bbcfc4..73722aa9d 100644 --- a/packages/skill-branches/src/skill-links.test.ts +++ b/packages/skill-branches/src/skill-links.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { join } from 'node:path' import { mkdtemp, readFile, realpath, rm, unlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { nodeGitRunner } from './git.js' +import { nodeGitRunner } from '@gemstack/agent-data' import { createCheckout } from './checkout.js' import { HARNESS_SKILL_DIRS, linkSkill, SKILL_DIR, SKILL_NAME } from './skill-links.js' diff --git a/packages/skill-branches/src/skill-links.ts b/packages/skill-branches/src/skill-links.ts index c12ee52a2..11a22a9ec 100644 --- a/packages/skill-branches/src/skill-links.ts +++ b/packages/skill-branches/src/skill-links.ts @@ -1,7 +1,6 @@ import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import { nodeGitRunner, type GitRunner } from './git.js' -import { excludeFromGit } from './git-exclude.js' +import { nodeGitRunner, type GitRunner, excludeFromGit } from '@gemstack/agent-data' import { nodeLinkFs, type LinkFs } from './worktree-deps.js' /** diff --git a/packages/skill-branches/src/worktree.test.ts b/packages/skill-branches/src/worktree.test.ts index c007f8616..816c4d531 100644 --- a/packages/skill-branches/src/worktree.test.ts +++ b/packages/skill-branches/src/worktree.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { join } from 'node:path' import { mkdir, mkdtemp, rm, writeFile, stat, realpath, symlink } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { nodeGitRunner, type GitRunner } from './git.js' +import { nodeGitRunner, type GitRunner, BRANCHES_DIR } from '@gemstack/agent-data' import { nameBranch, addWorktree, @@ -20,7 +20,7 @@ import { currentBranch, listWorktreeDirs, } from './worktree.js' -import { agentBranchName, BRANCHES_DIR } from './branch-names.js' +import { agentBranchName } from './branch-names.js' const REPO = '/repo' diff --git a/packages/skill-branches/src/worktree.ts b/packages/skill-branches/src/worktree.ts index 8ceec14f2..2d8bf7d19 100644 --- a/packages/skill-branches/src/worktree.ts +++ b/packages/skill-branches/src/worktree.ts @@ -1,7 +1,7 @@ import { basename, dirname, join } from 'node:path' import { realpath } from 'node:fs/promises' -import { nodeGitRunner, checkoutRoot, type GitRunner } from './git.js' -import { BRANCHES_DIR, AGENT_BRANCH_PREFIX, isSafeAgentId, isAgentBranch, agentBranchName, agentIdFromWorktreeDir } from './branch-names.js' +import { nodeGitRunner, checkoutRoot, type GitRunner, BRANCHES_DIR } from '@gemstack/agent-data' +import { AGENT_BRANCH_PREFIX, isSafeAgentId, isAgentBranch, agentBranchName, agentIdFromWorktreeDir } from './branch-names.js' /** * Git-worktree lifecycle for concurrent agents (#453/#735): give each agent its own diff --git a/packages/skill-tickets/DECISIONS.md b/packages/skill-tickets/DECISIONS.md new file mode 100644 index 000000000..9db658f1e --- /dev/null +++ b/packages/skill-tickets/DECISIONS.md @@ -0,0 +1,51 @@ +Non-obvious decisions only, grouped by business-logic flow. Anything not listed is left +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 with the same name plus `.plan.md` and `.lock.md`. + The shared name is the only link between them. +- Tickets live on the data branch of `@gemstack/agent-data`, never on a code branch: an + agent's checkout has no `tickets/` folder, and a pull request never carries a ticket. + For a person, a `tickets` shortcut at the project root points into the data branch's + checkout; it is created only if nothing of that name is there, and git is told to + ignore it. +- `tickets/` holds only open tickets: closing one deletes it, with its plan and its claim. +- The skill knows no issue tracker. A ticket may carry a `GitHub:` line with its issue, + but importing issues into tickets is done by the program using the skill, not by the + skill. + +## Flow: a claim +- A claim is a committed file holding one line, `CLAIMED: `, so that agents on other + machines see it too. +- One claim per ticket; it never expires, only a release, or a close by whoever holds it, + removes it. Whoever started an agent releases what it left claimed. +- A lock is written only by a claim; `put` refuses `.lock.md`. +- The holder's name is never typed; the command reads it from where it runs. Inside an + agent's checkout it is the agent id from the folder name, since the branch gets renamed + and the folder does not; anywhere else it is the current branch. +- The program says what a claim is for. A claim for planning is skipped, no lock written, + when the ticket already has a plan; a claim for implementing is not. + +## 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. Work is taken + from the top: highest section first, first line first. +- An entry is plain text: the task a future agent is started with. It may be a link to a + ticket, but the queue does not read links; it only stores and removes lines. The + program that starts agents reads the link, to claim the ticket for the agent it starts + on that entry. +- An entry added with no priority goes to the end of the file; one linked to a ticket + takes the ticket's priority, 5 when the ticket has none. +- Done means deleted, never checked off. + +## Flow: the command +- The command reads with `list`, `show` and `queue`, and writes with `put` (a ticket, a + plan, or `meta.json`, the importing program's own bookkeeping, opaque to the skill), + `close`, `claim`, `release`, `queue add` and `queue done`. +- A read fetches the branch from origin, the shared copy on the remote, once, and reads + everything from that copy rather than from the local branch: only origin is sure to + hold what every writer pushed, the command's own earlier writes included. +- A write is one commit per command, pushed straight to origin through a temporary copy + of the branch (the rule is in `@gemstack/agent-data`). A repository with no remote is + refused. diff --git a/packages/skill-tickets/SKILL.SPEC.md b/packages/skill-tickets/SKILL.SPEC.md index 555169e45..bcde99abc 100644 --- a/packages/skill-tickets/SKILL.SPEC.md +++ b/packages/skill-tickets/SKILL.SPEC.md @@ -7,7 +7,7 @@ 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 `tickets` 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** - every change it makes is one commit pushed straight to the `tickets` branch; nothing the agent commits on its own branch reaches them, and these files never belong on the agent's branch. +- **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 `tickets` 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. diff --git a/packages/skill-tickets/SKILL.md b/packages/skill-tickets/SKILL.md index 8293e4b86..9888220b4 100644 --- a/packages/skill-tickets/SKILL.md +++ b/packages/skill-tickets/SKILL.md @@ -7,36 +7,37 @@ description: The project's tickets and its agent queue: where they live, how to The tickets (`tickets/_.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 re-applied on the branch's new tip and pushed again, for you); nothing you commit on your own branch reaches them, and these files never belong on your branch. +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 `tickets` branch (a rejected push is re-applied on the branch's new tip and pushed again, for you); these files never belong on your branch. ## Read ``` -tickets list every open ticket, as JSON: file, title, summary, priority, topics, +npx tickets list every open ticket, as JSON: file, title, summary, priority, topics, github, date, planned, effort, uncertainty, locked, lockedBy -tickets show one ticket: its text, its plan, who holds it -tickets queue the queue's open entries, in order of work +npx tickets show one ticket: its text, its plan, who holds it +npx tickets queue the queue's open entries, in order of work ``` ## Change ``` -tickets put write one file under tickets/ from stdin (a ticket, a plan, meta.json) -tickets close remove a ticket with its plan and lock — tickets/ holds only open +npx tickets put write one file under tickets/ from stdin: a ticket, a plan, or meta.json + (whatever the program importing issues keeps there) +npx tickets close remove a ticket with its plan and lock — tickets/ holds only open tickets; refused while someone else holds the ticket -tickets queue add [--priority N] [--ticket ] +npx tickets queue add [--priority N] [--ticket ] put an entry on the queue; --ticket links it to the ticket and places it by the ticket's priority -tickets queue done take an entry off the queue: done means deleted +npx tickets queue done take an entry off the queue, as `npx tickets queue` printed it: done means deleted ``` ## Claim before you plan or work a ticket ``` -tickets claim {"ok":true,"holder":…} — the ticket is yours +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 -tickets release lift your own claim (a finished plan, work that is published) +npx tickets release lift your own claim (a finished plan, work that is published) ``` `` is the ticket's filename, e.g. `2042-01-01_some-ticket.md`. @@ -65,11 +66,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` (`tickets close`). +`tickets/` holds only open tickets: a closed ticket is removed, with its `.plan.md` and `.lock.md` (`npx tickets close`). ### A claim: `tickets/_.lock.md` -Written by `tickets claim`, removed by `tickets release` or `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 ticket with a lock is being planned or worked by its holder — pick another. ### A plan: `tickets/_.plan.md` @@ -136,4 +137,4 @@ Notes: ... ``` -The queue lists *all* tasks AI will work on next, sorted by priority. Priority 10 is rarely used (e.g. critical production bugs) and is treated as the utmost priority. Within a priority, the first tasks have higher priority (they are the "next" tasks within that "priority queue"). A done entry is removed (`tickets queue done`). +The queue lists *all* tasks AI will work on next, sorted by priority. Priority 10 is rarely used (e.g. critical production bugs) and is treated as the utmost priority. Within a priority, the first tasks have higher priority (they are the "next" tasks within that "priority queue"). A done entry is removed (`npx tickets queue done`). diff --git a/packages/skill-tickets/SPEC.md b/packages/skill-tickets/SPEC.md index 93dd7c868..fdb95d4b4 100644 --- a/packages/skill-tickets/SPEC.md +++ b/packages/skill-tickets/SPEC.md @@ -2,7 +2,7 @@ Tickets and the agent queue for coding agents, as an npm package: markdown ticke The package knows git, the filesystem and the ticket format, nothing else. The same functions serve every caller: a long-lived process (a daemon that starts agents, a dashboard that lists the roadmap) that keeps a checkout of the branch, reads the tickets and claims them for the agents it starts, and an agent's own shell, through the `tickets` command a caller puts on the PATH of every agent it starts on its machine. What a caller knows beyond git — which agent it started, whether that agent ended with nothing, which ticket it wants planned next — is passed in; the package never reads a caller's records. -The branch is a file store, the primitive `@gemstack/skill-branches` provides: a branch nobody edits in a working tree, safe to push and pull eagerly. It gives this package two writers over one rule. A long-lived process writes through its persistent checkout, `.branches/tickets`, in one serialized cycle per branch — sync with origin, apply, commit, push — and pulls the same way so it reads what other machines pushed. The `tickets` command writes as a one-shot remote writer instead: a throwaway checkout of origin's tip, one commit, pushed straight to the branch, gone afterwards — so a command an agent runs never touches a checkout that belongs to another process. Both treat a change as an intent: a push that loses a race is re-applied against the fresher state rather than forced. +The branch is a file store, the primitive `@gemstack/agent-data` provides: a branch nobody edits in a working tree, safe to push and pull eagerly. It gives this package two writers over one rule. A long-lived process writes through its persistent checkout, `.branches/tickets`, in one serialized cycle per branch — sync with origin, apply, commit, push — and pulls the same way so it reads what other machines pushed. The `tickets` command writes as a one-shot remote writer instead: a throwaway checkout of origin's tip, one commit, pushed straight to the branch, gone afterwards — so a command an agent runs never touches a checkout that belongs to another process. Both treat a change as an intent: a push that loses a race is re-applied against the fresher state rather than forced. ## Glossary diff --git a/packages/skill-tickets/package.json b/packages/skill-tickets/package.json index 2ba0984f9..fc85b07f5 100644 --- a/packages/skill-tickets/package.json +++ b/packages/skill-tickets/package.json @@ -41,6 +41,7 @@ "clean": "rm -rf dist dist-test" }, "dependencies": { + "@gemstack/agent-data": "workspace:*", "@gemstack/skill-branches": "workspace:*" }, "devDependencies": { diff --git a/packages/skill-tickets/src/cli.test.ts b/packages/skill-tickets/src/cli.test.ts index 0f993ab02..9831a1667 100644 --- a/packages/skill-tickets/src/cli.test.ts +++ b/packages/skill-tickets/src/cli.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' import { runCli, USAGE } from './cli.js' import { TICKETS_BRANCH } from './names.js' diff --git a/packages/skill-tickets/src/cli.ts b/packages/skill-tickets/src/cli.ts index 5c832456f..d0fcfab4e 100644 --- a/packages/skill-tickets/src/cli.ts +++ b/packages/skill-tickets/src/cli.ts @@ -1,15 +1,6 @@ import { parseArgs } from 'node:util' import { join } from 'node:path' -import { - checkoutRoot, - gitReason, - nodeBranchFileFs, - nodeGitRunner, - openBranchReader, - writeFileBranchDetached, - type BranchReader, - type GitRunner, -} from '@gemstack/skill-branches' +import { checkoutRoot, gitReason, nodeBranchFileFs, nodeGitRunner, openBranchReader, writeFileBranchDetached, type BranchReader, type GitRunner } from '@gemstack/agent-data' import { isTicketFile, isTicketPath, META_FILE, QUEUE_FILE, TICKETS_BRANCH, TICKETS_DIR, queuePriorityForTicket, ticketLockName, ticketPlanName, ticketStem } from './names.js' import { readTicket, readTickets, type TicketsFs } from './tickets.js' import { applyClaims, applyRelease, claimMessage, lockHolder, releaseMessage } from './locks.js' diff --git a/packages/skill-tickets/src/holder.ts b/packages/skill-tickets/src/holder.ts index 601a9e1d9..5bb08ecad 100644 --- a/packages/skill-tickets/src/holder.ts +++ b/packages/skill-tickets/src/holder.ts @@ -1,5 +1,6 @@ import { basename, dirname } from 'node:path' -import { agentIdFromWorktreeDir, BRANCHES_DIR, checkoutRoot, currentBranch, isAgentBranch, nodeGitRunner, type GitRunner } from '@gemstack/skill-branches' +import { BRANCHES_DIR, checkoutRoot, nodeGitRunner, type GitRunner } from '@gemstack/agent-data' +import { agentIdFromWorktreeDir, currentBranch, isAgentBranch } from '@gemstack/skill-branches' /** * Who the command claims as, read from where it runs — nothing for the agent to type or know. diff --git a/packages/skill-tickets/src/locks.test.ts b/packages/skill-tickets/src/locks.test.ts index 9fcb0f2ef..889e0215a 100644 --- a/packages/skill-tickets/src/locks.test.ts +++ b/packages/skill-tickets/src/locks.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert' import { test } from 'node:test' import { join } from 'node:path' -import type { FileBranchWrite } from '@gemstack/skill-branches' +import type { FileBranchWrite } from '@gemstack/agent-data' import { claimTickets, releaseTicket, lockContent, lockHolder, claimMessage, releaseMessage } from './locks.js' import type { TicketDeps, TicketsFunnel } from './store.js' diff --git a/packages/skill-tickets/src/names.ts b/packages/skill-tickets/src/names.ts index 75f7ba6e9..f11e0049d 100644 --- a/packages/skill-tickets/src/names.ts +++ b/packages/skill-tickets/src/names.ts @@ -1,4 +1,4 @@ -import { BRANCHES_DIR } from '@gemstack/skill-branches/branch-names' +import { BRANCHES_DIR } from '@gemstack/agent-data/names' /** * The names everything in the package hangs off, and the small pure rules that link a ticket to diff --git a/packages/skill-tickets/src/queue.test.ts b/packages/skill-tickets/src/queue.test.ts index 02b641eee..16dc3fcec 100644 --- a/packages/skill-tickets/src/queue.test.ts +++ b/packages/skill-tickets/src/queue.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { nodeGitRunner, withFileBranch } from '@gemstack/skill-branches' +import { nodeGitRunner, withFileBranch } from '@gemstack/agent-data' import { appendQueueEntry, insertQueueEntry, parseQueueEntries, queueAdd, queueDone, readQueue, readQueueEntries, removeQueueEntry } from './queue.js' import { QUEUE_FILE, TICKETS_BRANCH } from './names.js' diff --git a/packages/skill-tickets/src/queue.ts b/packages/skill-tickets/src/queue.ts index 286995fe4..220447b87 100644 --- a/packages/skill-tickets/src/queue.ts +++ b/packages/skill-tickets/src/queue.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import { fileBranchRepo, readBranchFile } from '@gemstack/skill-branches' +import { fileBranchRepo, readBranchFile } from '@gemstack/agent-data' import { QUEUE_FILE, TICKETS_BRANCH } from './names.js' import { resolveTicketDeps, type TicketDeps } from './store.js' diff --git a/packages/skill-tickets/src/store.test.ts b/packages/skill-tickets/src/store.test.ts index 202b9722f..a5e21ef1d 100644 --- a/packages/skill-tickets/src/store.test.ts +++ b/packages/skill-tickets/src/store.test.ts @@ -3,7 +3,7 @@ import { test } from 'node:test' import { mkdir, mkdtemp, readFile, readlink, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { nodeGitRunner } from '@gemstack/skill-branches' +import { nodeGitRunner } from '@gemstack/agent-data' import { syncTickets, ticketsCheckoutPath, ticketsDir } from './store.js' import { QUEUE_FILE, TICKETS_BRANCH, TICKETS_CHECKOUT_DIR } from './names.js' diff --git a/packages/skill-tickets/src/store.ts b/packages/skill-tickets/src/store.ts index 6b73a9b4c..030b9bc87 100644 --- a/packages/skill-tickets/src/store.ts +++ b/packages/skill-tickets/src/store.ts @@ -1,17 +1,5 @@ import { join } from 'node:path' -import { - excludeFromGit, - fileBranchPath, - nodeBranchFileFs, - nodeGitRunner, - pullFileBranch, - withFileBranch, - type BranchFileFs, - type CommitMessage, - type FileBranchSync, - type FileBranchWrite, - type GitRunner, -} from '@gemstack/skill-branches' +import { excludeFromGit, fileBranchPath, nodeBranchFileFs, nodeGitRunner, pullFileBranch, withFileBranch, type BranchFileFs, type CommitMessage, type FileBranchSync, type FileBranchWrite, type GitRunner } from '@gemstack/agent-data' import { QUEUE_FILE, TICKETS_BRANCH, TICKETS_CHECKOUT_DIR, TICKETS_DIR } from './names.js' // Where the tickets live, bound to the branch: the `tickets` branch of the project's repository, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ed120515..1a3554714 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,15 @@ importers: specifier: ^7.0.2 version: 7.0.2 + packages/agent-data: + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.43 + typescript: + specifier: ^7.0.2 + version: 7.0.2 + packages/agent-driver: devDependencies: '@types/node': @@ -26,6 +35,9 @@ importers: '@base-ui-components/react': specifier: 1.0.0-rc.0 version: 1.0.0-rc.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@gemstack/agent-data': + specifier: workspace:* + version: link:../agent-data '@gemstack/skill-branches': specifier: workspace:* version: link:../skill-branches @@ -122,6 +134,10 @@ importers: version: 4.1.10(@types/node@20.19.43)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages/skill-branches: + dependencies: + '@gemstack/agent-data': + specifier: workspace:* + version: link:../agent-data devDependencies: '@types/node': specifier: ^20.0.0 @@ -132,6 +148,9 @@ importers: packages/skill-tickets: dependencies: + '@gemstack/agent-data': + specifier: workspace:* + version: link:../agent-data '@gemstack/skill-branches': specifier: workspace:* version: link:../skill-branches