Skip to content

Commit 88ff10a

Browse files
committed
Branch management lives in its own package (#1725)
The git side of an agent's checkout — the naming conventions and the .the-framework/branches/ layout, the git runner with its per-subcommand time budgets, creating/attaching/listing/renaming/removing a worktree, sharing the parent's dependency trees into it, the branch-name links, and the retention rule under which a checkout is reclaimed — moves out of the framework into packages/branch-management, published as @superskill/branch-management. Step 1 of #1725: a lift with no behaviour change; the framework imports the package as a workspace dependency. What the framework keeps is the agent's side: its record (may the branch be pushed, what did a cloud hand-off already push), the teardown and the sweep, the dashboard's list and buttons, and how each refusal is worded. The package's reclaimWorktree takes those as options and returns a reason code. Also folded in: pushAgentBranch/gitReason (agent-handoff) were the same push the rule needs, so there is one pushBranch now, in the package.
1 parent e36525a commit 88ff10a

110 files changed

Lines changed: 1337 additions & 1024 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"description": "The Framework: autonomous AI programming — coding agents work your repos unattended and hand the result off as pull requests.",
77
"packageManager": "pnpm@11.5.3",
88
"scripts": {
9-
"build": "pnpm -C packages/framework build",
9+
"build": "pnpm -C packages/branch-management build && pnpm -C packages/framework build",
1010
"// Serve the production build the way an install does: the daemon, not a static server": "",
1111
"preview": "pnpm -C packages/framework preview",
1212
"dev": "pnpm -C packages/framework dev",
13-
"typecheck": "pnpm -C packages/framework typecheck && pnpm -C packages/the-framework.ai typecheck",
14-
"clean": "pnpm -C packages/framework clean",
15-
"test": "pnpm -C packages/framework test",
13+
"typecheck": "pnpm -C packages/branch-management typecheck && pnpm -C packages/framework typecheck && pnpm -C packages/the-framework.ai typecheck",
14+
"clean": "pnpm -C packages/branch-management clean && pnpm -C packages/framework clean",
15+
"test": "pnpm -C packages/branch-management test && pnpm -C packages/framework test",
1616
"========= Website": "",
1717
"// Develop website https://the-framework.ai locally": "",
1818
"website": "cd packages/the-framework.ai/ && pnpm run dev",

packages/SPEC.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
The three deliverables The Framework ships, one directory each:
1+
The deliverables The Framework ships, one directory each:
22

33
- `framework/` — the product: the `framework` npm package (CLI, daemon, agent lifecycle, dashboard).
4+
- `branch-management/` — the `@superskill/branch-management` npm package: the git conventions and operations behind an agent's own checkout, the first skill of the skills-plus architecture (#1725). The product depends on it; nothing else does yet.
45
- `chrome-extension/` — the Claude web bridge, a companion Chrome extension that connects Claude Code cloud sessions on claude.ai back to the local dashboard.
56
- `the-framework.ai/` — the marketing website.
67

7-
The product stands alone; the extension and the website depend on it only in what they present, not in code. See the root `SPEC.md` for how the three relate as a product.
8+
The product depends on the branch-management package in code; the extension and the website depend on the product only in what they present. See the root `SPEC.md` for how the three relate as a product.
89

910
## Before modifying/creating SPEC.md files
1011

packages/branch-management/SPEC.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
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.
2+
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.
4+
5+
## Business logic — TL;DR
6+
7+
- **The conventions** (`branch-names`) - branch names, the checkout directory layout under `.the-framework/branches/`, and the agent-id charset every path is built from.
8+
- **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.
10+
- **Dependencies shared, not copied** (`worktree-deps`) - a fresh checkout gets the parent's dependency trees as directories of links.
11+
- **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.
12+
- **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.
13+
14+
## Before modifying/creating SPEC.md files
15+
16+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@superskill/branch-management",
3+
"version": "0.0.0",
4+
"description": "Branch management for coding agents: one git checkout per agent under .the-framework/branches/, named as its branch, reclaimed once its work is on the remote.",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/framework/the-framework",
9+
"directory": "packages/branch-management"
10+
},
11+
"type": "module",
12+
"engines": {
13+
"node": ">=22.12.0"
14+
},
15+
"files": [
16+
"dist"
17+
],
18+
"exports": {
19+
".": {
20+
"types": "./dist/index.d.ts",
21+
"default": "./dist/index.js"
22+
}
23+
},
24+
"scripts": {
25+
"build": "tsc -p tsconfig.build.json",
26+
"typecheck": "tsc --noEmit",
27+
"test": "tsc -p tsconfig.test.json && node --test --test-timeout=60000 'dist-test/**/*.test.js'",
28+
"clean": "rm -rf dist dist-test"
29+
},
30+
"devDependencies": {
31+
"@types/node": "^20.0.0",
32+
"typescript": "^7.0.2"
33+
}
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The package's source: the git conventions and operations behind an agent's own checkout. See the package `SPEC.md` for how the modules relate; each module's `*.SPEC.md` holds its business logic.
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

packages/framework/src/branch-links.BUG-ANALYSIS.md renamed to packages/branch-management/src/branch-links.BUG-ANALYSIS.md

File renamed without changes.

packages/framework/src/branch-links.SPEC.md renamed to packages/branch-management/src/branch-links.SPEC.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
Keeps a project's `.the-framework/branches/` directory navigable by branch name. Every checkout there is a directory named as its birth branch (`tf-agent-<agent id>`), but an agent renames its branch to `tf-<session name>` early — so this pass maintains a symlink named as the branch each checkout is on *now*, whenever that differs from the directory's own name, plus a `branches` shortcut at the repo root. `cd branches/<name>` then reaches any agent's checkout by the name the dashboard shows, and a rename costs a link — a checkout is never moved under a live agent.
1+
Keeps a project's `.the-framework/branches/` directory navigable by branch name; a daemon runs the reconcile on its clock and after each checkout it creates. Every checkout there is a directory named as its birth branch (`tf-agent-<agent id>`), but an agent renames its branch to `tf-<session name>` early — so this pass maintains a symlink named as the branch each checkout is on *now*, whenever that differs from the directory's own name, plus a `branches` shortcut at the repo root. `cd branches/<name>` then reaches any agent's checkout by the name the dashboard shows, and a rename costs a link — a checkout is never moved under a live agent.
22

33
## Business logic — TL;DR
44

55
- **Reconcile, don't track** - each pass derives the wanted links from the checkouts actually on disk (one link per worktree whose current branch differs from its directory name), creates what is missing, and drops the framework's own links that are stale — no longer wanted, or now belonging to a newer checkout that reuses the name. A detached worktree, or one on a legacy slash-named branch, gets no link.
66
- **A directory that is not a checkout has no branch** - the branch behind each link is read in the form that answers only for a directory git knows as a checkout of its own. A leftover directory under `.the-framework/branches/` therefore gets no link at all, where a plain read would have answered with the *enclosing* repository's branch and produced a link named after the user's own branch, sitting among the agents' ones.
77
- **Touch only what is provably ours** - a link is created, replaced, or removed only when it points (or would point) at a sibling checkout directory; a user's own file, directory, or foreign symlink at the same path is left alone, and nothing is ever created over it. The pass never throws.
88
- **The repo-root `branches` shortcut** - created once, as a relative link into `.the-framework/branches/` (so a checkout that moves keeps working), and only when nothing already sits at that path. Being framework state, it is hidden from git the moment it is made — uncommitted at the root, it would otherwise ride any sweeping `git add -A` onto a code branch. The exclude comes as a pair (`/branches`, then `!/branches/`) shaped so a user's own `branches` *directory* keeps committing while the symlink stays hidden, because a trailing slash never matches a symlink.
9-
- **The daemon's pass** - reconciles every registered project, one pass per call on the daemon's clock, and again right after each worktree allocation so a fresh checkout gets its link immediately. Overlapping calls join the pass in flight, a stopped pass does nothing, and nothing is logged: links are presentation, and narrating every rename would drown the log.
109

1110
## Before modifying/creating SPEC.md files
1211

packages/framework/src/branch-links.test.BUG-ANALYSIS.md renamed to packages/branch-management/src/branch-links.test.BUG-ANALYSIS.md

File renamed without changes.

packages/framework/src/branch-links.test.SPEC.md renamed to packages/branch-management/src/branch-links.test.SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
What the tests cover: a checkout still on its birth branch gets no link, since the directory already carries the name; a renamed branch gets a sibling link and the stale name is dropped in the same pass; a reclaimed checkout loses its link, and detached or legacy slash-named branches never get one; against a real repository, a leftover `.the-framework/branches/` directory that is not a checkout gets no link — where a plain branch read would have named it after the user's own branch — while a genuinely renamed checkout beside it still gets its link; user files and foreign symlinks are never removed, and nothing is created over a user's entry that occupies a wanted name; the repo-root `branches` shortcut is created once, relative, and hidden from git with the exclude pair — while an occupied path is left alone and nothing is excluded on the user's behalf; the recurring pass visits every registered project, and a stopped pass does nothing.
1+
What the tests cover: a checkout still on its birth branch gets no link, since the directory already carries the name; a renamed branch gets a sibling link and the stale name is dropped in the same pass; a reclaimed checkout loses its link, and detached or legacy slash-named branches never get one; against a real repository, a leftover `.the-framework/branches/` directory that is not a checkout gets no link — where a plain branch read would have named it after the user's own branch — while a genuinely renamed checkout beside it still gets its link; user files and foreign symlinks are never removed, and nothing is created over a user's entry that occupies a wanted name; the repo-root `branches` shortcut is created once, relative, and hidden from git with the exclude pair — while an occupied path is left alone and nothing is excluded on the user's behalf.
22

33
## Before modifying/creating SPEC.md files
44

packages/framework/src/branch-links.test.ts renamed to packages/branch-management/src/branch-links.test.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { test } from 'node:test'
33
import { join } from 'node:path'
44
import { tmpdir } from 'node:os'
55
import { mkdir, mkdtemp, readdir, realpath, rm, writeFile } from 'node:fs/promises'
6-
import { nodeGitRunner } from './project.js'
7-
import { reconcileBranchLinks, startBranchLinksPass, type LinksFs } from './branch-links.js'
8-
import { FRAMEWORK_DIR, BRANCHES_DIR, addWorktree, worktreePath, type WorktreeDirEntry } from './store/index.js'
6+
import { nodeGitRunner } from './git.js'
7+
import { reconcileBranchLinks, type LinksFs } from './branch-links.js'
8+
import { FRAMEWORK_DIR, BRANCHES_DIR, addWorktree, worktreePath, type WorktreeDirEntry } from './index.js'
99

1010
const CWD = '/repo'
1111
const LINKS = join(CWD, FRAMEWORK_DIR, BRANCHES_DIR)
@@ -108,16 +108,3 @@ test('a branches/ directory that is not a worktree gets no link, against real gi
108108
await rm(repo, { recursive: true, force: true })
109109
}
110110
})
111-
112-
test('the pass covers every registered project and a stopped pass does nothing', async () => {
113-
const seen: string[] = []
114-
const pass = startBranchLinksPass({
115-
projects: async () => [{ path: '/a' }, { path: '/b' }],
116-
reconcile: async cwd => void seen.push(cwd),
117-
})
118-
await pass.tick()
119-
assert.deepEqual(seen, ['/a', '/b'])
120-
pass.stop()
121-
await pass.tick()
122-
assert.equal(seen.length, 2)
123-
})

0 commit comments

Comments
 (0)