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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions packages/agent-driver/SPEC.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,82 @@
The driver seam, as the npm package `agent-driver`: The Framework never calls a model of its own β€” it wraps a coding-agent CLI as a black box, and this package is that wrapper, published on its own because every product that drives a coding agent needs the same thing. A driver is started once per agent, bound to the agent's workspace; each prompt lets the wrapped CLI's own loop run one full turn on the user's own subscription, and The Framework gates on outcomes β€” the turn's final message, the code produced, the exit β€” never on the agent's individual tool calls.
The npm package `agent-driver`: a coding-agent CLI wrapped as a black box, for any product that drives coding agents without calling a model of its own. A driver is started once per agent, bound to the agent's workspace; each prompt lets the wrapped CLI's own loop run one full turn on the user's own subscription, and the caller gates on outcomes β€” the turn's final message, the code produced, the exit β€” never on the agent's individual tool calls.

## User story

- The user picks which coding-agent CLI does the work β€” Claude Code (`claude`) or Codex (`codex`) β€” and the run target: this device, a GitHub Actions runner, or a Claude Code cloud session.
- The user watches an agent live on the dashboard: its streamed text, which tools it reached for, its final message, what the turn spent.
- The user clicks Stop, or the daemon dies hard; no agent process keeps burning CPU afterwards.
- The user picks which coding-agent CLI does the work β€” Claude Code (`claude`) or Codex (`codex`) β€” and where it runs: this device, a GitHub Actions runner, or a Claude Code cloud session.
- The user watches an agent live in the caller's UI: its streamed text, which tools it reached for, its final message, what the turn spent.
- The user clicks Stop, or the caller's process dies hard; no agent process keeps burning CPU afterwards.
- Unattended work stands down at the quota boundary, which needs an honest reading of where the account's quota stands.
- The whole product can be demoed and tested offline, with no CLI installed and no model.

## Glossary

- **caller** β€” the product that embeds the package and drives agents through it.
- **turn** β€” one prompt to the wrapped CLI: the CLI's own loop runs to completion, and the turn resolves with the CLI's final message, plus the CLI's session id and the turn's usage when the CLI reports them.
- **framing** β€” the role text a driver delivers as the wrapped CLI's system prompt: fixed for the agent when the driver starts, optionally extended per turn.

## Business logic β€” TL;DR

- **A black box, gated on outcomes** - drive by prompting, verify by result; a crashed turn never passes as a finished one, and the wrapped CLI's internal loop stays untouched and swappable.
- **One seam, five implementations** - Claude Code locally, Codex locally, a GitHub Actions workflow run per turn, and the scripted fake driver ship in the package; The Framework's own hand-off to a Claude Code cloud session (the `web` run target) implements the same contract from outside it β€” the proof that the contract is enough for a third party.
- **Bring your own subscription** - every driver runs on the user's own account and auth; The Framework holds no model API key and never reads the user's credentials.
- **Progress is visible, never load-bearing** - drivers stream progress events for the dashboard; control flow never branches on them.
- **No stray processes** - each spawned CLI runs as its own process group, stopped as a whole tree and reaped even on a hard daemon exit.
- **One seam, four drivers in the box** - Claude Code locally, Codex locally, a GitHub Actions workflow run per turn, and the scripted fake driver ship in the package; the contract also fixes an id for a fifth implementation built outside it, a Claude Code cloud session β€” the proof that the contract is enough for a third party.
- **Bring your own subscription** - every driver runs on the user's own account and auth; the caller holds no model API key and never reads the user's credentials.
- **Progress is visible, never load-bearing** - drivers stream progress events for the caller's UI; control flow never branches on them.
- **No stray processes** - each spawned CLI runs as its own process group, stopped as a whole tree and reaped even on a hard exit of the calling process.

## Business logic

### A black box, gated on outcomes

#### User story

The user trusts The Framework to run agents unattended, so a turn that actually failed must never be treated as finished work.
The user trusts the caller to run agents unattended, so a turn that actually failed must never be treated as finished work.

#### Business logic

A driver does four things: start bound to a workspace, prompt for a turn, read a file the agent produced, and tear down. Everything else β€” which tools the CLI used, how it looped β€” stays inside the wrapped CLI. Verification happens on the outcome: the turn's final message, the code in the workspace (or on the branch a remote turn pushed), and the CLI's exit. A CLI that exits non-zero fails its turn even when it streamed text first.

#### Rationale

Gating on individual tool calls would couple The Framework to each CLI's internals and break the subscription-auth story. Keeping the seam at the code and the outcome is what lets a second coding-agent CLI slot in behind the same four moves.
Gating on individual tool calls would couple the caller to each CLI's internals and break the subscription-auth story. Keeping the seam at the code and the outcome is what lets a second coding-agent CLI slot in behind the same four moves.

### One seam, five implementations
### One seam, four drivers in the box

#### User story

The user picks the driver and the run target; everything above the seam behaves identically.
The user picks the CLI and where it runs; everything above the seam behaves identically.

#### Business logic

