Skip to content

Commit 9b96d44

Browse files
authored
The branch-management command line (#1725) (#1727)
* The branch-management command line (#1725) Step 2 of #1725: the package's operations as a CLI for the agent's shell — create, attach, name, status, list, remove, prune — JSON on stdout, the reason on stderr, exit 1 for a refusal and 2 for a usage error. The daemon puts the package's bin directory first on every spawned agent's PATH. Naming is a rename of the checkout's branch (tf-agent-<id> -> tf-<name>), refused for a branch The Framework did not mint, suffixed when taken. * Review: one naming path, the project from its layout, reserved names, a renamed branch's own upstream - The framework's session-name handler names the branch by the package's rule (nameBranch); renameAgentBranch is gone. - The project is the checkout whose .the-framework/branches/ the cwd is under, not git's common dir: a project that is itself a linked worktree keeps its own checkouts. - `name`: reserved names (data, agent-…) refused; asking again for the name the checkout already carries (suffixed or pushed) is a no-op; a rename lost to a sibling's race takes the next suffix. - reclaim: a branch's tracked upstream is its own copy — a branch renamed after a push is pushed under its new name, never read as empty. - createCheckout/attachCheckout: one sequence for the daemon and the CLI. - CLI: prototype keys are not commands; id checked before the project is looked for; prune reconciles once; only git's 'not a git repository' reads as not-a-repo. - SPEC wording: the PATH is for agents the daemon starts on its machine.
1 parent 6a40efd commit 9b96d44

33 files changed

Lines changed: 1110 additions & 93 deletions

SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ The Framework: autonomous AI programming. Humans make the important decisions; c
33
Four top-level pieces, one product:
44

55
- `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.
6-
- `packages/branch-management` — the git conventions and operations behind an agent's own checkout, published as `@superskill/branch-management`: the first of the skills the product is being split into, and the only one so far. The product depends on it; it depends on nothing of the product.
6+
- `packages/branch-management` — the git conventions and operations behind an agent's own checkout, as an API and as the `branch-management` command every agent the daemon starts on its machine gets on its PATH, published as `@superskill/branch-management`: the first of the skills the product is being split into, and the only one so far. The product depends on it; it depends on nothing of the product.
77
- `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.
88
- `packages/the-framework.ai` — the marketing website at https://the-framework.ai.
99

packages/branch-management/SPEC.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
Branch management for coding agents, as an npm package: one git checkout per agent under a project's `.the-framework/branches/`, named as its branch; the parent checkout's dependencies shared into it; a navigable link per branch name; and one retention rule under which a checkout is reclaimed — only once everything it holds is on the remote.
22

3-
The package knows git and the filesystem, nothing else. It is the first skill of The Framework's skills-plus architecture (#1725): the same functions serve every caller — The Framework's daemon (allocation, teardown, the reclaim sweep), its dashboard (the retained-checkouts list, the Remove and Prune buttons), and, later, an agent's own command line. What a caller knows beyond git — whether an agent is still running, whether its handoff allows a push, what a cloud hand-off already pushed — is passed in; the package never reads an agent's record.
3+
The package knows git and the filesystem, nothing else. It is the first skill of The Framework's skills-plus architecture (#1725): the same functions serve every caller — The Framework's daemon (allocation, teardown, the reclaim sweep), its dashboard (the retained-checkouts list, the Remove and Prune buttons), and an agent's own shell, through the `branch-management` command the 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 its handoff allows a push, what a cloud hand-off already pushed — is passed in; the package never reads an agent's record.
44

55
## Business logic — TL;DR
66

77
- **The conventions** (`branch-names`) - branch names, the checkout directory layout under `.the-framework/branches/`, and the agent-id charset every path is built from.
88
- **Running git** (`git`) - one runner with a time budget per subcommand, and a timeout told apart from a git failure.
9-
- **A checkout's lifecycle** (`worktree`) - create, attach, list, rename, remove, prune; the reads every retention decision is built on.
9+
- **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.
10+
- **A checkout as an agent gets it** (`checkout`) - the worktree, the dependencies linked in, the links refreshed: one sequence for the daemon and the command line.
1011
- **Dependencies shared, not copied** (`worktree-deps`) - a fresh checkout gets the parent's dependency trees as directories of links.
1112
- **Reachable by branch name** (`branch-links`, `git-exclude`) - a symlink per current branch name beside the checkouts, and a `branches` shortcut at the repo root, hidden from git.
1213
- **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 a framework-minted branch that holds nothing.
14+
- **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.
1315

1416
## Before modifying/creating SPEC.md files
1517

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The `branch-management` executable: it hands the shell's arguments, working directory and streams to the command line (`src/cli`) and exits with the code it returns. No logic of its own.
2+
3+
## Before modifying/creating SPEC.md files
4+
5+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env node
2+
import { runCli } from '../dist/cli.js'
3+
4+
runCli(process.argv.slice(2), { cwd: process.cwd(), stdout: line => console.log(line), stderr: line => console.error(line) })
5+
.then(code => {
6+
process.exitCode = code
7+
})
8+
.catch(err => {
9+
console.error(err)
10+
process.exitCode = 1
11+
})

packages/branch-management/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616
"access": "public"
1717
},
1818
"files": [
19-
"dist"
19+
"dist",
20+
"bin"
2021
],
22+
"bin": {
23+
"branch-management": "./bin/branch-management"
24+
},
2125
"exports": {
2226
".": {
2327
"types": "./dist/index.d.ts",
@@ -27,7 +31,7 @@
2731
"scripts": {
2832
"build": "tsc -p tsconfig.build.json",
2933
"typecheck": "tsc --noEmit",
30-
"test": "tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'",
34+
"test": "pnpm build && tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'",
3135
"clean": "rm -rf dist dist-test"
3236
},
3337
"devDependencies": {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Where the `branch-management` executable lives, for a caller that puts it on a spawned process's PATH: The Framework's daemon does so for every agent it starts on its machine, so the agent's shell finds the command by name (an agent running on a GitHub runner or in a cloud sandbox is started elsewhere and gets no such PATH). The directory sits beside the compiled code, so the same path holds from the monorepo and from an installed package.
2+
3+
## Before modifying/creating SPEC.md files
4+
5+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { fileURLToPath } from 'node:url'
2+
3+
/**
4+
* Where the `branch-management` executable lives, for a caller that puts it on a spawned
5+
* process's PATH — the daemon, for every agent it starts (#1725). Beside `dist/`, so it is the
6+
* same path from a workspace checkout and from an installed package.
7+
*/
8+
export const CLI_BIN_DIR = fileURLToPath(new URL('../bin/', import.meta.url))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
A checkout as an agent gets it: the worktree on its branch, the parent checkout's dependency directories linked in, and the `branches/` links brought up to date — one sequence for a new agent (a fresh `tf-agent-<agent id>` branch, from a stated base or the project's head) and for a continued one (back on the branch its work is on). The daemon allocating a run and the command line both go through it, so the two never differ in what a checkout starts with. The linking and the links are best-effort: a checkout without its dependencies is a worse run, not a failed one, and a missing link is made by the next reconcile pass.
2+
3+
## Before modifying/creating SPEC.md files
4+
5+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { nodeGitRunner, type GitRunner } from './git.js'
2+
import { agentBranchName } from './branch-names.js'
3+
import { addWorktree, attachWorktree, type AddedWorktree } from './worktree.js'
4+
import { linkDependencies } from './worktree-deps.js'
5+
import { reconcileBranchLinks } from './branch-links.js'
6+
7+
/**
8+
* A checkout as an agent gets it (#1725): the worktree, the parent's dependency trees linked in,
9+
* and the `branches/` links brought up to date — one sequence, whichever surface asks for it (the
10+
* daemon allocating a run, the command line).
11+
*/
12+
13+
/** A new agent's checkout, on a fresh `tf-agent-<id>` branch from `base` or the project's head. */
14+
export async function createCheckout(
15+
repo: string,
16+
opts: { agentId: string; base?: string },
17+
git: GitRunner = nodeGitRunner(),
18+
): Promise<AddedWorktree> {
19+
const worktree = await addWorktree(repo, { agentId: opts.agentId, branch: agentBranchName(opts.agentId), ...(opts.base ? { base: opts.base } : {}) }, git)
20+
await settle(repo, worktree.path, git)
21+
return worktree
22+
}
23+
24+
/** A continued agent's checkout, back on the branch its work is on. */
25+
export async function attachCheckout(
26+
repo: string,
27+
opts: { agentId: string; branch: string },
28+
git: GitRunner = nodeGitRunner(),
29+
): Promise<AddedWorktree> {
30+
const worktree = await attachWorktree(repo, opts, git)
31+
await settle(repo, worktree.path, git)
32+
return worktree
33+
}
34+
35+
/**
36+
* What a checkout gets besides its files. Both are best-effort: `node_modules` is gitignored, so
37+
* a fresh checkout has none and a link that cannot be made is a worse run, not a failed one; a
38+
* link under `branches/` is a view, and the next reconcile pass makes it.
39+
*/
40+
async function settle(repo: string, path: string, git: GitRunner): Promise<void> {
41+
await linkDependencies(repo, path).catch(() => [])
42+
await reconcileBranchLinks(repo, { git }).catch(() => {})
43+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
The package's command line: the same operations The Framework's daemon calls, for an agent (or a person) in a shell inside a project — so an agent names its session, checks its tree and reclaims checkouts through the one implementation the daemon uses, and a second surface is never a second behaviour.
2+
3+
## User story
4+
5+
- An agent, started inside its own checkout, names its session and learns the branch name it got.
6+
- An agent checks, before it finishes, that it has left nothing uncommitted.
7+
- The user, in a terminal, lists and reclaims the checkouts under a project without opening the dashboard.
8+
9+
## Glossary
10+
11+
- **refusal** - a rule saying no to a command: a dirty tree that may not be removed, a name that is not a session name, a directory that is not a checkout. Not a failure: the command ran, and the answer is "not this one".
12+
13+
## Business logic — TL;DR
14+
15+
- **Seven commands over the package** - `create`, `attach`, `name`, `status`, `list`, `remove`, `prune`; each is the corresponding package operation and nothing more.
16+
- **JSON out, a reason for a person, an exit code that says which** - every result is one JSON document on stdout; a refusal or a git failure also puts one line on stderr and exits 1; a command that cannot be read gets the usage on stderr and exits 2.
17+
- **The project is found from where the command runs** - commands acting on the project find it by the checkout layout, from anywhere inside it, an agent's checkout included; commands acting on a checkout act on the one they run in.
18+
- **What the package does not know is not decided here** - whether an agent is still running is not read; whether its branch may be pushed is `--no-push`, the caller's fact.
19+
20+
## Business logic
21+
22+
### Seven commands over the package
23+
24+
#### User story
25+
26+
See `## User story`.
27+
28+
#### Business logic
29+
30+
- `create <id> [--base <ref>]` - a checkout for the agent, on a fresh `tf-agent-<id>` branch from the stated base or the project's current head; the parent checkout's dependency directories are linked in and the `branches/` links are refreshed. Reports the checkout's path and branch.
31+
- `attach <id> <branch>` - a checkout for a continued agent on an existing branch, with the same linking. An id that is not path-safe is refused by both, before git runs.
32+
- `name <name>` - renames the branch of the checkout the command runs in to `tf-<name>`, refreshes the `branches/` links, and reports the name the branch ended up with — suffixed when the wanted one was taken. Refused for a name that is not `[a-z0-9-]+`, for one that spells The Framework's own branches (`data`, `agent-…`), and for a checkout on a branch The Framework did not mint, so the user's own branch is never renamed. The rules are the checkout lifecycle's (`worktree`).
33+
- `status [path]` - the branch, whether the tree is clean, and whether the branch tip is on the remote, for the checkout the command runs in or the one at the stated path. A path that is not a checkout of its own is refused.
34+
- `list [--sizes]` - every agent checkout under `.the-framework/branches/`: the agent id, the path, the branch it is on now, and, on request, its size on disk.
35+
- `remove <id> [--no-push]` - reclaims one agent's checkout under the reclaim rule (`reclaim`): a dirty tree is kept, the branch is pushed unless `--no-push`, the checkout goes only once the remote has it, and a branch that holds nothing goes with it. The `branches/` links are refreshed afterwards, so a link named after a branch that just went is dropped. An id that is not path-safe is refused before anything is looked up; an id with no checkout is refused as such.
36+
- `prune [--no-push]` - `remove` for every checkout, with the links refreshed once at the end rather than after each; reports which were removed and, for each kept, the reason. Exits 0: a checkout kept under the rule is the rule working, not a refusal of the command.
37+
38+
### JSON out, a reason for a person, an exit code that says which
39+
40+
#### User story
41+
42+
An agent parses what it is told; a person reads it; a script branches on the exit code.
43+
44+
#### Business logic
45+
46+
Every command writes exactly one JSON document to stdout. A result is the operation's outcome. A refusal is `{ ok: false, reason }` — the reason a short fixed word (`dirty`, `not-on-remote`, `invalid-name`, `not-a-run-branch`, `not-a-worktree`, `no-checkout`, `invalid-id`, `not-a-repo`, …) plus what identifies the case (the branch, the path, the id) — and one sentence on stderr says the same for a person; the exit code is 1. A git failure past the decision is reported the same way, reason `git-failed`, with git's own line. A command that cannot be read — unknown command, an argument missing or extra, an unknown option — gets the usage on stderr, no JSON, and exit code 2.
47+
48+
### The project is found from where the command runs
49+
50+
#### User story
51+
52+
An agent runs the commands from inside its own checkout, from whichever subdirectory it happens to be in.
53+
54+
#### Business logic
55+
56+
`create`, `attach`, `list`, `remove` and `prune` act on the project: the checkout whose `.the-framework/branches/` the working directory is under, else the checkout the working directory is in (the checkout lifecycle's rule, `worktree`). So the same command names the same project from the project's own checkout and from any agent checkout under it — including when the project is itself a linked worktree of some other repository. `name` and `status` act on a checkout: the root of the one the working directory is in, or, for `status`, the directory named on the command line. Outside any repository every command that looks for the project or the checkout is refused as `not-a-repo` rather than failing on git — only git's own "not a repository" reads as that; a timeout or a broken git stays the failure it is. `status <path>` answers about the named directory, which outside a repository is simply not a checkout.
57+
58+
## Before modifying/creating SPEC.md files
59+
60+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md

0 commit comments

Comments
 (0)