Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ The Framework: autonomous AI programming. Humans make the important decisions; c
Four 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/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.
- `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.
- `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.

Expand Down
6 changes: 4 additions & 2 deletions packages/branch-management/SPEC.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
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.

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.
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.

## Business logic — TL;DR

- **The conventions** (`branch-names`) - branch names, the checkout directory layout under `.the-framework/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, rename, remove, prune; the reads every retention decision is built on.
- **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, the dependencies linked in, the links refreshed: one sequence for the daemon and the command line.
- **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, and a `branches` shortcut at the repo root, hidden from git.
- **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.
- **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.

## Before modifying/creating SPEC.md files

Expand Down
5 changes: 5 additions & 0 deletions packages/branch-management/bin/SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
11 changes: 11 additions & 0 deletions packages/branch-management/bin/branch-management
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
import { runCli } from '../dist/cli.js'

runCli(process.argv.slice(2), { cwd: process.cwd(), stdout: line => console.log(line), stderr: line => console.error(line) })
.then(code => {
process.exitCode = code
})
.catch(err => {
console.error(err)
process.exitCode = 1
})
8 changes: 6 additions & 2 deletions packages/branch-management/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
"access": "public"
},
"files": [
"dist"
"dist",
"bin"
],
"bin": {
"branch-management": "./bin/branch-management"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -27,7 +31,7 @@
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"test": "tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'",
"test": "pnpm build && tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'",
"clean": "rm -rf dist dist-test"
},
"devDependencies": {
Expand Down
5 changes: 5 additions & 0 deletions packages/branch-management/src/bin-dir.SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
8 changes: 8 additions & 0 deletions packages/branch-management/src/bin-dir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fileURLToPath } from 'node:url'

/**
* Where the `branch-management` executable lives, for a caller that puts it on a spawned
* process's PATH — the daemon, for every agent it starts (#1725). Beside `dist/`, so it is the
* same path from a workspace checkout and from an installed package.
*/
export const CLI_BIN_DIR = fileURLToPath(new URL('../bin/', import.meta.url))
5 changes: 5 additions & 0 deletions packages/branch-management/src/checkout.SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
43 changes: 43 additions & 0 deletions packages/branch-management/src/checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { nodeGitRunner, type GitRunner } from './git.js'
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'

/**
* A checkout as an agent gets it (#1725): the worktree, the parent's dependency trees linked in,
* and the `branches/` links brought up to date — one sequence, whichever surface asks for it (the
* daemon allocating a run, the command line).
*/

/** A new agent's checkout, on a fresh `tf-agent-<id>` branch from `base` or the project's head. */
export async function createCheckout(
repo: string,
opts: { agentId: string; base?: string },
git: GitRunner = nodeGitRunner(),
): Promise<AddedWorktree> {
const worktree = await addWorktree(repo, { agentId: opts.agentId, branch: agentBranchName(opts.agentId), ...(opts.base ? { base: opts.base } : {}) }, git)
await settle(repo, worktree.path, git)
return worktree
}

/** A continued agent's checkout, back on the branch its work is on. */
export async function attachCheckout(
repo: string,
opts: { agentId: string; branch: string },
git: GitRunner = nodeGitRunner(),
): Promise<AddedWorktree> {
const worktree = await attachWorktree(repo, opts, git)
await settle(repo, worktree.path, git)
return worktree
}

/**
* What a checkout gets besides its files. Both are best-effort: `node_modules` is gitignored, so
* a fresh checkout has none and a link that cannot be made is a worse run, not a failed one; a
* link under `branches/` is a view, and the next reconcile pass makes it.
*/
async function settle(repo: string, path: string, git: GitRunner): Promise<void> {
await linkDependencies(repo, path).catch(() => [])
await reconcileBranchLinks(repo, { git }).catch(() => {})
}
60 changes: 60 additions & 0 deletions packages/branch-management/src/cli.SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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.

## User story

- An agent, started inside its own checkout, names its session and learns the branch name it got.
- An agent checks, before it finishes, that it has left nothing uncommitted.
- The user, in a terminal, lists and reclaims the checkouts under a project without opening the dashboard.

## Glossary

- **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".

## Business logic — TL;DR

- **Seven commands over the package** - `create`, `attach`, `name`, `status`, `list`, `remove`, `prune`; each is the corresponding package operation and nothing more.
- **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.
- **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.
- **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.

## Business logic

### Seven commands over the package

#### User story

See `## User story`.

#### Business logic

- `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.
- `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.
- `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`).
- `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.
- `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.
- `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.
- `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.

### JSON out, a reason for a person, an exit code that says which

#### User story

An agent parses what it is told; a person reads it; a script branches on the exit code.

#### Business logic

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.

### The project is found from where the command runs

#### User story

An agent runs the commands from inside its own checkout, from whichever subdirectory it happens to be in.

#### Business logic

`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.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
23 changes: 23 additions & 0 deletions packages/branch-management/src/cli.test.SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
What the tests cover: every command of the command line against real git, and the contract around them.

- **`create`** - makes the checkout on `tf-agent-<id>`, links the parent's dependency directory in, creates the repo-root `branches` shortcut; works from inside another checkout; `--base` puts the branch on the stated commit.
- **Ids** - an id that could escape the branches directory is refused by `create` and `remove` with nothing created.
- **`name`** - renames from a subdirectory of the checkout, leaves no second branch, moves the `branches/` link to the new name without moving the checkout; the same name again is a no-op and another name renames again, dropping the old link; a name taken locally or only on the remote gets a numeric suffix that the caller reads back; a name outside `[a-z0-9-]+` is refused with the branch untouched; the project's main checkout is refused and keeps its branch.
- **`status`** - reads dirty before the agent commits, clean after, on the remote after a push; accepts an absolute or a relative path; refuses a leftover directory that is not a checkout.
- **`list`** - empty for a project with no checkouts; each checkout with the branch it is on now, renamed or not; a number per checkout with `--sizes`.
- **`remove`** - a dirty checkout is kept with the reason on stderr; a committed one is pushed, removed, and its link dropped; a second removal reports no checkout; with `--no-push` an unpushed checkout is kept and nothing reaches the remote, until someone pushes it by hand.
- **`attach`** - a continued agent gets its checkout back on the named branch, with its previous commit, dependencies and link.
- **`prune`** - removes what the rule allows, lists each kept checkout with its reason, exits 0.
- **Outside a repository** - every command is refused as such.
- **Usage** - no command, an unknown command, a missing or extra argument, an unknown option: the usage on stderr, no JSON, exit code 2.
- **A git failure** - reported with git's own line, exit code 1.
- **The executable** - runs by name from the exported bin directory: JSON on stdout, the reason on stderr, the exit codes 0, 1 and 2.
- **Naming again** - asking again for the name the checkout already carries changes nothing, whether it got the plain name or a suffixed one, and whether or not its own branch has been pushed.
- **Reserved names** - `data` and any `agent-…` name are refused, the branch stays, and no phantom checkout appears in the listing.
- **The project from its layout** - a project that is itself a linked worktree gets its checkouts under its own directory, lists them from inside one of them, and its links follow a rename; the main checkout it was made from sees none of them.
- **Before the project is looked for** - a bad id is refused even outside a repository; `status <path>` on a directory outside a repository answers "not a checkout"; `attach` outside a repository is refused as such.
- **Not commands** - names of built-in object properties are not commands.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Loading
Loading