Claude Code local and Codex each spawn their CLI afresh per turn and share one process engine β€” spawn in an own process group, prompt over stdin, stream the output through the CLI's own dialect, gate on the exit β€” differing only in command line and output dialect. The `actions` implementation runs each turn as a GitHub Actions workflow run, with continuity carried by the branch the previous run pushed. The fake driver replays scripted turns in memory for tests and offline demo runs. The `web` run target's implementation lives in The Framework, not here: it hands the whole task to a cloud session on claude.ai β€” hands-off, exactly one hand-off for the agent's life β€” and needs The Framework's daemon and browser bridge to do it. Each implementation carries a stable implementation id (`claude-code`, `codex`, `claude-web`, `github-actions`, `fake`), the set fixed by the package; one driver has an implementation per place it can run, and the product maps the id back to the user's driver choice.
Claude Code local and Codex each spawn their CLI afresh per turn and share one process engine β€” spawn in an own process group, prompt over stdin, stream the output through the CLI's own dialect, gate on the exit β€” differing only in command line and output dialect. The `actions` implementation runs each turn as a GitHub Actions workflow run, with continuity carried by the branch the previous run pushed. The fake driver replays scripted turns in memory for tests and offline demo runs. Each implementation carries a stable implementation id, and the contract fixes the set (`claude-code`, `codex`, `claude-web`, `github-actions`, `fake`): one CLI has an implementation per place it can run, and the caller maps the id back to the user's choice. `claude-web` is reserved for a driver built outside the package β€” one that hands the whole task to a Claude Code cloud session on claude.ai, which needs a browser the package does not have.

### Bring your own subscription

#### User story

The user already pays for Claude or ChatGPT; The Framework must add no separate model bill and never handle their credentials.
The user already pays for Claude or ChatGPT; the caller must add no separate model bill and never handle their credentials.

#### Business logic

Claude Code runs on the user's Claude subscription and Codex on their ChatGPT subscription; the CLI authenticates itself, so The Framework never reads or holds a token. A workflow run authenticates with an OAuth token the repo holds, minted by the user's own `claude setup-token`. What a turn spent is reported as usage (tokens always, a price only when the CLI prices its turns); where the account's quota stands is a separate account-wide read that only the Claude Code driver can answer β€” a driver that cannot answer omits the ability rather than fake a number.
Claude Code runs on the user's Claude subscription and Codex on their ChatGPT subscription; the CLI authenticates itself, so the caller never reads or holds a token. A workflow run authenticates with an OAuth token the repo holds, minted by the user's own `claude setup-token`. What a turn spent is reported as usage (tokens always, a price only when the CLI prices its turns); where the account's quota stands is a separate account-wide read that only the Claude Code driver can answer β€” a driver that cannot answer omits the ability rather than fake a number.

### Progress is visible, never load-bearing

#### User story

The user follows an agent on the dashboard as it works.
The user follows an agent in the caller's UI as it works.

#### Business logic

While a turn runs, a driver streams progress events: the prompt started, the CLI announced its session id, a chunk of text, a tool was used (its name only, never the arguments), the turn settled, where the account stands against its rate limit, an error, or a notice about something the driver worked around. Every surface renders them; nothing gates on them, and a listener that throws never breaks the agent.
While a turn runs, a driver streams progress events: the prompt started, the CLI announced its session id, a chunk of text, a tool was used (its name only, never the arguments), the turn settled, where the account stands against its rate limit, an error, or a notice about something the driver worked around. The caller renders them; nothing gates on them, and a listener that throws never breaks the agent.

### No stray processes

#### User story

The user clicks Stop β€” or the daemon crashes β€” and expects a quiet machine.
The user clicks Stop β€” or the caller's process crashes β€” and expects a quiet machine.

#### Business logic

A wrapped CLI spawns a deep subtree (workers, shell tool calls, MCP servers); stopping only the top process orphans the rest, which keeps burning CPU. Every long-lived child therefore runs as its own process-group leader and is stopped as a whole group β€” politely first, forcibly after a grace window β€” and every live child is registered so that even a hard daemon exit reaps every agent's tree on the way out.
A wrapped CLI spawns a deep subtree (workers, shell tool calls, MCP servers); stopping only the top process orphans the rest, which keeps burning CPU. Every long-lived child therefore runs as its own process-group leader and is stopped as a whole group β€” politely first, forcibly after a grace window β€” and every live child is registered so that even a hard exit of the calling process reaps every agent's tree on the way out.

## Before modifying/creating SPEC.md files

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-driver/src/actions-zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { inflateRawSync } from 'node:zlib'

// A minimal zip reader, for one job: the GitHub artifact download API always returns a zip,
// even for a single file, and that is the only REST-readable channel out of an Actions run
// (#610). Node ships deflate but no zip, and the framework has no runtime dependencies worth
// (#610). Node ships deflate but no zip, and the package has no runtime dependencies worth
// adding for ~60 lines. Reading only, and only what upload-artifact writes: stored or
// deflated entries, no zip64, no encryption.

