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
3 changes: 2 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
The Framework: autonomous AI programming. Humans make the important decisions; coding agents work the user's registered repos unattended and hand the result off as pull requests. The product never makes model calls of its own — it drives a coding-agent CLI the user already pays for (Claude Code or Codex) as a black box, on the user's own subscription.

Three top-level pieces, one product:
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/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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/framework clean",
"test": "pnpm -C packages/framework test",
"clean": "pnpm -C packages/branch-management clean && pnpm -C packages/framework clean",
"test": "pnpm -C packages/branch-management test && pnpm -C packages/framework test",
"========= Website": "",
"// Develop website https://the-framework.ai locally": "",
"website": "cd packages/the-framework.ai/ && pnpm run dev",
Expand Down
5 changes: 3 additions & 2 deletions packages/SPEC.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
The three deliverables The Framework ships, one directory each:
The deliverables The Framework ships, one directory each:

- `framework/` — the product: the `framework` npm package (CLI, daemon, agent lifecycle, dashboard).
- `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.
- `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 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.
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 they relate as a product.

## Before modifying/creating SPEC.md files

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

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

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
37 changes: 37 additions & 0 deletions packages/branch-management/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@superskill/branch-management",
"version": "0.0.0",
"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.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/framework/the-framework",
"directory": "packages/branch-management"
},
"type": "module",
"engines": {
"node": ">=22.12.0"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"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'",
"clean": "rm -rf dist dist-test"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^7.0.2"
}
}
5 changes: 5 additions & 0 deletions packages/branch-management/src/SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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.

## Before modifying/creating SPEC.md files

You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
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.
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.

## Business logic — TL;DR

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

## Before modifying/creating SPEC.md files

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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.
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.

## Before modifying/creating SPEC.md files

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 './project.js'
import { reconcileBranchLinks, startBranchLinksPass, type LinksFs } from './branch-links.js'
import { FRAMEWORK_DIR, BRANCHES_DIR, addWorktree, worktreePath, type WorktreeDirEntry } from './store/index.js'
import { nodeGitRunner } from './git.js'
import { reconcileBranchLinks, type LinksFs } from './branch-links.js'
import { FRAMEWORK_DIR, BRANCHES_DIR, addWorktree, worktreePath, type WorktreeDirEntry } from './index.js'

const CWD = '/repo'
const LINKS = join(CWD, FRAMEWORK_DIR, BRANCHES_DIR)
Expand Down Expand Up @@ -108,16 +108,3 @@ test('a branches/ directory that is not a worktree gets no link, against real gi
await rm(repo, { recursive: true, force: true })
}
})

test('the pass covers every registered project and a stopped pass does nothing', async () => {
const seen: string[] = []
const pass = startBranchLinksPass({
projects: async () => [{ path: '/a' }, { path: '/b' }],
reconcile: async cwd => void seen.push(cwd),
})
await pass.tick()
assert.deepEqual(seen, ['/a', '/b'])
pass.stop()
await pass.tick()
assert.equal(seen.length, 2)
})
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { basename, join } from 'node:path'
import { nodeGitRunner, type GitRunner } from './project.js'
import { nodeGitRunner, type GitRunner } from './git.js'
import { excludeFromGit } from './git-exclude.js'
import { FRAMEWORK_DIR, BRANCHES_DIR, worktreeDirEntries, worktreeBranch, type WorktreeDirEntry } from './store/index.js'
import { isWorktreeDirName } from './branch-names.js'
import { startProjectPass, type ProjectPass, type ProjectsSource } from './project-pass.js'
import { FRAMEWORK_DIR, BRANCHES_DIR, isWorktreeDirName } from './branch-names.js'
import { worktreeDirEntries, worktreeBranch, type WorktreeDirEntry } from './worktree.js'

// The branches view (#1580): every checkout under `.the-framework/branches/` is a directory named
// as its birth branch (`tf-agent-<id>`), and this pass keeps the *current* names reachable beside
Expand All @@ -13,7 +12,7 @@ import { startProjectPass, type ProjectPass, type ProjectsSource } from './proje
// shows, and a rename costs a link, never moving a checkout under a live agent (the #1589
// review's call).
//
// The daemon reconciles on its clock and after each worktree it allocates: derive the wanted
// A daemon reconciles on its clock and after each worktree it allocates: derive the wanted
// links from the checkouts on disk, add what is missing, drop only our own stale links.

/** The filesystem the reconcile needs; `node:fs/promises` in production. */
Expand All @@ -32,7 +31,7 @@ export interface LinksFs {
lexists(path: string): Promise<boolean>
}

/** A {@link LinksFs} over `node:fs/promises`, dynamically imported like {@link nodeDirLister}. */
/** A {@link LinksFs} over `node:fs/promises`. */
function nodeLinksFs(): LinksFs {
const fs = () => import('node:fs/promises')
return {
Expand Down Expand Up @@ -121,21 +120,3 @@ export async function reconcileBranchLinks(cwd: string, deps: BranchLinksDeps =
await exclude(cwd, '!/branches/').catch(() => {})
}
}

/** What {@link startBranchLinksPass} needs from the daemon. */
export interface BranchLinksOptions {
projects: ProjectsSource
/** The per-project reconcile (default {@link reconcileBranchLinks}). */
reconcile?: (cwd: string) => Promise<void>
}

/**
* Keep every registered project's branch links current, one turn per call. Runs on the daemon's
* clock; renames and reclaimed worktrees settle within a tick, and a freshly-allocated worktree
* gets its link immediately because allocation calls the reconcile too. Quiet on purpose: links
* are presentation, and narrating every rename would drown the log.
*/
export function startBranchLinksPass(opts: BranchLinksOptions): ProjectPass {
const reconcile = opts.reconcile ?? reconcileBranchLinks
return startProjectPass(opts.projects, cwd => reconcile(cwd).catch(() => {}))
}
9 changes: 9 additions & 0 deletions packages/branch-management/src/branch-names.SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The naming rules for everything The Framework mints in git, and the layout they imply on disk — the `.the-framework` directory at a project's root, the `branches` directory under it 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 framework-minted branch starts with `tf-` — 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 `.the-framework/branches/` be named exactly as its branch. The data branch is `tf-data`. An agent branch is born `tf-agent-<agent id>` — created from the agent id because the id exists before the session name does — and renamed to `tf-<session name>` once the agent picks a name.

The same rules answer the filesystem questions around `.the-framework/branches/`: an agent's worktree directory carries its birth branch's name, so the flat listing reads as branch names; the agent id is recoverable from a directory name; and only names in the minted `tf-agent-` spelling count as framework checkouts, since the same directory also holds the rename links and possibly a user's own entries.

They also answer which branches The Framework may ever delete on its own: those it minted for an agent — the `tf-agent-<agent id>` spelling or a session-named `tf-<session name>` — and never the data branch, which shares the prefix but is not an agent's. A branch of the user's own is out of scope by name alone, so no cleanup can reach it however empty it looks.

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