diff --git a/.github/LOGIC.md b/.github/LOGIC.md new file mode 100644 index 000000000..5d426ec66 --- /dev/null +++ b/.github/LOGIC.md @@ -0,0 +1,5 @@ +GitHub's configuration of the repository, which consists only of its workflows: the check every change gets, the deploy of the marketing website, and the workflow that runs an agent's turn on a GitHub-hosted runner for The Framework's `github-actions` driver. + +## Business logic — TL;DR + +- **The workflows** (`workflows/`) - verifying every push and fork pull request, publishing the website from `main`, and running one turn of an agent on a runner when The Framework dispatches it; each is described in `workflows/LOGIC.md`. diff --git a/.github/workflows/LOGIC.md b/.github/workflows/LOGIC.md new file mode 100644 index 000000000..70e241a96 --- /dev/null +++ b/.github/workflows/LOGIC.md @@ -0,0 +1,13 @@ +The repository's three GitHub workflows: the check every push and every pull request from a fork gets, the deploy that keeps the marketing website live, and the workflow that runs one turn [2] of an agent [1] on a GitHub-hosted runner for The Framework's `github-actions` driver [3]. The first two serve this repository's own development; the third is a piece of the product, carried by any repository that wants its agents to run on GitHub's runners, this one included. + +## Glossary + +[1] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. +[2] turn: one prompt sent to the driver; the coding agent's own loop runs to completion and answers with a final message. +[3] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. The driver implementations are `claude-code`, `codex`, `github-actions`, `claude-web` and `fake`. + +## Business logic — TL;DR + +- **Verifying every change** (`ci.yml`) - on every push, and on a pull request only when it comes from a fork, one job installs the monorepo, builds it, type-checks it and runs every package's tests, stopping at the first failure; the "CI" check's status is its only product, and it pushes nothing. +- **Publishing the website** (`website-deploy.yml`) - on a push to `main` that touches the website package, the site is built and replaces the whole `gh-pages` branch as one commit, carrying the custom domain `the-framework.ai`. +- **Running an agent's turn on a runner** (`framework-agent.yml`) - only when dispatched by the driver [3], with a prompt and a correlation id: the run checks the repository out, runs Claude Code on the prompt with every permission granted on the user's subscription token, pushes whatever the coding agent left to the branch the driver named, and uploads the transcript and that branch name as one artifact keyed by the correlation id, even when the coding agent failed. diff --git a/.github/workflows/ci.LOGIC.md b/.github/workflows/ci.LOGIC.md new file mode 100644 index 000000000..1e734fadc --- /dev/null +++ b/.github/workflows/ci.LOGIC.md @@ -0,0 +1,52 @@ +Verifies every change to the repository on a GitHub-hosted runner: one job installs the monorepo, builds it, type-checks it and runs every package's tests, in that order, and its pass or fail is the "CI" check GitHub shows on the commit and on the pull request. It pushes nothing and publishes nothing. + +## Context + +**User story**: a contributor pushes a branch or opens a pull request and sees a green or red "CI" check on it before anyone reviews or merges; a red check names the step that failed. + +**Problem**: a pull request opened from a branch of this same repository fires both a push event and a pull request event for the same commit, which would verify it twice for no information; a pull request from a fork fires no push event here, so it needs the pull request event. + +## Business logic — TL;DR + +- **When it runs** - on every push to any branch, and on a pull request only when the pull request comes from a fork. +- **What it verifies** - install, build, type check and every package's tests, stopping at the first failing step. +- **What it may do and produces** - the repository's default token permissions, no artifact, no push: its only product is the check's status. + +## Business logic + +### When it runs + +#### Context + +See `## Context`. + +#### Business logic + +The workflow, named "CI", runs on every push to any branch and on every pull request event, with one exclusion: a pull request event whose head branch lives in this same repository is skipped, because the push of that branch already runs the workflow. A pull request from a fork is not skipped, since a push to a fork never reaches this repository's workflows. + +### What it verifies + +#### Context + +**Business logic story**: the root `package.json` scripts define what "build", "typecheck" and "test" mean for the whole monorepo; this workflow only calls them. + +#### Business logic + +One job on the latest Ubuntu runner, with pnpm, Node 22 and pnpm's download cache, runs four commands at the repository root, each only if the previous one succeeded: + +1. `pnpm install`: every package's dependencies. +2. `pnpm build`: the framework package's build, which builds the four skill packages and the `agent-driver` package, generates the prompts module from the prompt markdown (the rule in `packages/framework/scripts/gen-prompts.mjs`), compiles the framework and builds the dashboard. +3. `pnpm typecheck`: the framework package, its dashboard, and the website package. +4. `pnpm test`: the tests of every package, in order: `agent-data`, `skill-branches`, `skill-tickets`, `skill-queue`, `skill-logs`, `agent-driver`, then `framework`. + +The first failing command fails the job; the later commands do not run. + +### What it may do and produces + +#### Context + +**Problem**: a verification workflow that could write to the repository would turn a compromised dependency or test into a way to push code. + +#### Business logic + +The workflow declares no permissions, so it runs with the repository's default token permissions. It uploads no artifact, deploys nothing and pushes nothing: the only thing it produces is the pass or fail status of the check on the commit and on the pull request. diff --git a/.github/workflows/framework-agent.LOGIC.md b/.github/workflows/framework-agent.LOGIC.md new file mode 100644 index 000000000..fbee89006 --- /dev/null +++ b/.github/workflows/framework-agent.LOGIC.md @@ -0,0 +1,128 @@ +Runs one turn [2] of an agent [1] on a GitHub-hosted runner instead of on the user's machine: The Framework's `github-actions` driver [3] dispatches this workflow with the prompt, and the workflow run, "the run" below, checks the repository out, runs Claude Code on that prompt with every permission granted, pushes whatever the coding agent [4] left behind to the branch the driver named, and uploads the coding agent's transcript together with that branch name as one artifact keyed by the driver's correlation id [5]. The run is the far end of the driver in `packages/agent-driver/src/actions.ts`: the driver never sees the runner, only what the run pushes and uploads. + +## Context + +**User story**: the user starts an agent whose location [6] is `actions`, so the agent's turns run on GitHub's runners and cost nothing on this machine; the agent's events, its final message and its branch still show up in the dashboard, and the next turn continues where the previous one stopped. + +**Business logic story**: the driver starts a driver session [7] for the agent, and each prompt of that session is one dispatch of this workflow. The runner is discarded when the run ends, so everything the driver needs afterwards has to leave the runner before that: the work as a pushed branch, the transcript as an uploaded artifact. + +**Problem**: dispatching a workflow answers with no run id, so the driver has no handle on the run it started. The driver therefore mints a correlation id per turn, passes it as an input, and the run echoes it into its display name and into its artifact name; the driver finds its run and its artifact by that id and by nothing else. + +## Glossary + +[1] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. +[2] turn: one prompt sent to the driver; the coding agent's own loop runs to completion and answers with a final message. +[3] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. The driver implementation here is `github-actions`. +[4] coding agent: the CLI doing the actual work: Claude Code or Codex. Only Claude Code runs on this workflow. +[5] correlation id: the id the driver mints for one turn, unique across driver processes, that the run echoes into its display name and its artifact name so the driver can find them. +[6] location: where an agent's turns run: `local` (this machine), `actions` (a GitHub Actions runner), or `web` (a Claude Code cloud session). +[7] driver session: the coding agent's own conversation for one agent, which the driver can resume by its session id. + +## Business logic — TL;DR + +- **When it runs and what it is given** - only on a manual dispatch, with the prompt, the correlation id, and optionally a model, a session id to resume and a branch to push to; the correlation id becomes part of the run's name. +- **What the run is allowed to do** - write the repository's contents and pull requests, mint an identity token, and run for at most 60 minutes; the coding agent itself runs with every permission granted, on a subscription OAuth token held by the repository. +- **How the coding agent is started** - a full-history checkout, then Claude Code on the prompt passed verbatim, with the model and the resumed session id passed as arguments that are never composed through shell interpolation. +- **What is pushed** - whatever the coding agent left, committed if needed, to the branch the driver named, but only when the turn advanced past the commit it started from, and even when the coding agent failed. +- **What is uploaded for the driver** - one artifact named after the correlation id holding the transcript and the name of the pushed branch, kept for 7 days, uploaded even when the coding agent failed. +- **How the driver and the run correlate** - the driver finds the run by the correlation id in its name, fails the turn when the run does not succeed, replays the uploaded transcript as the agent's events, and dispatches the next turn on the branch the run pushed. + +## Business logic + +### When it runs and what it is given + +#### Context + +See `## Context`. + +#### Business logic + +The workflow, named "framework-agent", never runs on a push or a pull request: it runs only when dispatched by hand or through GitHub's API, which is how the driver [3] starts it. It takes five inputs: + +- `prompt` (required): what the agent [1] should do this turn [2]. The driver sends The Framework's system prompt and the user's prompt as one text, the system prompt first. +- `correlation_id` (required): the correlation id [5]. The run's display name is "framework-agent " followed by it. +- `model` (optional): the model id to run on; empty means the action's default. +- `resume_session_id` (optional): the session id of a prior driver session [7] to continue instead of starting fresh. +- `branch` (optional): the branch the run pushes its work to, so the driver can read the work back and run the next turn on it. + +The driver dispatches the first turn on the repository's configured ref (its default is `main`) and every later turn on the branch the previous run pushed. + +### What the run is allowed to do + +#### Context + +**Problem**: the coding agent [4] must be able to edit files, run commands, commit and open a pull request without anyone approving each action, since no human watches a runner; the runner being thrown away at the end of the run is what makes granting everything acceptable. + +#### Business logic + +The workflow is granted three permissions: writing the repository's contents (the push of the run branch), writing pull requests (the agent [1] may open one), and minting an identity token. The identity token is not optional: the action exchanges it to authenticate the subscription OAuth token, and without that permission every run fails with "Could not fetch an OIDC token". + +The coding agent [4] authenticates with the repository secret `CLAUDE_CODE_OAUTH_TOKEN`, an OAuth token produced by `claude setup-token`: the run spends the user's subscription, never an API key of The Framework's. Without that secret the action cannot authenticate and the run fails. + +The coding agent is started with all permissions skipped, because in this mode it is otherwise granted none and an unattended run could neither edit nor run anything. + +One job runs on the latest Ubuntu runner and is cut off after 60 minutes, well under GitHub's cap of six hours: a turn [2] that runs longer has gone wrong. + +### How the coding agent is started + +#### Context + +**Problem**: the prompt, the model id and the session id are inputs a caller controls; interpolating them into a shell script would let a crafted input become a command on the runner. + +#### Business logic + +The run checks the repository out with its full history, because the coding agent [4] reads the log to understand what it is changing. + +The coding agent's arguments are composed from environment variables, never by interpolating inputs into the script: the permission skip always; `--model ` only when a model was given; `--resume ` only when a session id was given. The driver [3] validates both values on its side too, refusing anything that is not a plain token of letters, digits, dots, underscores, colons and dashes. + +The prompt is handed to the action as an input, verbatim and never through a shell, so a multi-line prompt is safe. The action then runs Claude Code on it for one turn [2]. + +### What is pushed + +#### Context + +**Problem**: the runner and its checkout vanish when the job ends, so the only way the driver [3] can read the agent's work, or run the next turn on top of it, is a branch on the remote. The action creates no branch for a dispatched run, so the workflow pushes one itself, to the name the driver chose. This mirrors the local flow: The Framework pushes the agent's branch, the coding agent [4] only commits. + +#### Business logic + +The push step runs even when the coding agent [4] step failed. When no branch was requested, the step logs "no run branch requested" and does nothing. Otherwise: + +1. The git identity is set to "framework-agent". +2. Anything the coding agent left uncommitted is committed as "framework agent run ()", so it is not lost with the runner. +3. When the checkout's tip is still the commit the run started from (the dispatched ref's tip on the remote), nothing is pushed, so a turn [2] that changed nothing creates no empty branch. When that ref cannot be resolved, the run errs toward pushing. +4. The tip is pushed to the requested branch, creating or advancing it. The push authenticates explicitly with the workflow's own token through a tokenized URL: the coding agent's step runs its own git setup and leaves the checkout's persisted credentials unusable, so a plain push would fail with "Authentication failed". + +The branch name is recorded as the step's output only when a push happened. + +### What is uploaded for the driver + +#### Context + +**Problem**: an artifact is the only channel out of a run that the driver [3] can read over GitHub's API, and the transcript matters most exactly when the turn [2] failed. + +#### Business logic + +The collect step runs even when the coding agent [4] step failed. It assembles a directory `framework-run/` holding: + +- `execution.json`: the transcript the action produced, a JSON array of the coding agent's messages. When the action produced none, the file holds an empty array, so a crashed coding agent yields an empty turn [2] rather than a driver error. +- `meta.json`: the name of the branch the run actually pushed (empty when nothing was pushed) and the coding agent's session id. + +The directory is not dot-prefixed on purpose: the upload drops every file under a hidden path, and a hidden directory would upload nothing, leaving the driver [3] no artifact to read. + +The directory is uploaded as one artifact named `framework-run-`, kept for 7 days, whether or not the coding agent succeeded. + +### How the driver and the run correlate + +#### Context + +See `## Context`. + +#### Business logic + +The driver [3] side lives in `packages/agent-driver/src/actions.ts`; the contract between the two is: + +- The driver mints one correlation id [5] per turn [2], unique across driver processes, and passes it as the `correlation_id` input. It finds its run among the workflow's recent dispatched runs by that id in the run's name, and its artifact among the run's artifacts by that id in the artifact's name. +- The driver names the branch the run pushes to, one stable name per driver session [7], and passes it as the `branch` input: the run's pushes chain on that branch from turn to turn. +- A run that completes with any conclusion other than success fails the turn, and the driver reports the run's URL. +- The uploaded `execution.json` is replayed as the agent's events in one burst when the run ends, since the run streams nothing while it runs; its final message is the turn's final message. +- The branch in `meta.json` is where the driver reads the agent's files from over GitHub's contents API, since the runner is gone, and the ref it dispatches the next turn on. The session id the driver continues with is read off the transcript itself. diff --git a/.github/workflows/website-deploy.LOGIC.md b/.github/workflows/website-deploy.LOGIC.md new file mode 100644 index 000000000..6e511a379 --- /dev/null +++ b/.github/workflows/website-deploy.LOGIC.md @@ -0,0 +1,50 @@ +Publishes the marketing website, the `packages/the-framework.ai` package, to GitHub Pages at `the-framework.ai`: on every push to `main` that touches the website, it builds the site and replaces the whole `gh-pages` branch with the fresh build. + +## Context + +**User story**: a change to the website merged into `main` is live at https://the-framework.ai a few minutes later with nothing to do by hand; a change elsewhere in the repository does not redeploy the site. + +## Business logic — TL;DR + +- **When it deploys** - only a push to `main` that changes the website package or this workflow. +- **What it builds** - the framework package, then the website's production build, after a website test step that has no tests yet. +- **How it publishes** - the built site replaces the `gh-pages` branch as one single commit, with the custom domain and the no-Jekyll marker carried inside the build. + +## Business logic + +### When it deploys + +#### Context + +See `## Context`. + +#### Business logic + +The workflow, named "Website Deployment", runs on a push to the `main` branch only, and only when the push changes a file under `packages/the-framework.ai/` or the workflow file itself. A pull request never deploys, and a push to any other branch never deploys. + +### What it builds + +#### Context + +**Business logic story**: the root `package.json` scripts define the website's build and test commands; this workflow calls them in order. + +#### Business logic + +One job on the latest Ubuntu runner, with pnpm, runs four commands at the repository root, each only if the previous one succeeded: + +1. `pnpm install`: every package's dependencies. +2. `pnpm run build`: the framework package's build. +3. `pnpm run website:test`: the website's tests. There are none yet: the command prints "no tests yet" and succeeds, so this step cannot fail today. +4. `pnpm run website:build`: the website's production build, which lands in `packages/the-framework.ai/dist/client`. + +A failing command fails the job and nothing is deployed; the site already deployed stays up. + +### How it publishes + +#### Context + +**Problem**: a deploy branch that kept one commit per build would grow the repository without bound, and a deploy that wiped the branch would also wipe the custom-domain file GitHub Pages needs. + +#### Business logic + +The workflow is allowed to write the repository's contents, which pushing the `gh-pages` branch needs. The "Deploy" step publishes the folder `packages/the-framework.ai/dist/client` to the `gh-pages` branch, removing every file of the previous deploy first and rewriting the branch as one single commit, so the branch never keeps history. The website's `public/` folder ships `CNAME`, holding `the-framework.ai`, and an empty `.nojekyll` into that build folder, so every deploy carries the custom domain and the marker that stops GitHub Pages from running Jekyll on the build, and wiping the branch loses neither. diff --git a/LOGIC.md b/LOGIC.md new file mode 100644 index 000000000..0845a9702 --- /dev/null +++ b/LOGIC.md @@ -0,0 +1,100 @@ +The Framework: autonomous AI programming. The user registers repositories as projects in a browser dashboard and states what to build or fix; a coding agent — Claude Code or Codex, on the user's own subscription — works the task unattended in a checkout [3] of its own, stops only for decisions a human must make, and hands the result off as a pull request. While nobody is at the keyboard, the same daemon keeps the account's leftover quota productive: it works the agent queue [7], refills it by triaging and planning tickets, keeps the pull requests it opened green, and merges them once their checks pass — standing down before unattended work could eat into the quota a human will want. The product never calls a model itself, and the user's own checkout is never touched. + +## Context + +**User story**: +- A developer registers a repository, types what they want, and gets a reviewable pull request without babysitting: the agent [1] asks only when a real decision is needed and otherwise finishes on its own. +- A developer walks away. The daemon spends the week's remaining quota on the roadmap — draining the agent queue [7], triaging and planning tickets, fixing red CI on its own pull requests, merging them once green — and never starves work the developer asks for. +- Nothing The Framework removes on its own initiative is lost: it only removes what is already on the git remote. + +## Glossary + +[1] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. Started from the dashboard by the user, or by the daemon. +[2] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. The user's driver choice is `claude` or `codex`; the driver implementations are `claude-code`, `codex`, `github-actions`, `claude-web` and `fake`. +[3] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. +[4] gate: a question with options at which an agent stops and waits for an answer: it emits the question in its turn's final message, the dashboard shows it as a card, and the answer re-prompts the agent. When nobody can answer, the recommended option is taken. +[5] handoff: what happens to an agent's work when the agent ends, as one ladder of four levels: `local` (keep the work in its checkout), `push` (push its branch), `pr` (also open a pull request — the default), `merge` (also merge it). +[6] the `agent-data` branch: the branch of a project's repository used as a file store for everything agents share: tickets, the agent queue, the runs, routine locks. Born as an orphan, written through one sync → commit → push cycle. +[7] the agent queue: `TODO_AGENTS.md` on the `agent-data` branch: every task agents will work next, in priority sections, worked top-down. +[8] skill: one of the four capabilities an agent is taught — `branches`, `tickets`, `queue`, `logs` — each a package with the instructions the agent reads (its `SKILL.md`, linked into the checkout where the coding agent's harness looks for skills), a command on the agent's PATH, and an API the product calls. +[9] location: where an agent's turns run: `local` (this machine), `actions` (a GitHub Actions runner), or `web` (a Claude Code cloud session). +[10] sweep: a background job the daemon runs on its clock: Auto PM, the CI watch, the notification watchers, the sweep that reclaims checkouts, the branch-links sweep, the cloud scratch sweep, cloud work adoption. +[11] quota boundary: the share of the quota week that may be spent by now, rising with the clock; unattended work stands down past it, work a human asked for never does. +[12] Auto PM: the daemon's unattended product management: drain the agent queue, and refill it by running the routines. +[13] routine: a preset the daemon fires on its own on a schedule — update tickets, triage quick, triage consensual, plan tickets, maintenance — each switchable off and runnable on demand. +[14] the Claude web bridge: the daemon's bridge endpoints plus the Chrome extension: carries the question a cloud session is parked on into the dashboard, and types the pick back into the session. +[15] hands-off: said of an agent whose work leaves this machine, so its first prompt is the whole agent: an agent whose location is `web`. +[16] session name: the name an agent gives its own work (`[a-z0-9-]+`); its branch is renamed to `agent-` and the dashboard labels the agent by it. +[17] ready for merge: the signal an agent emits when it believes its work is complete: it flips the agent's badge from building to ready and authorizes the handoff. +[18] run: the `logs` skill's record of one agent on the `agent-data` branch: a card (what was asked, the ticket, the branch, the pull request, how it ended, what it cost) and a diary (what the agent said). +[19] device: another machine's daemon the user saved by URL and token, to run agents on it from this dashboard. +[20] cloud anchor: an empty commit a web agent pushes before its task leaves this machine, unique to the agent: the branch the cloud session later pushes descends from it, which is how the daemon recognizes that branch as the agent's. + +## Business logic — TL;DR + +- **How the packages fit together** - one product package on top of six libraries it is built from, plus two companions; every user-facing behavior is the product's, the libraries are the seams it is built on. +- **From a prompt to a pull request** - the user starts an agent [1] from the dashboard; the daemon gives it a checkout [3], frames the coding agent, honors the gates [4] it stops at, and publishes the work per its handoff [5] level when it ends. +- **While nobody is at the keyboard** - on one shared clock the daemon drains and refills the agent queue [7] (Auto PM [12] and the routines [13]), keeps its pull requests green and merges them, notifies, and reclaims what is on the remote — every start gated by the quota boundary [11]. +- **Work that runs somewhere else** - an agent's location [9] can be a GitHub Actions runner, a Claude Code cloud session (with the Claude web bridge [14] carrying its questions home), or a device [19] running another daemon; each is followed from this dashboard like a local agent. + +## Business logic + +### How the packages fit together + +#### Context + +**Business logic story**: the product is one npm package, `framework`; what it is built from is published as libraries so the coding-agent side of it — driving a coding agent, keeping shared files on a branch, the four skills [8] — can stand on its own. + +#### Business logic + +- `packages/framework` — the product: the `the-framework` CLI (four options, no verbs), the daemon it runs in the foreground (Ctrl-C closes it and every agent [1] it is running), the agent lifecycle, the dashboard the daemon serves — the product's only user interface — and every prompt an agent is sent, as markdown. Depends on all six libraries below. +- `packages/agent-driver` — the driver [2] seam: one contract for driving a coding agent as a black box, with the Claude Code, Codex, GitHub Actions and fake implementations; the product adds its own cloud-session implementation behind the same contract. The Framework prompts one turn at a time, lets the coding agent's own loop run to completion, and learns everything from the turn's final message: it never gates on the agent's individual tool calls, holds no model key, and runs on the user's own subscription. +- `packages/agent-data` — a branch of the project's repository used as a file store: the `agent-data` branch [6], checked out under `.branches/`, written through one sync → commit → push cycle that re-applies when a push loses a race. A library, not a skill: read by code, never by an agent. Every skill depends on it; nothing else does. +- `packages/skill-branches`, `packages/skill-tickets`, `packages/skill-queue`, `packages/skill-logs` — the four skills [8]: an agent's own checkout [3] and branch; the tickets with their plans and claims; the agent queue [7]; the record of every run [18]. Tickets, queue, runs and routine locks live on the `agent-data` branch, never on a code branch, so the default branch stays code only. No skill depends on another. +- `packages/chrome-extension` — the far end of the Claude web bridge [14], a Chrome extension reading claude.ai in a signed-in browser; it talks to the daemon over HTTP only. +- `packages/the-framework.ai` — the marketing site at https://the-framework.ai; it presents the product and shares no code with it. +- `.github/workflows` — every push builds, type-checks and tests the monorepo; the website deploys itself from the default branch; and one workflow is the far end of the `github-actions` driver implementation, running one turn per workflow run. + +### From a prompt to a pull request + +#### Context + +See `## Context`. + +#### Business logic + +The user activates a project from the dashboard, picked in the OS folder picker behind a trust confirmation: any dirty state is committed first, a `.the-framework/` directory is created with its ignore file and layout marker, and the project is registered in the daemon's one user file. From the project's home the user types a prompt or picks a preset, choosing the driver [2], the model, the location [9] and the handoff [5] level if the defaults are not wanted. + +The daemon gives the agent [1] a fresh checkout [3] — a git worktree under `.branches/` on the branch `agent-`, its dependency directories linked from the project's checkout, the four skills [8] linked in where the coding agent's harness looks and their commands on the PATH — checks that the chosen driver's coding agent can actually start, and spawns one process for the agent with its whole configuration as one JSON file. That process frames the coding agent with the built-in system prompt (plus the project's `SYSTEM.md` and the signal protocols), renders the user's text into the prompt's user slot, and lets the driver run a turn. + +The coding agent names its work before its first change (the session name [16]; the branch is renamed to `agent-`), commits as it goes — nothing is ever committed on its behalf — and ends each turn with a final message The Framework parses. A question with options is a gate [4]: the dashboard shows it where it happened, the user's pick is written to the agent's control file and re-prompts the agent, an unattended agent takes the recommended option, and a pick marked to stop ends the agent. The message may also carry markdown views for the dashboard's right rail, an error the user must fix, the pull request's title and body, and the ready-for-merge signal [17]. + +Everything the agent does is appended as events to `.the-framework/events.jsonl` in its checkout; the daemon tails that file to the browser, so watching now and reading later show the same record. Once the opening work settles, a build agent works the agent queue [7] one entry per turn, and then takes the user's own messages, each continuing the same driver session; a settled agent reads as waiting for the user, not as finished. + +When the agent ends, the handoff [5] runs at the level in force — by default push the branch and open a pull request named and described by the agent, with the issue reference of the ticket it worked; a merge is armed by configuration but authorized only by the agent's ready-for-merge signal, and a withheld merge is reported with its reason. An agent that committed nothing publishes nothing and leaves no branch behind. The run [18] is recorded on the `agent-data` branch [6] the moment the agent ends, and its checkout is reclaimed by a later sweep [10] once its work is on the remote — never before. + +### While nobody is at the keyboard + +#### Context + +See `## Context`. + +#### Business logic + +The daemon runs its sweeps [10] on one shared clock. Auto PM [12], when the user turned it on, works each project's agent queue [7] down — one agent per pass on the first open entry, the linked ticket claimed for that agent before it starts — and, once the queue is empty, refills it by running the routines [13]: update tickets from GitHub, triage quick wins and consensual work onto the queue, plan the tickets without a plan (several agents at once, one ticket each), and a calendar-paced maintenance sweep. Each routine runs once across machines, guarded by a routine lock on the `agent-data` branch [6] that the daemon takes before starting and frees when the agent ends; a lock left by a dead machine expires after four hours. + +Every unattended start is refused, with its reason reported, when Auto PM is off, when the project's concurrency cap is reached (the agents holding the slots are named), when an agent was started for the project moments ago, when the queue or the quota cannot be read, or when the account is past the quota boundary [11]: the pro-rated share of the quota week that has elapsed, rising continuously with the clock and shifted by the user's spend offset. The chosen model's own week gates unattended work the same way. + +The CI watch polls the pull requests The Framework is waiting to land: it merges one once its checks pass, and when a check goes red it starts one unattended fix agent per failing head commit, at most two attempts per pull request; a pull request older than a week is a human's to land. The notification watchers announce what is new on two feeds — what needs a human (an open question, a pull request to review, unpushed commits) and plain activity (an agent started or finished) — to the browser and, when configured, to Discord. Other sweeps reclaim the checkouts whose work is on the remote, adopt the branches cloud sessions pushed, and expire dead cloud refs. + +### Work that runs somewhere else + +#### Context + +**User story**: the user wants an agent to run on a fresh GitHub Actions runner, in a Claude Code cloud session on claude.ai, or on another machine of theirs — and to follow it from this dashboard as if it were local. + +#### Business logic + +An agent's location [9] is chosen at start and never changes. On `actions`, every turn is one workflow run on a GitHub Actions runner, with continuity carried by the branch the previous run pushed; the agent is followed like a local one. On `web`, the agent is hands-off [15]: the driver pushes a cloud anchor [20], the Claude web bridge's [14] extension creates the cloud session through claude.ai's repository picker on the chosen model, and the local agent ends there, with a link to the session. The bridge then carries any question the session parks on into the dashboard's open questions and types the pick back; the daemon later recognizes the branch the session pushed by its ancestry from the cloud anchor, records it on the run [18], and opens the armed pull request itself when the session opened none. The bridge runs either in the user's own Chrome or in a Chrome for Testing the daemon downloads and drives itself. + +A device [19] is another machine's daemon the user saved by URL and token: the local daemon forwards the start to it, streams the agent's events back and forwards steering, so the agent renders like a local one; the token never leaves the two daemons and is never persisted with the agent. diff --git a/packages/LOGIC.md b/packages/LOGIC.md new file mode 100644 index 000000000..083eddd39 --- /dev/null +++ b/packages/LOGIC.md @@ -0,0 +1,23 @@ +The deliverables The Framework ships, one directory each. The product is the `framework` package; every other package is either a library the product is built from — the driver [2] seam, a branch used as a file store, and the four skills [3] — or a companion: the Chrome extension of the Claude web bridge, and the marketing website. + +## Glossary + +[1] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[2] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. +[3] skill: one of the four capabilities an agent is taught — `branches`, `tickets`, `queue`, `logs` — each a package with the instructions the agent reads (its `SKILL.md`), a command on the agent's PATH, and an API the product calls. +[4] the `agent-data` branch: the branch of a project's repository used as a file store for everything agents share: tickets, the agent queue, the runs, routine locks. +[5] the Claude web bridge: the daemon's bridge endpoints plus the Chrome extension: carries the question a cloud session is parked on into the dashboard, and types the pick back into the session. + +## Business logic — TL;DR + +- **The product** (`framework/`) - the `framework` npm package: the `the-framework` CLI, the daemon, the agent [1] lifecycle, the dashboard it serves, and every prompt an agent is sent. Depends on all six packages below. +- **The driver seam** (`agent-driver/`) - the `agent-driver` npm package: one contract for driving a coding agent as a black box, with the Claude Code, Codex, GitHub Actions and fake driver [2] implementations. The product adds its own cloud-session implementation behind the same contract. Depends on nothing else here. +- **A branch used as a file store** (`agent-data/`) - the `@gemstack/agent-data` npm package: the `agent-data` branch [4] every skill keeps its files on, checked out under `.branches/`, written through one sync → commit → push cycle, plus the git runner and the git-exclude rule that machinery is built on. A library, not a skill: read by code, never by an agent. Every skill depends on it; it depends on nothing. +- **The `branches` skill** (`skill-branches/`) - the `@gemstack/skill-branches` npm package: one checkout per agent under `.branches/`, named as its branch, reclaimed once its work is on the remote; the `branches` command and the skill [3] every agent reads. Depends on `agent-data`. +- **The `tickets` skill** (`skill-tickets/`) - the `@gemstack/skill-tickets` npm package: the project's tickets with their plans and claims on the `agent-data` branch, the `tickets` command that reads, writes, claims and closes them, and the skill text. Depends on `agent-data`. +- **The `queue` skill** (`skill-queue/`) - the `@gemstack/skill-queue` npm package: the agent queue on the `agent-data` branch, the `queue` command that reads it, adds an entry at a priority and takes one off, and the skill text. Depends on `agent-data`. +- **The `logs` skill** (`skill-logs/`) - the `@gemstack/skill-logs` npm package: the record of every run agents made on a project, on the `agent-data` branch, and the read-only `logs` command; the product writes every run through it. Depends on `agent-data`. +- **The Claude web bridge's extension** (`chrome-extension/`) - a Chrome extension, not an npm package: the far end of the Claude web bridge [5], reading claude.ai in a signed-in browser. Talks to the product over HTTP only. +- **The website** (`the-framework.ai/`) - the marketing site at https://the-framework.ai. Presents the product; shares no code with it. + +No skill depends on another skill, and nothing but the product depends on a skill. diff --git a/packages/agent-data/LOGIC.md b/packages/agent-data/LOGIC.md new file mode 100644 index 000000000..f1cd8a8af --- /dev/null +++ b/packages/agent-data/LOGIC.md @@ -0,0 +1,22 @@ +A library, not a skill: it has no `SKILL.md` and no command. It turns a branch of the project's repository into a file store for everything coding agents share, the way `gh-pages` holds a site: the branch is checked out once under `.branches/`, every write is one sync → apply → commit → push cycle that re-applies the change when the push loses a race, and every read works from anywhere in the repository. In the product the branch is the `agent-data` branch [1], the writers are the daemon, through the branch's checkout [2], and the four skills' commands, one-shot from any clone of an agent [3] or a cloud session [4]; the skills (`branches`, `tickets`, `queue`, `logs`) and the product import this library and never each other. The source lives in `src/`, whose `LOGIC.md` tells the story file by file. `package.json` and the `tsconfig*.json` files configure the build and carry no business logic; `DECISIONS.md` records the human decisions behind the package. + +## Context + +**User story**: the user registers a project and, from then on, every machine and cloud session working it shares one set of tickets, one agent queue and one record of runs; nothing of that ever appears as a commit on a code branch, as a change to a tracked file, or as a diff in the user's own checkout. + +**Problem**: several writers push one branch with no coordinator, and a hung git call would hold the daemon. + +## Glossary + +[1] the `agent-data` branch: the branch of a project's repository used as a file store for everything agents share: tickets, the agent queue, the runs, routine locks. Born as an orphan, written through one sync → commit → push cycle. +[2] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. Also "the `agent-data` branch's checkout". +[3] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. +[4] cloud session: a Claude Code cloud session on claude.ai, the far end of a `web` agent. + +## Business logic — TL;DR + +- **A branch used as a file store** (`src/`, `src/file-branch.ts`) - checked out at `.branches/` and hidden from git, born an orphan or adopted from origin, written one cycle at a time (sync, apply, commit, push) with the change re-applied when the push loses a race and the remote winning a conflict, never force-pushed, pulled on the daemon's clock, read from anywhere without ever failing, and written one-shot from any clone through a throwaway checkout. +- **Git within a time budget** (`src/git.ts`) - every git call gets 10, 30 or 120 seconds by subcommand; a timeout is its own failure kind; a failure is summarized by git's own reason line. +- **Nothing tracked ever changes** (`src/git-exclude.ts`, `src/names.ts`) - the checkouts directory `.branches` is hidden through git's own `info/exclude`, written once for every worktree; the two names, `.branches` and `agent-data`, are spelled once for every consumer. +- **What consumers import** (`src/index.ts`) - the one entry point of the library, plus a `names` entry point for browser code. +- **Proven against real repositories** (`src/file-branch.test.ts`, `src/git.test.ts`) - the whole life of the branch, and the budgets and failure readings of the git runner. diff --git a/packages/agent-data/src/LOGIC.md b/packages/agent-data/src/LOGIC.md new file mode 100644 index 000000000..5c9e38ec3 --- /dev/null +++ b/packages/agent-data/src/LOGIC.md @@ -0,0 +1,20 @@ +The library every skill builds on: a branch of the project's repository used as a file store, checked out under `.branches/`, written through one sync → apply → commit → push cycle that re-applies the change when the push loses a race, and read from anywhere in the repository; underneath it, one way of running git with a time budget per subcommand, and one way of hiding a path from git without touching a tracked file. In the product the branch is the `agent-data` branch [1], and its writers are the daemon, through the branch's checkout [2], and the commands agents [3] run, one-shot from any clone. `git-exclude.BUG-ANALYSIS.md` is an analysis note and carries no code. + +## Context + +**User story**: every machine and cloud session working a project shares one set of tickets, one agent queue and one record of runs, without the user ever seeing a commit on a code branch or a diff in the project's tracked files. + +## Glossary + +[1] the `agent-data` branch: the branch of a project's repository used as a file store for everything agents share: tickets, the agent queue, the runs, routine locks. Born as an orphan, written through one sync → commit → push cycle. +[2] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. Also "the `agent-data` branch's checkout". +[3] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. + +## Business logic — TL;DR + +- **The two names** (`names.ts`) - `.branches`, the directory holding a project's persistent checkouts, and `agent-data`, the branch every skill keeps its files on; spelled once, importable from browser code. +- **Running git** (`git.ts`) - one runner with a time budget per subcommand (10 seconds for a read, 30 for a local mutation, 120 for the network or a whole checkout), a timeout that is its own failure kind, the line of a failure worth showing, and a push to `origin` that reports rather than throws. +- **Hiding the checkouts from git** (`git-exclude.ts`) - one ignore rule appended once to git's own `info/exclude` in the common git directory, so every worktree is covered and no tracked file changes. +- **The branch as a file store** (`file-branch.ts`) - the branch's checkout under `.branches/`, its birth as an orphan or adoption from origin, the serialized write cycle with its lost-race re-apply and its no-force rule, the pull, the reads from anywhere that never fail, and the one-shot write from any clone. +- **The entry point** (`index.ts`) - what every skill and the product import. +- **The tests** (`git.test.ts`, `file-branch.test.ts`) - the budgets and failure readings of the git runner; the whole life of the branch against real repositories: birth, adoption, writes, stranded commits, conflicts, lost races, the pull, serialization, reads, and one-shot writes. diff --git a/packages/agent-data/src/file-branch.LOGIC.md b/packages/agent-data/src/file-branch.LOGIC.md new file mode 100644 index 000000000..d9f363a8e --- /dev/null +++ b/packages/agent-data/src/file-branch.LOGIC.md @@ -0,0 +1,179 @@ +Implements a branch of the project's repository used as a file store: files that programs read and write and nobody edits in a working copy, the way `gh-pages` holds a site, kept on a branch of their own so that no code commit is ever in their history. In the product that branch is the `agent-data` branch [1]; the caller names it, and this module knows git, not what the files mean. Two writers share one rule: a long-lived process (the daemon) writes through the branch's persistent checkout [2] under `.branches/` in a write cycle [3] (sync, apply, commit, push), and a command an agent [4] runs writes one-shot from any clone through a throwaway checkout of origin's tip; both treat the change as an intent that is re-applied when the push loses a race, and neither ever force-pushes. + +## Context + +**User story**: an agent [4] runs `npx tickets`, `npx queue` or `npx logs` from its own checkout [2], on the user's machine or in a cloud session [5], and sees the tickets, the agent queue [6] and the runs [7] every other machine pushed; the daemon's own records are on the remote a moment after they are written; the user's own branches and tracked files never change, and the user never sees a diff. + +**Business logic story**: every skill package and the product's routine locks hand this module a change to apply to a directory and a commit message; this module owns the branch, its checkout, syncing, committing, pushing, and reading. "origin's copy of the branch" below means what the repository last fetched of the branch from its `origin` remote. + +**Problem**: several machines and cloud sessions write one branch with no coordinator. A stale commit force-fitted onto the branch would erase another writer's change, a change left half written in the checkout would ride the next unrelated commit, and a hung git call would hold the daemon. + +## Glossary + +[1] the `agent-data` branch: the branch of a project's repository used as a file store for everything agents share: tickets, the agent queue, the runs, routine locks. Born as an orphan, written through one sync → commit → push cycle. +[2] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. Also "the `agent-data` branch's checkout". +[3] write cycle: one write to the branch through its persistent checkout: sync with origin, apply the caller's change, commit, push; the change is re-applied when the push loses a race. +[4] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. +[5] cloud session: a Claude Code cloud session on claude.ai, the far end of a `web` agent. +[6] the agent queue: `TODO_AGENTS.md` on the `agent-data` branch: every task agents will work next, in priority sections, worked top-down. +[7] run: the `logs` skill's record of one agent on the `agent-data` branch: a card and a diary. Never the unit of work. +[8] one-shot write: a write to the branch from any clone by a command that holds no checkout of the branch: a throwaway checkout of origin's tip is created, the change applied, committed and pushed, and the checkout removed. + +## Business logic — TL;DR + +- **The branch's checkout, hidden from git** - the branch is checked out once at `/.branches/`, registered with git as a worktree, and hidden through git's own ignore file rather than a committed `.gitignore`. +- **Adopted from origin or born an orphan** - a branch missing locally is taken from origin's copy; missing there too, it is born from the empty tree with the commit "create the branch", so no code commit is ever an ancestor. +- **Only `origin` is the remote** - every fetch and push names `origin`; a repository without it is remote-less whatever other remotes it has, with a stated outcome for each operation. +- **One write at a time** - writes and pulls to one branch of one repository run one after another within a process, never interleaved. +- **The write cycle** - sync with origin, apply the change to the checkout, commit whatever changed under the caller's message, push whenever the branch is ahead of origin's copy. +- **Sync: rebase onto origin, and origin wins a conflict** - unpushed local commits are rebased onto origin's copy; when the rebase fails, the checkout is reset to origin's copy and those commits are dropped, unreported. +- **A push that loses a race re-applies the change once** - the attempt's commit is wound back, the cycle re-syncs and re-applies; a second failed push keeps the commit local and reports it, for the next cycle to carry out; never a force push. +- **A failed change leaves the checkout clean** - any other failure, a timeout included, resets the checkout to its last commit, removes stray files, and is reported rather than thrown. +- **The pull** - a write cycle with no change, run on the daemon's clock so this machine converges on what others pushed and pushes what an earlier cycle left stranded; a repository with no remote is an error it names. +- **Reads from anywhere, and never a failure** - a file or a directory listing is read off the checkout, the local branch, or origin's copy, from any directory of the repository, an agent's checkout included; whatever is missing reads as absent. +- **A one-shot reader opens the branch once** - one fetch, then every read off origin's copy, so a command sees every writer's pushes, its own one-shot writes included. +- **The one-shot write from any clone** - a throwaway checkout of origin's tip, applied, committed, pushed straight to the branch and removed; it never touches the persistent checkout nor moves the local branch. +- **A change sees plain files** - the change is handed a directory and reads, writes, deletes and lists plain files in it; a write creates missing parent directories. + +## Business logic + +### The branch's checkout, hidden from git + +#### Context + +**Problem**: the project's own tracked files must never change because of the file store, and a per-worktree ignore file looks right but is silently never read by git. + +#### Business logic + +The branch's persistent checkout [2] lives at `/.branches/` and is registered with git as a worktree of the project's repository, beside the agents' [4] own checkouts. Every operation through the checkout first checks that the directory is checked out on the branch and, when it is, does nothing more: the common case, taken on every write. Otherwise the branch is made to exist locally (next section), any stale worktree registration at that path (the directory was deleted by hand) is pruned so that the add is not blocked, and the checkout is created. The rule `/.branches` is then appended to git's own ignore file, `info/exclude` in the repository's common git directory (the rule in `git-exclude.ts`), never to a committed `.gitignore`: one line written once covers every worktree of the repository, and no tracked file changes. That last step is best effort: the checkout stands even when the rule could not be written. Making sure the branch and its checkout exist never fails loudly: a project this cannot be set up in reports why and is left alone. + +### Adopted from origin or born an orphan + +#### Context + +**Problem**: a second history born on a machine while origin already has one would leave two unrelated branches to reconcile, and a branch started from a code commit would carry the whole code history. + +#### Business logic + +A branch missing locally is first looked for on origin: when the repository has an `origin` remote the branch is fetched (a failed fetch is ignored), and origin's copy, when it exists, becomes the local branch. When origin has no such branch either, the branch is born parentless from git's empty tree with the commit message "create the branch", which touches no checkout and gives the branch a history that shares no commit with any code branch. A branch born locally is pushed by its first write cycle [3], even one whose change writes nothing, so origin gets it. + +### Only `origin` is the remote + +#### Context + +See `## Context`. + +#### Business logic + +Every fetch and push names `origin`. A repository whose only remote has another name is remote-less to this module, with one stated outcome per operation instead of a push that fails twice: a write cycle [3] commits locally and reports success with nothing pushed; a one-shot write [8] refuses with the reason `no-remote`; the pull reports the error "the repository has no remote, so the branch cannot be shared with other machines". Outside any repository git can list no remote, so a one-shot write there is refused the same way. + +### One write at a time + +#### Context + +**Problem**: a writer and the pull that interleaved on one checkout would commit each other's half-written files under the wrong message. + +#### Business logic + +Within one process, every write cycle [3], pull, and checkout setup for one branch of one repository runs one after another, in the order requested: the next waits for the previous to finish, and a failed one does not block the ones behind it. Two processes on one clone are not guarded against each other. + +### The write cycle + +#### Context + +See `## Context`. + +#### Business logic + +A write cycle [3] applies one change through the persistent checkout [2], in this order: + +- The checkout is made to exist. +- The checkout is synced with origin (next section). +- The caller's change runs against the checkout directory. +- Everything the checkout now differs by is staged, new files included, and committed when anything changed, under the caller's message: a fixed text, or a text computed after the change ran, since a batch only knows what it did once done. +- Without a remote, the cycle ends here: success, saying whether anything changed, with nothing pushed. +- With a remote, a push is owed whenever the local branch is ahead of origin's copy: this cycle's commit, an earlier cycle's commit the sync just rebased, or a branch origin does not have yet. When nothing is owed the cycle ends with nothing changed and nothing pushed; otherwise the branch is pushed to `origin` under its own name, never with force. + +The outcome reports whether the change changed anything and whether a push went out, or a failure with its reason and whether the change still landed as a local commit. + +### Sync: rebase onto origin, and origin wins a conflict + +#### Context + +**Problem**: a commit an earlier cycle could not push must not block every later write, and a conflict between it and what another machine pushed has no human to resolve it. + +#### Business logic + +Without a remote, a sync does nothing. With one, the branch is fetched from origin (a fetch that fails, the network being down, is ignored) and, when origin's copy exists, every unpushed local commit is rebased onto it. When the rebase fails for any reason, a conflict included, it is aborted and the checkout [2] is reset to origin's copy: the remote wins, every unpushed local commit is dropped without a report, and only the change of the current cycle is applied afterwards, against origin's state. Because the change is applied after the sync, an intent such as "append this entry" lands on top of the other machine's version rather than on the stale local one. + +### A push that loses a race re-applies the change once + +#### Context + +**Problem**: two writers pushing the same branch cannot both land; the loser must neither force its stale commit over the winner's nor apply its change twice. + +#### Business logic + +The change is an intent and the commit only its serialization, so the caller's change must be safe to run again. When the push fails on the first attempt, the attempt's commit is wound back to the tip the cycle started from (when a commit was made), the cycle syncs again, bringing in what the other writer pushed, and runs the change again against the fresher files, so the change lands exactly once. When the push fails on the second attempt too (the network, most likely), the commit stays local in the checkout [2] and the cycle reports the failure as "the branch could not be pushed: ", marked as committed: the next write cycle [3] or pull rebases that commit onto whatever origin has by then, and its push carries it out together with the new change. A push killed on its time budget counts as a failed push and may have landed anyway; the next sync's rebase absorbs a commit origin already has. The branch is never force-pushed. + +### A failed change leaves the checkout clean + +#### Context + +**Problem**: files a change left half written would be swept into the next cycle's commit, under an unrelated message, by the staging of everything. + +#### Business logic + +When anything else fails inside the cycle, the change itself, a git call that fails, or a git call killed on its time budget (the budgets are the rule in `git.ts`), the checkout [2] is reset to its last commit and every file not under version control is removed, so nothing half written survives; the failure is reported with its reason and marked as not committed. A write cycle [3] never throws: its callers run on the daemon's clock with nothing to catch it. + +### The pull + +#### Context + +**User story**: a machine that writes nothing still shows the tickets, the agent queue [6] and the runs [7] other machines and cloud sessions [5] pushed, without waiting for its next local write. + +#### Business logic + +The pull is a write cycle [3] with an empty change and the commit message "sync", behind the same one-at-a-time rule: it creates the checkout [2] when needed, so a fresh clone converges on its first pull; syncs in what others pushed; and pushes anything an earlier failed cycle left stranded, by the same owed-push rule. It reports success when converged with origin, and an error when the cycle failed or when the repository has no remote: a repository nothing can reach is an error state the caller has to surface, not a mode the pull supports. An error is also logged as "[branches] : ". The pull never throws. + +### Reads from anywhere, and never a failure + +#### Context + +**Problem**: an agent's [4] checkout holds none of the branch's files, and a read that could fail would turn every missing file into an error for every skill. + +#### Business logic + +A read starts by finding the repository a directory belongs to: the directory holding the real `.git`, which from an agent's [4] checkout [2] is the project's checkout the worktree was made from (worktrees share the repository's branches, so a read from an agent's checkout sees the same files without holding a copy). One file is read, in order, from the branch's persistent checkout on disk when this repository has one checked out on the branch and the file is there; else off the local branch; else off origin's copy of the branch (a clone that fetched but never branched, the cloud session's [5] case); it is absent when none of them has it. A read marked fresh, for a long-lived process about to act on the files where the local branch may trail what others pushed, fetches origin's copy first when there is a remote and reads it before the local branch, skipping the checkout. A directory's entries are listed by name off the local branch, else off origin's copy, and are empty when neither has the directory. A missing file, a missing branch, a git that could not run, and a directory outside any repository all read as absent, never as a failure. + +### A one-shot reader opens the branch once + +#### Context + +**Problem**: a command an agent [4] runs holds no checkout [2], and its own one-shot writes [8] go straight to the remote without moving the local branch, so only origin's copy has every writer's pushes. + +#### Business logic + +A one-shot reader fetches the branch from origin once, when the repository has an `origin` remote, and then reads every file and directory off origin's copy; the local branch is read only when origin's copy is not there (no remote, or nothing ever fetched). Nothing is fetched again for the reader's lifetime, and no checkout is held. A file the copy lacks reads as absent and a directory it lacks lists as empty. + +### The one-shot write from any clone + +#### Context + +**User story**: an agent [4] claims a ticket or edits the agent queue [6] from its own checkout [2] or from a cloud session [5], and the change is on origin when the command returns. + +**Problem**: the persistent checkout belongs to a long-lived process whose cycle stages everything it finds and resets the checkout on failure, so a second writer inside it would be committed under the wrong message or wiped. + +#### Business logic + +A one-shot write [8] refuses with `no-remote` when the repository has no `origin`. Otherwise the branch is fetched from origin and a throwaway checkout of origin's tip is created in the system's temporary directory and registered with git as a worktree of the clone; when origin has no such branch yet, the write itself births it parentless from the empty tree with the message "create the branch". The change runs against the throwaway checkout and everything is staged; when nothing changed the write ends with no commit and no push, reporting no change. Otherwise the commit is made under the caller's message and pushed straight to the branch on origin. When the push loses a race, the throwaway checkout is reset to origin's re-fetched tip and the change runs again, once; a second failed push throws with git's reason, and there is nothing left to retry. Whether or not the push landed, the throwaway checkout is removed, its registration pruned, and its directory deleted. The write never touches the branch's persistent checkout and never moves the clone's local branch: the persistent checkout, when there is one, converges on its own next pull. + +### A change sees plain files + +#### Context + +**Problem**: git keeps no empty directory, so a skill's folder vanishes with its last file and is absent on a newborn branch. + +#### Business logic + +A change is handed the checkout [2] directory and works with plain files under it: read one, write one, delete one, list a directory by file name. A write creates the missing parent directories. A read that cannot be made is reported to the change as a rejection it reads as "absent", and listing a missing directory yields no entries. diff --git a/packages/agent-data/src/file-branch.test.LOGIC.md b/packages/agent-data/src/file-branch.test.LOGIC.md new file mode 100644 index 000000000..31a2c5550 --- /dev/null +++ b/packages/agent-data/src/file-branch.test.LOGIC.md @@ -0,0 +1,11 @@ +What the tests cover, against real git repositories with a bare `origin` and a second clone standing in for another machine: + +- **Birth and checkout** - making the branch exist births it parentless (it shares no commit with `main`), checks it out at `.branches/`, hides the checkout from the project's own git status, and doing it again changes nothing and still reports success. +- **Adoption** - a branch origin already has is adopted with its files, instead of a second history being born. +- **A write** - commits on the branch under the caller's message, pushes it to origin, and leaves `main` untouched; a change that writes nothing commits nothing and reports no change; in a repository with no remote the write lands locally and reports that nothing was pushed. +- **Stranded commits and conflicts** - a write syncs in what another machine pushed and carries out an earlier local-only commit together with its own; a stranded commit that conflicts with origin's version resolves toward origin, and the change is re-applied on top of origin's version. +- **A lost race** - a push rejected because another writer landed in between re-runs the change once against the fresher files: the change is on the branch exactly once, next to the other writer's file. +- **The pull** - converges the checkout on what another machine pushed; in a repository with no remote it reports an error naming the missing remote. +- **One write at a time** - three concurrent writes run one after another, never interleaving, and all three land in order. +- **Reads** - a file reads off the checkout, from an agent's own checkout of the same repository too, which resolves to the project as its repository; a missing file reads as absent; after another machine moved origin on, a plain read still shows the checkout while a fresh read shows origin; a clone that holds no local branch reads origin's copy and lists a directory off it, and a missing directory lists as empty. +- **The one-shot write** - from a clone, lands on origin under the caller's message and births the branch when origin has none; the clone holds no local branch afterwards; another machine's persistent checkout is left where it was until its own pull; a lost race re-runs the change once and both writers' files land; no throwaway worktree stays registered; a change that writes nothing makes no commit; a repository without `origin` is refused as `no-remote`, even when it has a remote by another name. diff --git a/packages/agent-data/src/git-exclude.LOGIC.md b/packages/agent-data/src/git-exclude.LOGIC.md new file mode 100644 index 000000000..d625f0bfa --- /dev/null +++ b/packages/agent-data/src/git-exclude.LOGIC.md @@ -0,0 +1,7 @@ +Hides a path from git without touching any tracked file: one ignore rule is appended to `info/exclude` in the repository's common git directory, the ignore file that is git's own rather than the project's, so no `.gitignore` changes and the user never sees a diff. The rule goes in the common git directory rather than in a worktree's own because git reads excludes only from there; one line, written once, covers every worktree of the repository, an agent's checkouts included. + +## Business logic — TL;DR + +- **Written once** - a rule already present as a line of the file, ignoring surrounding whitespace, is left alone; otherwise it is appended on a line of its own (after a newline when the file does not end with one), and the `info` directory is created when it is missing. +- **Where the file is** - the common git directory is asked from git and resolved against the repository when git answers with a relative path; an empty answer means nothing is written. +- **Failure is the caller's call** - outside a repository, or with a git directory that cannot be written, the write fails with git's or the file system's error and the caller decides whether that matters; the checkout code in `file-branch.ts` treats it as best effort and keeps the checkout. diff --git a/packages/agent-data/src/git.LOGIC.md b/packages/agent-data/src/git.LOGIC.md new file mode 100644 index 000000000..937b3b91b --- /dev/null +++ b/packages/agent-data/src/git.LOGIC.md @@ -0,0 +1,79 @@ +Runs git for the whole product: one runner executes `git` in a directory, resolves its output, fails on a non-zero exit, and gives every invocation a time budget chosen by its subcommand, so a hung git never holds the daemon for long and a push killed on its budget is never mistaken for a push git rejected. It also fixes the two readings of a failed invocation every caller wants: whether it was a timeout, and which line is worth showing. + +## Context + +**User story**: the dashboard stays responsive and the daemon keeps starting agents [1] on a large repository: a `git worktree add` that writes every tracked file, or a `git push` that uploads a packfile, gets the minutes it needs, while a stuck read fails within seconds instead of freezing a sweep [2]; when a push fails, the user sees git's own reason ("fatal: …") rather than an echoed command line. + +**Problem**: one flat budget for every git call is wrong in both directions. A read that hangs holds its caller for the whole budget, so the budget must stay short; a whole-checkout [3] write or an upload killed on a short budget may have half happened, so those must get far longer. + +## Glossary + +[1] agent: the unit of work: one task worked by a coding agent under The Framework's control, in its own checkout, on its own branch, streaming events, handed off when it ends. +[2] sweep: a background job the daemon runs on its clock: Auto PM, the CI watch, the notification watchers, the sweep that reclaims checkouts, the branch-links sweep, the cloud scratch sweep, cloud work adoption. +[3] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. Also "the `agent-data` branch's checkout". + +## Business logic — TL;DR + +- **A time budget per subcommand** - reads get 10 seconds, local mutations 30 seconds, and the network or a whole checkout 120 seconds; the subcommand decides, whatever global options precede it. +- **A timeout is its own kind of failure** - a git killed for outrunning its budget fails as a timeout, recognizable across packages, distinct from a git that rejected the operation. +- **The line worth showing** - a failed invocation is summarized by git's own `fatal:`, `error:` or `remote:` line, else its first line, else "git failed". +- **Inside a repository, and where its checkout starts** - whether a directory is inside a git working tree, answered "no" whenever git cannot say, and the root of the checkout a directory is in. +- **Pushing a branch to origin** - one push that sets the branch's upstream, whose failure is reported as git's reason rather than thrown. + +## Business logic + +### A time budget per subcommand + +#### Context + +See `## Context`. + +#### Business logic + +Every invocation runs under a budget chosen from its subcommand: + +- Reads get 10 seconds: `branch` in its listing and query forms, `cat-file`, `diff`, `for-each-ref`, `log`, `ls-files`, `merge-base`, `remote`, `rev-list`, `rev-parse`, `show`, `show-ref`, `status`, `symbolic-ref`, and `worktree list`. +- The network and a whole checkout [3] get 120 seconds: `clone`, `fetch`, `pull`, `push`, `ls-remote`, and `worktree add`. +- Every other subcommand is a local mutation and gets 30 seconds, `worktree remove` and `worktree prune` among them, and so does any subcommand the runner does not know: an unknown operation is treated as a mutation, never as slow. +- `branch` reads when it is bare, or when every flag it carries lists or queries (`--list`, `--contains`, `--no-contains`, `--merged`, `--no-merged`, `--points-at`, `--show-current`, `--remotes`, `--all`, `--verbose`, `--format=…`, and their short forms); a `-D`, a `-m`, any other flag, or the `branch [start]` form writes a ref and gets the mutation budget. +- Global options placed before the subcommand (`--no-pager`, `-C `, `-c `, `--git-dir`, `--work-tree`, `--namespace`, `--exec-path`, in both their separate and their `--option=value` forms) are skipped, so the subcommand itself is what is classified: `git -C /repo push` is a push, not a read of `/repo`. + +### A timeout is its own kind of failure + +#### Context + +**Problem**: a push killed on its budget usually writes nothing to stderr, so without a distinct failure kind it surfaces as a bare failed push, and a caller cannot tell "the network was slow, the commit may have landed" from "git refused". + +#### Business logic + +A git killed for outrunning its budget fails with the message "git timed out after ms", and the failure carries a mark any caller in any package can check to recognize a timeout, as opposed to a git that exited with an error of its own. An invocation killed for printing more than the runner holds is an ordinary failure, not a timeout. + +### The line worth showing + +#### Context + +**Problem**: the process runner buries git's useful line under its own "Command failed: git …" preamble, which is what the user would otherwise see. + +#### Business logic + +The summary of a failed invocation is git's own line beginning with `fatal:`, `error:` or `remote:` (in any letter case) when there is one, else the first non-empty line of the message, else the placeholder "git failed". + +### Inside a repository, and where its checkout starts + +#### Context + +**Problem**: "this project cannot host a checkout at all" and "git was there and the operation failed" are the same rejection out of `git worktree add`, but call for opposite handling. + +#### Business logic + +Whether a directory sits inside a git working tree is answered by git, and only a clear "yes" counts: a missing or unreadable git reads as "no repository", the conservative answer for a caller that treats a repository's failure as fatal. The root of the checkout [3] a directory is in (an agent's [1] own, from anywhere under it) is git's top-level directory for that directory; outside a repository the question fails. + +### Pushing a branch to origin + +#### Context + +**Business logic story**: the product pushes an agent's [1] branch to `origin` when the agent ends, and tells the user why when it cannot. + +#### Business logic + +A branch is pushed to `origin` with its upstream set. The result is success, or failure carrying the line worth showing; nothing is thrown. A push killed on its budget reports the timeout line, which names the command and the budget. diff --git a/packages/agent-data/src/git.test.LOGIC.md b/packages/agent-data/src/git.test.LOGIC.md new file mode 100644 index 000000000..bbb3551c4 --- /dev/null +++ b/packages/agent-data/src/git.test.LOGIC.md @@ -0,0 +1,7 @@ +What the tests cover: + +- **Budgets by subcommand** - `push`, `worktree add`, `clone` and `fetch` get the 120-second budget; `add`, `commit`, `init`, `checkout`, `worktree remove` and `worktree prune` the 30-second one; `ls-files`, `status`, `rev-parse`, `rev-list`, `log`, `diff`, `show`, `remote`, `symbolic-ref`, `show-ref`, `for-each-ref`, `worktree list` and the listing forms of `branch` the 10-second one, while `branch -D`, `branch -m` and `branch ` are mutations. +- **The three budgets stay distinct** - pinned at 10, 30 and 120 seconds, and a slow operation gets more than twice a read's budget, so no read is ever widened to fit a slow operation. +- **Unknown and disguised subcommands** - an unknown subcommand and an empty command line are mutations; leading global options, the value of a global option (`-C /repo`, `-c key=value`, `--git-dir`, `--work-tree`, `--namespace`, `--exec-path`) and the inline `--option=value` form never hide the subcommand, and `worktree add` behind `-C` still gets the slow budget. +- **A failed push is reported, not thrown** - the result carries the failure; a timed-out push names the command and says "timed out after 120000ms" instead of reading like a rejected push; a timeout is distinguishable from git's own rejection. +- **The reason line** - a failure shows git's own `fatal:` line rather than the echoed command line, and the plain message when there is no such line. diff --git a/packages/agent-data/src/index.LOGIC.md b/packages/agent-data/src/index.LOGIC.md new file mode 100644 index 000000000..0e73816c5 --- /dev/null +++ b/packages/agent-data/src/index.LOGIC.md @@ -0,0 +1 @@ +The package's entry point: one import gives a consumer the two names (`.branches` and `agent-data`), the git runner with its time budgets and its two readings of a failure, the writer of git's own ignore rule, and every operation on a branch used as a file store: its checkout, the write cycle, the pull, the reads from anywhere and the one-shot write. The four skill packages (`skill-branches`, `skill-tickets`, `skill-queue`, `skill-logs`) and the product (`packages/framework`) import from here; no skill imports another skill, since this library is what they share. A second entry point, `names`, exposes only the two names, for code that must not load Node modules such as the dashboard. diff --git a/packages/agent-data/src/names.LOGIC.md b/packages/agent-data/src/names.LOGIC.md new file mode 100644 index 000000000..457262fa7 --- /dev/null +++ b/packages/agent-data/src/names.LOGIC.md @@ -0,0 +1 @@ +Fixes the two names every consumer of the package hangs off, as plain values with no dependency on Node, so browser code can name them too through the package's `names` entry point. `.branches` is the directory at a project's root that holds the project's persistent checkouts, one per branch and named as its branch: the agents' own checkouts beside the `agent-data` branch's. It is dotted on purpose: a `*` glob skips a leading dot, so the type-checkers, test runners and formatters run in the project never descend into N full copies of the repository. `agent-data` is the name of the one branch every skill keeps its files on, a path per skill, checked out once at `.branches/agent-data`; it is spelled here once and imported everywhere else. diff --git a/packages/agent-driver/LOGIC.md b/packages/agent-driver/LOGIC.md new file mode 100644 index 000000000..3ad22d79b --- /dev/null +++ b/packages/agent-driver/LOGIC.md @@ -0,0 +1,21 @@ +The `agent-driver` package is how The Framework works a repository without ever calling a model itself: it wraps a coding agent [1] the user already pays for as a driver [2], a black box started in a directory, prompted for one turn [3] at a time, streamed as progress events [4] and resumed later, and ships the implementations of that contract for Claude Code on this machine, Codex on this machine, Claude Code on a GitHub Actions runner, and a scripted fake for tests and offline demos. The product's agent [5] lifecycle in `packages/framework` speaks only this contract, which is what lets it add Claude Code in a cloud session [6] as a fifth implementation and swap one coding agent for another without changing anything above. `package.json`, `tsconfig.json`, `tsconfig.build.json` and `tsconfig.test.json` configure the build and the test runner and carry no business logic; `dist/` and `dist-test/` are build output; each source file's `*.BUG-ANALYSIS.md` records when it was last reviewed for bugs. + +## Context + +**User story**: the user picks `claude` or `codex` as an agent's [5] driver [2] and where its turns [3] run; the agent view shows what the coding agent [1] says and does and what it spent, the dashboard shows the account's quota [7] for Claude Code, and stopping the agent leaves nothing of the coding agent running. + +**Business logic story**: every implementation runs on the user's own login to the coding agent [1], a subscription in the normal case, or on the OAuth token the repository holds for a runner; The Framework holds no model key. The seam is the prompt, the final message and the code left behind, never the coding agent's tool calls: a turn [3] succeeds on the process's exit code or the run's conclusion, and the caller shows the progress events [4] but never decides on them. + +## Glossary + +[1] coding agent: the CLI doing the actual work: Claude Code or Codex. +[2] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. The user's driver choice is `claude` or `codex`; the driver implementations are `claude-code`, `codex`, `github-actions`, `claude-web` and `fake`. +[3] turn: one prompt sent to the driver; the coding agent's own loop runs to completion and answers with a final message. +[4] progress event: what a driver reports while a turn runs, for a caller to show and never to decide on: the prompt sent, the session id, streamed text, a tool used, the final result, a rate limit reading, an error, a notice. +[5] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[6] cloud session: a Claude Code cloud session on claude.ai, the far end of a `web` agent. +[7] quota: the account's subscription allowance, as the coding agent reports it: a session window and a quota week, each with a percentage used. + +## Business logic — TL;DR + +- **The driver seam and its implementations** (`src/`) - the contract every driver [2] honors, the process core shared by the local implementations with its stop and reaping rules, the Claude Code, Codex, GitHub Actions and fake implementations, and the reader of Claude Code's quota [7]; told in `src/LOGIC.md`. diff --git a/packages/agent-driver/src/LOGIC.md b/packages/agent-driver/src/LOGIC.md new file mode 100644 index 000000000..4b07314b3 --- /dev/null +++ b/packages/agent-driver/src/LOGIC.md @@ -0,0 +1,69 @@ +The driver [1] seam of The Framework: one contract under which a coding agent [2] is a black box, started in a directory, prompted for one turn [3] at a time, streamed as progress events [4] and resumed later, and four implementations of it: Claude Code on this machine, Codex on this machine, Claude Code on a GitHub Actions runner, and a scripted fake. The product's agent [5] lifecycle in `packages/framework` speaks only this contract, so a fifth implementation, Claude Code in a cloud session [6] (`packages/framework/src/driver/cloud.ts`), slots in behind it without touching anything above. Everything here runs on the user's own login to the coding agent: The Framework never holds a model key and never calls a model itself. + +## Context + +**User story**: the user picks `claude` or `codex` as the driver [1] for an agent [5], and a location [7] for it; the agent view then shows what the coding agent [2] says, which tools it uses, what it spent, and, for Claude Code, where the account's quota [8] stands. Stopping the agent, or closing The Framework with Ctrl-C, leaves no coding agent process running on the machine. + +**Business logic story**: the seam is deliberately the prompt, the final message and the code left in the directory, never the coding agent's individual tool calls. Each coding agent keeps its own subscription login and its own loop; The Framework prompts it, reads its output for text, tool names, a session id, usage [9] and rate limit [10] readings, and decides a turn's [3] success from the process's exit code or the run's conclusion, never from streamed text. + +## Glossary + +[1] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. The user's driver choice is `claude` or `codex`; the driver implementations are `claude-code`, `codex`, `github-actions`, `claude-web` and `fake`. +[2] coding agent: the CLI doing the actual work: Claude Code or Codex. +[3] turn: one prompt sent to the driver; the coding agent's own loop runs to completion and answers with a final message. +[4] progress event: what a driver reports while a turn runs, for a caller to show and never to decide on: the prompt sent, the session id, streamed text, a tool used, the final result, a rate limit reading, an error, a notice. +[5] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[6] cloud session: a Claude Code cloud session on claude.ai, the far end of a `web` agent. +[7] location: where an agent's turns run: `local` (this machine), `actions` (a GitHub Actions runner), or `web` (a Claude Code cloud session). +[8] quota: the account's subscription allowance, as the coding agent reports it: a session window and a quota week, each with a percentage used. +[9] usage: what one turn spent, as the coding agent reports it: token counts, and a notional price in US dollars when the coding agent prices its turns. +[10] rate limit: the coding agent's per-turn reading of whether the account may still spend against one quota window, and when that window resets. +[11] driver session: the coding agent's own conversation for one agent, which the driver can resume by its session id. +[12] framing: the standing instructions a caller gives a driver session, plus any extra instructions for one turn; the driver delivers them as the coding agent's system prompt, or ahead of the prompt when the coding agent has no system prompt flag. +[13] stop request: the caller's signal that a driver session, or one turn of it, must end now; the product raises one when the user stops the agent. +[14] checkout: an agent's own working copy of the project: a git worktree under the project's `.branches/` directory, named as its branch. +[15] live chat: the user's own messages to a running agent, each continuing the same driver session. One of them is a message. +[16] correlation id: the id the `github-actions` driver makes up for one turn and hands the workflow, which echoes it into the run's display name and the artifact's name; it is the only way the driver finds its own run. + +## Business logic — TL;DR + +- **The contract** (`types.ts`) - what every driver [1] promises, how a driver session [11] is started, what one turn [3] returns, the eight kinds of progress event [4], the three readings of spend (usage [9], rate limit [10], quota [8]), and which reasons for an empty quota reading are transient. +- **What every driver session shares** (`session-support.ts`) - reporting progress events without letting a listener break the coding agent [2], folding the driver session's stop request [13] and framing [12] with a turn's own, and reading a file out of the directory. +- **One turn as one process** (`cli-session.ts`, `cli-session.test.ts`) - a local coding agent is spawned as its own process-group leader in the driver session's directory, fed the prompt over standard input, streamed line by line through the driver's parser, and judged on its exit code; a stop request terminates the whole tree, with a forced kill 5 seconds later. +- **Reaping every process tree** (`child-registry.ts`) - every live process group is registered so a stop reaches the whole tree with one signal and a hard exit of The Framework still kills every tree on the way out. +- **Claude Code on this machine** (`claude-code.ts`, `claude-code.test.ts`) - one non-interactive `claude` invocation per turn with framing as the system prompt, text, tool names, session id, usage and rate limit readings read off its streamed JSON, the driver session continued and revived through Claude Code's own resume, and a vanished conversation retried once as a fresh turn. +- **The account's quota** (`claude-code-quota.ts`, `claude-code-quota.test.ts`) - Claude Code's own usage readout parsed into windows with a percentage each, and every empty reading named by a reason that says whether the attempt failed or the login has no quota. +- **Codex on this machine** (`codex.ts`, `codex.test.ts`) - one non-interactive `codex` invocation per turn, sandboxed to the directory, framing ahead of the prompt, the last message as the answer, tokens without a price, never resumed and with no quota. +- **Claude Code on a GitHub Actions runner** (`actions.ts`, `actions.test.ts`) - each turn dispatches the agent workflow with the prompt as an input, finds its run by a correlation id [16], waits up to 1 hour, reads the transcript back from the run's artifact and replays it in a burst; continuity across turns is one branch the driver names and every run pushes to. +- **Reading a run's artifact** (`actions-zip.ts`, `actions-zip.test.ts`) - the zip archive GitHub hands back is read entry by entry and refused outright when it is not an archive, never read short. +- **The scripted fake** (`fake.ts`, `fake.test.ts`) - scripted or responder-driven turns with the same progress events as a real driver, no process and no model, for tests and offline demos. +- **The entry point** (`index.ts`) - everything the product may import: the contract, the four drivers with their parsers, the quota reader and the pieces an outside driver builds on; the zip reader stays internal. +- **A turn on this machine, end to end** - how a local driver's command line and parser, the shared process core, the isolated reporter and the process registry together carry one turn from prompt to exit code, and what a stop does to the process tree. +- **Where the implementations differ** - how each implementation starts its coding agent, delivers framing, resumes a conversation, reports spend and quota, reads code back, and whose login it spends. + +## Business logic + +### A turn on this machine, end to end + +#### Context + +**User story**: the user starts an agent [5] with location [7] `local` and follows it in the agent view; the user stops it, or closes The Framework, and nothing of the coding agent [2] survives. + +#### Business logic + +The Claude Code and Codex drivers [1] each supply only two things for a turn [3]: the command line to run and the parser that understands their coding agent's [2] output. Everything about the process is shared (`cli-session.ts`): the coding agent is spawned in the driver session's [11] directory, the agent's [5] checkout [14], as the leader of its own process group; the prompt is fed over standard input; a `start` progress event [4] announces the turn; every output line goes through the driver's parser and its progress events are forwarded through the isolated reporter (`session-support.ts`) as they come; and the exit code decides the turn, so a crash after streamed text is a failed turn. While the process lives its group is registered (`child-registry.ts`), so a stop request [13] terminates the whole tree at once, with a forced kill 5 seconds later, and a hard exit of The Framework's process kills every registered tree on the way out. A turn has no time limit of its own on this machine; only the GitHub Actions driver gives up waiting, after 1 hour. + +### Where the implementations differ + +#### Context + +**Business logic story**: the contract is one, the coding agents [2] are not. The differences below are each coding agent's own business; the caller sees the same driver session [11], turns [3] and progress events [4] whichever implementation carries the agent [5]. + +#### Business logic + +- **How the coding agent is started**: Claude Code and Codex are one process per turn [3] on this machine; the GitHub Actions implementation dispatches one workflow run per turn on a fresh runner; the fake starts nothing. +- **How framing reaches the coding agent**: Claude Code takes the framing [12] as an addition to its system prompt; Codex and the GitHub Actions workflow take it ahead of the prompt as its own block; the fake ignores it. +- **Resuming a conversation**: Claude Code resumes its own conversation by session id on this machine, and retries once fresh when that conversation is gone; the GitHub Actions implementation hands the session id to the workflow to resume; Codex and the fake never resume, so a live chat [15] message to a Codex agent starts fresh. +- **What is spent**: Claude Code reports tokens and a price; Codex reports tokens and no price, never zero; only Claude Code reports rate limit [10] readings, and only the `claude-code` implementation reads the account's quota [8]; Codex, the GitHub Actions implementation and the fake offer no quota reading rather than a made-up number. +- **Where the code is read**: the local implementations and the fake read a file from the directory or the seeded files; the GitHub Actions implementation reads it from the branch the run pushed, because the runner is gone. +- **Whose login is spent**: Claude Code's and Codex's own logins on this machine, and the OAuth token the repository holds as a secret on a runner. In every case the caller never holds a model key. diff --git a/packages/agent-driver/src/actions-zip.LOGIC.md b/packages/agent-driver/src/actions-zip.LOGIC.md new file mode 100644 index 000000000..b5e2f5b44 --- /dev/null +++ b/packages/agent-driver/src/actions-zip.LOGIC.md @@ -0,0 +1,7 @@ +Reads every file out of the zip archive GitHub hands back when a run's artifact is downloaded, since the artifact download is the one channel out of a GitHub Actions run that the `github-actions` driver can read and GitHub always answers with a zip, even for a single file. It reads only what the upload-artifact action writes, stored or deflated entries, and refuses anything else rather than returning a partial archive: a transcript silently cut short would read as an agent that said less than it did. It is an implementation detail of the driver in `actions.ts`, not product API. + +## Business logic — TL;DR + +- **Every entry, by the authoritative listing** - the archive's central directory names every file with its size and compression, so each entry is read from there, never by scanning for file headers whose sizes may be deferred. +- **Stored or deflated, nothing else** - an entry stored as is, or compressed with deflate, is decompressed; any other compression method fails the read, naming the file, as does a header that is not where the listing says. +- **Not an archive is refused** - bytes too short to be an archive, or without the record that closes an archive, fail the read outright; an archive ending in a comment is still read, since the closing record is searched backwards past the comment. diff --git a/packages/agent-driver/src/actions-zip.test.LOGIC.md b/packages/agent-driver/src/actions-zip.test.LOGIC.md new file mode 100644 index 000000000..0146910c6 --- /dev/null +++ b/packages/agent-driver/src/actions-zip.test.LOGIC.md @@ -0,0 +1,6 @@ +What the tests cover, against archives assembled byte for byte as the upload-artifact action writes them: + +- **Deflated entries** - the transcript and the branch metadata come out of a deflated archive by name and with their exact contents, including a transcript large enough to span more than one deflate block. +- **Stored entries** - a tiny file the action stores uncompressed is read as is. +- **Not an archive** - bytes that are not a zip archive, or too short to be one, fail the read instead of being read short. +- **A trailing comment** - an archive carrying a comment after its closing record is still read. diff --git a/packages/agent-driver/src/actions.LOGIC.md b/packages/agent-driver/src/actions.LOGIC.md new file mode 100644 index 000000000..b744648d2 --- /dev/null +++ b/packages/agent-driver/src/actions.LOGIC.md @@ -0,0 +1,173 @@ +Drives Claude Code on a GitHub Actions runner as a driver [1]: each turn [2] dispatches one run of the project's agent workflow with the prompt as a workflow input, finds that run by a correlation id [3] of its own making, waits for it to finish, downloads the transcript the run uploads and replays it as the turn's progress events [4] and final message. Every run is a fresh runner with a fresh copy of the repository, so continuity across turns is a branch the driver names and every run pushes its work to. Its implementation id is `github-actions`. + +## Context + +**User story**: +- The user starts an agent [5] whose location [6] is `actions`; its turns [2] run on GitHub's runners instead of this machine, and the agent view shows a link to the run while it is in progress and, once it has finished, everything Claude Code said and did, all at once. +- The user picks a model for the agent, and sends live chat [7] messages that continue Claude Code's conversation on the next run. + +**Business logic story**: the workflow the driver [1] dispatches is `.github/workflows/framework-agent.yml`. On the runner, Claude Code authenticates with an OAuth token the repository holds as a secret, produced by `claude setup-token`, so every run spends the user's subscription; The Framework never holds a model key and passes none. The driver itself holds a GitHub token that dispatches the workflow and reads runs, artifacts and file contents; it must be a user's token, since the action refuses an agent run triggered by a bot. What the driver requires of the workflow: the inputs `prompt`, `correlation_id` and `branch`, optionally `model` and `resume_session_id`; the correlation id [3] echoed into the run's display name; the run's work pushed to the `branch` input; and one uploaded artifact named after the correlation id, holding `execution.json` (Claude Code's transcript as a JSON array) and `meta.json` (the branch the run pushed). The workflow answers with minutes, not seconds, and with no live stream: the transcript arrives once, at the end. + +## Glossary + +[1] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. +[2] turn: one prompt sent to the driver; the coding agent's own loop runs to completion and answers with a final message. +[3] correlation id: the id the driver makes up for one turn and hands the workflow, which echoes it into the run's display name and the artifact's name; it is the only way the driver finds its own run, since dispatching a workflow answers with no run id. +[4] progress event: what a driver reports while a turn runs, for a caller to show and never to decide on: the prompt sent, the session id, streamed text, a tool used, the final result, a rate limit reading, an error, a notice. +[5] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[6] location: where an agent's turns run: `local` (this machine), `actions` (a GitHub Actions runner), or `web` (a Claude Code cloud session). +[7] live chat: the user's own messages to a running agent, each continuing the same driver session. One of them is a message. +[8] driver session: the coding agent's own conversation for one agent, which the driver can resume by its session id. +[9] framing: the standing instructions a caller gives a driver session, plus any extra instructions for one turn; the driver delivers them as the coding agent's system prompt, or ahead of the prompt when the coding agent has no system prompt flag. +[10] stop request: the caller's signal that a driver session, or one turn of it, must end now; the product raises one when the user stops the agent. +[11] coding agent: the CLI doing the actual work: Claude Code or Codex. +[12] usage: what one turn spent, as the coding agent reports it: token counts, and a notional price in US dollars when the coding agent prices its turns. +[13] quota: the account's subscription allowance, as the coding agent reports it: a session window and a quota week, each with a percentage used. + +## Business logic — TL;DR + +- **One turn is one workflow run** - a turn [2] dispatches the workflow with the framing [9] placed ahead of the prompt, the correlation id [3], the run branch, and the model and session id when there are any, on the branch the previous run pushed or else the configured ref or `main`. +- **The correlation id** - `-turn-`, where the driver session [8] id carries a random tag so two daemons, or two processes of one daemon, never match each other's runs. +- **Only ids reach the runner's shell** - a model id or session id containing anything but letters, digits, dots, underscores, colons and hyphens is refused before dispatch; the prompt is a workflow input and never goes through a shell. +- **Waiting for the run** - the workflow's recent runs are polled every 5 seconds for one whose display name contains the correlation id; the run's link is reported once as an action; a run that concludes with anything but success fails the turn, naming the run; the wait gives up after 1 hour; a stop request [10] ends the wait, not the run. +- **Reading the transcript back** - the run's artifact named after the correlation id is downloaded as a zip and read for `execution.json` and `meta.json`; a run without an artifact or without a transcript fails the turn. +- **Replaying the transcript** - the transcript is a JSON array of the messages Claude Code streams one per line, read by the Claude Code parser, so the progress events replay in a burst at the end and the last message is the turn's answer; a transcript that is not a JSON array fails the turn, and an empty one is an empty turn. +- **Continuity through the branch** - the driver names one run branch per driver session, `claude/`, that every run pushes to; the branch a run reports back is where the next turn is dispatched and where produced code is read. +- **Continuing the driver session** - the session id read off the last transcript, or the one the driver session was started with, is passed to the workflow when a turn asks to continue. +- **Model pass-through** - the model the caller names is passed as a workflow input; without one, the action's default runs. +- **Reading produced code off the branch** - the runner is gone, so a file is read from the pushed branch over GitHub's contents API; before any run has pushed one, the reader is told so. +- **No quota reading** - the driver reports no quota [13]: it belongs to the account whose token the repository holds, and the runner that could answer is torn down. +- **GitHub API failures** - any failed GitHub API call fails the turn with the call, the status and the start of GitHub's answer. +- **Ending the driver session** - nothing is freed; every run reaps itself on the runner. + +## Business logic + +### One turn is one workflow run + +#### Context + +See `## Context`. + +#### Business logic + +A turn [2] first reports a `start` progress event [4] carrying the prompt it sends. The driver session's [8] framing [9] and the turn's extra framing are joined as separate paragraphs and placed ahead of the prompt with a blank line between, as with Codex, because the prompt is passed to the action as a workflow input, where a multi-line prompt is safe, whereas a system prompt flag would have to survive shell quoting inside the workflow and is not worth an injection seam. The workflow is dispatched with the inputs `prompt` (framing plus prompt), `correlation_id` (see "The correlation id"), `branch` (the run branch, see "Continuity through the branch"), `model` when the caller named one, and `resume_session_id` when the turn asks to continue and a session id is known (see "Continuing the driver session"). It is dispatched on the branch the previous run pushed; the first turn, and any turn after a run that pushed nothing, is dispatched on the ref the driver [1] was configured with, or `main` when none was. Dispatching answers with no run id, which is why the correlation id [3] exists. Once dispatched, a `notice` progress event tells the user "Dispatched to /; waiting for the runner." + +### The correlation id + +#### Context + +**Problem**: dispatching a workflow answers with nothing, so the driver [1] cannot be told which run is its own. The daemon starts a fresh process per agent [5], and a counter alone would restart at one in each, so two agents would both look for a run named after the same first turn [2] and one could latch onto the other's run. + +#### Business logic + +Each driver session [8] gets an id of the form `actions--`: the counter climbs within one process and reads well in logs, and the tag is eight random characters that keep the id unique across processes. Each turn's [2] correlation id [3] is `-turn-` with `n` climbing from 1 within the driver session, so it is unique per turn. The workflow echoes it into the run's display name and into the artifact's name, and the driver [1] matches on it in both places. + +### Only ids reach the runner's shell + +#### Context + +**Problem**: the model id and the session id are composed into a command line by a shell on the runner. An id that is not an id is a bug or an attack, and the workflow's own care in composing its arguments from environment variables is only one half of the protection. + +#### Business logic + +Before dispatch, the model id and the session id to resume must consist only of letters, digits, dots, underscores, colons and hyphens. Anything else fails the turn [2] before anything reaches GitHub: "Refusing to pass an unsafe model to the workflow: ", or "Refusing to pass an unsafe resume session id to the workflow: ". The prompt is never checked this way, because the action takes it as an input verbatim and never through a shell. + +### Waiting for the run + +#### Context + +**User story**: the user sees, in the agent view, a link to the run as soon as GitHub has created it, and the agent [5] fails plainly when the run goes red rather than passing a broken run off as a result. + +#### Business logic + +After dispatch the driver [1] polls the repository's fifty most recent dispatched workflow runs, every 5 seconds unless configured otherwise, for a run whose display name contains the correlation id [3]. While GitHub is still creating the run, nothing is found and polling continues. The first time the run is found, one `action` progress event [4] reports "run ", and never again. Once the run has completed, its conclusion decides the turn [2]: `success` lets the turn go on to read the transcript; any other conclusion fails the turn with `GitHub Actions run concluded "": `, the URL being the only way to see why it went red. The wait gives up after 1 hour unless configured otherwise, failing the turn with "Timed out waiting for the GitHub Actions run ()." A stop request [10], the driver session's [8] or the turn's own, is checked before and after each pause and fails the turn with "Session aborted while waiting for the GitHub Actions run."; the run itself is not cancelled and keeps going on the runner. + +### Reading the transcript back + +#### Context + +**Problem**: the runner and its copy of the repository vanish when the job ends. The one channel out of a run that the driver [1] can read is the artifact the workflow uploads, always, even when the turn [2] failed on the runner, which is exactly when it is most worth reading. + +#### Business logic + +The driver [1] lists the run's artifacts and takes the one whose name contains the correlation id [3], or the first artifact when none matches. A run that uploaded no artifact fails the turn [2]: "Run uploaded no artifact; the workflow must upload the transcript as one." The artifact is downloaded as a zip and read with the reader in `actions-zip.ts`. The entry ending in `execution.json` is the transcript; without one the turn fails with "Artifact has no execution.json (entries: )". The entry ending in `meta.json` names the branch the run pushed: a missing entry, a malformed one, or an empty branch means the run pushed nothing, which costs the next turn's continuity and the reading of produced code, never the turn. + +### Replaying the transcript + +#### Context + +**Business logic story**: the transcript is a JSON array holding exactly the messages Claude Code streams one per line when it runs on this machine. The whole difference between a local turn [2] and a runner's turn is array versus lines, so the transcript is read by the same parser as a local turn (`claude-code.ts`). + +#### Business logic + +The transcript is parsed as JSON; a transcript that is not JSON fails the turn [2] with "Could not parse the run transcript as JSON: ", and one that is not a JSON array with "The run transcript is not a JSON array of messages.", so a transcript in a shape the driver [1] does not recognize never reads as an agent [5] that did nothing. Each message is fed to the Claude Code parser, and the progress events [4] it yields (the session id first, then text and tool names) replay in a burst at the end of the turn: this driver has no live stream, and the caller sees the same event stream either way. An empty array, which is what the workflow uploads when the action crashed before writing a transcript, is an empty turn with an empty final message rather than a failure. The parser's result is the turn's answer: the final message, the session id and the usage [12], reported once more as the `result` progress event. + +### Continuity through the branch + +#### Context + +**Problem**: every run is a fresh runner and a fresh copy of the repository, so the machine cannot carry state from one turn [2] to the next; only a branch on the remote can. The action leaves its own branch name empty for a dispatched run, so there is nothing to discover after the fact: the driver [1] has to name the branch itself. + +#### Business logic + +Each driver session [8] names one run branch, the configured prefix followed by the driver session id, `claude/` by default, and passes it as the `branch` input of every turn's [2] dispatch, so each run pushes its work to the same branch and a later run builds on the earlier one. The branch the run reports back in `meta.json` is remembered as the branch of the driver session: the next turn is dispatched on it, and produced code is read from it. Until a run has reported a branch, turns are dispatched on the configured ref or `main`. + +### Continuing the driver session + +#### Context + +**User story**: the user sends a live chat [7] message to a running agent [5], and it lands in the same Claude Code conversation on the next run; the user revives a finished agent, and its opening prompt continues the conversation it had. + +#### Business logic + +The driver session [8] keeps the session id read off the last turn's [2] transcript; a driver session started with the session id of an earlier driver session begins with that one. A turn that asks to continue the previous turn passes the known session id to the workflow as `resume_session_id`, after the id check in "Only ids reach the runner's shell", and the workflow resumes Claude Code with it. A turn that asks to continue when no session id is known, and a turn that does not ask, are dispatched without one and run fresh. Whether the runner can actually resume the conversation is the workflow's and Claude Code's business; the driver [1] passes the id and reads what comes back. + +### Model pass-through + +#### Context + +**User story**: the user picks a model for an agent [5] in the launcher. + +#### Business logic + +When the caller names a model, it is passed as the workflow's `model` input, after the id check in "Only ids reach the runner's shell"; the driver [1] neither validates it further nor substitutes it. Without one, the input is left out and the action's default model runs. + +### Reading produced code off the branch + +#### Context + +**Business logic story**: the seam is the code, so a caller verifies a turn [2] by reading what the coding agent [11] left behind. The runner is gone by then, so the code is read from the branch the run pushed, over GitHub's contents API, rather than from disk. + +#### Business logic + +A file is read by its path relative to the repository root, from the branch the last run reported, and decoded as UTF-8 text from GitHub's answer. Before any run of the driver session [8] has reported a branch, reading fails with "No branch yet: readCode is only available after a run has pushed one." A path that is not a file on that branch fails with " is not a file on ". + +### No quota reading + +#### Context + +**Business logic story**: the quota [13] every run draws down is the subscription of whichever account's OAuth token the repository holds, not the runner's minutes; free runner minutes on a public repository change nothing about it. The runner that could report that quota is torn down before the driver [1] ever reads it. + +#### Business logic + +The driver [1] offers no quota [13] reading at all rather than a made-up number. + +### GitHub API failures + +#### Context + +**Problem**: dispatching, polling, listing and downloading artifacts and reading files are all GitHub API calls, each of which can fail on a bad token, a missing workflow, a missing permission or an outage. The user must see which call failed and what GitHub said. + +#### Business logic + +Every call carries the driver's [1] GitHub token as a bearer token. A call GitHub does not answer successfully fails the turn [2], or the code read, with "GitHub API failed ( ): ", or "" when the answer cannot be read, so the real error is never replaced by a failure to read its body. + +### Ending the driver session + +#### Context + +See `## Context`. + +#### Business logic + +Ending a driver session [8] frees nothing: every run reaps itself on the runner, and the driver [1] holds no process. Ending twice is safe. diff --git a/packages/agent-driver/src/actions.test.LOGIC.md b/packages/agent-driver/src/actions.test.LOGIC.md new file mode 100644 index 000000000..be18a4bb3 --- /dev/null +++ b/packages/agent-driver/src/actions.test.LOGIC.md @@ -0,0 +1,15 @@ +What the tests cover, against a stand-in for GitHub's API and a real action transcript: + +- **A turn is a workflow run** - a turn dispatches the workflow, keeps polling while the run is queued and in progress rather than reading it early, and answers with the transcript's final message, session id and usage, price included. +- **The dispatch** - the workflow named in the configuration is dispatched on `main` with the driver session's framing, then the turn's extra framing, then the prompt as one blank-line separated input, and with a correlation id of `-turn-1`. +- **Unique correlation ids** - two driver sessions started in separate driver processes get different ids, so a fresh process never matches a stale run of the same name. +- **The run branch** - every turn asks the run to push to `claude/`, the same branch on every turn of the driver session. +- **Continuity through the branch** - the first turn is dispatched on `main` and the next on the branch the previous run reported back. +- **Continuing the driver session** - a turn that asks to continue passes the session id read off the previous transcript; a turn that does not passes none. +- **Model pass-through** - the model the caller names reaches the dispatch as the `model` input; a model id that could break out of the runner's shell is refused before anything is dispatched. +- **A red run** - a run that concludes with a failure fails the turn, naming the run's URL. +- **Giving up** - a run that never finishes fails the turn on the timeout instead of being polled forever. +- **Reading produced code** - a file is read from the branch the run pushed, not the default branch; before any run has pushed a branch, the reader is told so plainly. +- **Replaying the run** - the turn opens with a `start` progress event, replays the transcript's text and tool names, reports the run's link as an action and closes with a `result` progress event. +- **No quota reading** - the driver offers no quota reading. +- **The transcript** - the transcript is read by the Claude Code parser, announcing the session id before the conversation replays; an empty array is an empty turn; a transcript that is not a JSON array, or not JSON at all, is rejected rather than read as an agent that did nothing. diff --git a/packages/agent-driver/src/child-registry.LOGIC.md b/packages/agent-driver/src/child-registry.LOGIC.md new file mode 100644 index 000000000..f709c0f52 --- /dev/null +++ b/packages/agent-driver/src/child-registry.LOGIC.md @@ -0,0 +1,53 @@ +Keeps the coding agent's [1] whole process tree reapable: every coding agent process a driver [2] spawns is the leader of its own process group and is registered here while it lives, so a stop reaches the whole tree with one signal, and a hard exit of The Framework's own process still kills every tree on the way out. + +## Context + +**User story**: the user stops an agent [3], or The Framework's process dies from a crash, and no stray coding agent [1] processes are left burning CPU on the machine. + +**Problem**: a coding agent spawns a deep subtree of its own: worker processes, search tools, the shell commands it runs, MCP servers. Signaling only the top process orphans that subtree, which keeps running on its own after the agent is gone. + +## Glossary + +[1] coding agent: the CLI doing the actual work: Claude Code or Codex. +[2] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. +[3] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[4] stop request: the caller's signal that a driver session, or one turn of it, must end now; the product raises one when the user stops the agent. +[5] driver session: the coding agent's own conversation for one agent, which the driver can resume by its session id. + +## Business logic — TL;DR + +- **One signal for the whole tree** - a process group is signaled as a whole through its leader, so the coding agent [1] and everything it spawned go down together; a group that has already exited is not an error. +- **Every live leader is registered** - a driver [2] registers each process-group leader it spawns and removes it once the process is gone. +- **A hard exit reaps everything** - when The Framework's process exits for any reason other than a signal, every registered group is force-killed on the way out; a death by signal is the caller's to handle by raising the stop request [4] of each driver session [5] first. + +## Business logic + +### One signal for the whole tree + +#### Context + +See `## Context`. + +#### Business logic + +A coding agent [1] process is spawned as the leader of its own process group. Signaling the group, rather than the leader alone, reaches the coding agent and every process it spawned in one shot. Signaling a group that has already exited, or a process that never led one, is silently ignored: it is not an error. + +### Every live leader is registered + +#### Context + +See `## Context`. + +#### Business logic + +A driver [2] registers the process-group leader it spawns for a turn as soon as it has a process id, and removes it once the process has exited or been killed. The registry holds only live groups, so a reap on exit never signals a process id that may since have been reused by something else. + +### A hard exit reaps everything + +#### Context + +**Problem**: The Framework's process may end without any driver [2] getting the chance to end its turn: a crash, an uncaught error, a deliberate exit. Whatever coding agent [1] trees are alive at that moment must not survive it. + +#### Business logic + +When The Framework's process exits, whether normally, deliberately, or after an uncaught error, every registered process group is force-killed on the way out. Only a forced kill is possible at that point, since nothing may wait during an exit. A death by signal, such as the user's Ctrl-C or a termination request from the system, does not pass through this net: it is the caller's to handle, by raising the stop request [4] of each driver session [5] first, which ends each turn gracefully (`cli-session.ts`). The net is installed the first time a process is registered and is the last resort for every other exit path. diff --git a/packages/agent-driver/src/claude-code-quota.LOGIC.md b/packages/agent-driver/src/claude-code-quota.LOGIC.md new file mode 100644 index 000000000..69400ab82 --- /dev/null +++ b/packages/agent-driver/src/claude-code-quota.LOGIC.md @@ -0,0 +1,87 @@ +Reads where the account's quota [1] stands by asking Claude Code for its own usage readout and parsing the prose it prints: one window per line, each with its label, its kind, the percentage used and, when printed, when it resets. Claude Code answers locally with its own credentials, so the reading costs no model turn and The Framework never handles the user's token. A reading that yields no window is reported as unavailable with a reason that says whether this attempt failed or the setup has no quota to report, never as an empty list that would read as nothing used. + +## Context + +**User story**: the dashboard shows the account's quota [1] as bars, one per window, and unattended work stands down past the quota boundary [2]; a wrong reading of "nothing used" would let unattended agents [3] run the account dry. + +**Business logic story**: Claude Code prints its usage as prose for a person, not as data, so this is a text parse and a reworded readout is a real failure mode. Every empty reading therefore carries a reason, and the daemon's rules for keeping or dropping an earlier reading rest on that reason (`packages/framework/src/quota-poller.ts`). The Claude Code driver [4] reads the quota through this reader (`claude-code.ts`). + +## Glossary + +[1] quota: the account's subscription allowance, as the coding agent reports it: a session window and a quota week, each with a percentage used. +[2] quota boundary: the share of the quota week that may be spent by now, rising with the clock; unattended work stands down past it, work a human asked for never does. +[3] agent: the unit of work: one task worked by a coding agent under The Framework's control — in its own checkout, on its own branch, streaming events, handed off when it ends. +[4] driver: a coding agent wrapped as a black box: start it in a directory, prompt it for one turn, stream what it does, resume it later. +[5] coding agent: the CLI doing the actual work: Claude Code or Codex. +[6] stop request: the caller's signal that a driver session, or one turn of it, must end now; the product raises one when the user stops the agent. + +## Business logic — TL;DR + +- **Asking Claude Code for its readout** - the `claude` command runs its own usage command in print mode with JSON output, never in the mode that pins it to API-key login; the answer costs nothing and the caller never sees a credential. +- **Unwrapping the answer** - Claude Code's JSON envelope is opened for the readout text; an envelope flagged as an error is a failed fetch, and anything not shaped like the envelope is unrecognized. +- **Reading the windows** - each line of the form "