Expand Down
18 changes: 9 additions & 9 deletions packages/agent-driver/src/actions.SPEC.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
The driver for the `actions` run target: instead of running the coding-agent CLI on this device, it hands each turn to a GitHub Actions runner β€” dispatch the project's agent workflow, wait for the run to finish, then read back the transcript the run uploaded. From the rest of The Framework this looks exactly like any other driver; only the tempo differs.
The driver for the `actions` run target: instead of running the coding-agent CLI on this device, it hands each turn to a GitHub Actions runner β€” dispatch the project's agent workflow, wait for the run to finish, then read back the transcript the run uploaded. To the caller this looks exactly like any other driver; only the tempo differs.

## User story

The user wants an agent to work a task without tying up their own machine β€” the laptop can sleep, and the work happens on GitHub's runners against the repo's own subscription token.

## Business logic β€” TL;DR

- **One turn is one workflow run** - every prompt dispatches a fresh runner with a fresh checkout; nothing survives on the runner between turns.
- **One turn is one workflow run** - every prompt dispatches the workflow the caller names, on a fresh runner with a fresh checkout; nothing survives on the runner between turns.
- **The pushed branch is the continuity** - each run pushes to one branch that stays the same for the whole agent, and the next turn is dispatched from it, so later turns build on earlier work.
- **A correlation id finds the run** - GitHub's dispatch reports no run identifier, so a unique per-turn tag is echoed into the run's name and its uploaded artifact and matched on.
- **The transcript replays in a burst** - there is no live stream from a runner; the whole turn's events arrive at once when the run completes.
Expand All @@ -24,7 +24,7 @@ See `## User story`.

#### Business logic

Each prompt dispatches the project's agent workflow (`framework-agent.yml` unless the project names another) with the prompt text, then polls until that run completes. A run that concludes as anything other than success fails the turn and reports the run's URL. The wait gives up after an hour by default β€” GitHub's own job cap is six β€” and the user pressing Stop, at either the agent level or for the single turn, ends the wait immediately.
Each prompt dispatches the workflow the caller named when it configured the driver β€” one that echoes the correlation id into its run name and uploads the transcript β€” with the prompt text, then polls until that run completes. A run that concludes as anything other than success fails the turn and reports the run's URL. The wait gives up after an hour by default β€” GitHub's own job cap is six β€” and the user pressing Stop, at either the agent level or for the single turn, ends the wait immediately.

The system prompt framing is prepended to the prompt text rather than passed as a separate input.

Expand All @@ -40,11 +40,11 @@ See `## User story`.

#### Business logic

The agent's session picks one branch name up front β€” `claude/framework-<session id>` by default β€” and asks every run to push to it. The first turn runs on the project's default ref; once a run reports the branch it pushed, every later turn is dispatched from that branch, so the runner's fresh checkout already contains the previous turns' work. The agent's own session id is carried across turns as well, so a turn can resume the CLI's conversation rather than starting cold.
The agent's session picks one branch name up front β€” `claude/<session id>` unless the caller sets another prefix β€” and asks every run to push to it. The first turn runs on the project's default ref; once a run reports the branch it pushed, every later turn is dispatched from that branch, so the runner's fresh checkout already contains the previous turns' work. The agent's own session id is carried across turns as well, so a turn can resume the CLI's conversation rather than starting cold.

#### Rationale

The branch is named by The Framework and handed to the workflow rather than discovered afterwards, because a dispatched run reports no branch name of its own β€” there would be nothing to discover.
The branch is named by the driver and handed to the workflow rather than discovered afterwards, because a dispatched run reports no branch name of its own β€” there would be nothing to discover.

### A correlation id finds the run

Expand All @@ -54,13 +54,13 @@ See `## User story`.

#### Business logic

Dispatching a workflow returns no identifier for the run it creates. Each turn therefore carries a correlation id that the workflow writes into the run's display name and into the name of the artifact it uploads; polling matches recent dispatched runs on that id. The id mixes a random tag with a per-turn counter, so two agents β€” or the same agent after the daemon restarts β€” never match each other's runs.
Dispatching a workflow returns no identifier for the run it creates. Each turn therefore carries a correlation id that the workflow writes into the run's display name and into the name of the artifact it uploads; polling matches recent dispatched runs on that id. The id mixes a random tag with a per-turn counter, so two agents β€” or the same agent after the calling process restarts β€” never match each other's runs.

### The transcript replays in a burst

#### User story

The dashboard shows an agent's tool calls and messages as an event stream, the same way for every run target.
The caller's UI shows an agent's tool calls and messages as an event stream, the same way for every run target.

#### Business logic

Expand All @@ -72,7 +72,7 @@ An artifact with no transcript file fails the turn and says so, naming what the

#### User story

The framework reads files the agent produced β€” its plan, its notes, its config β€” as part of a turn's follow-up.
The caller reads files the agent produced β€” its plan, its notes, its config β€” as part of a turn's follow-up.

#### Business logic

Expand All @@ -92,7 +92,7 @@ The model name and the resumed session id are passed into the workflow and end u

#### User story

The dashboard shows how much of the account's quota week is left, so unattended work can stand down near the quota boundary.
The caller's UI shows how much of the account's quota week is left, so unattended work can stand down near the quota boundary.

#### Business logic

Expand Down
Loading
Loading