Skip to content

Commit f0e989f

Browse files
committed
Install SDD: next slice of per-file specs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSM4FVUbnP4PJJ9sawMfLb
1 parent 10114d1 commit f0e989f

36 files changed

Lines changed: 848 additions & 0 deletions
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
The GitHub Actions half of the `actions` run target: the workflow the driver dispatches to execute one agent turn on a disposable GitHub Actions runner. A workflow run receives the prompt, runs Claude Code on a fresh checkout of the repository, pushes whatever the turn produced to the run branch the driver named, and uploads the turn's transcript as an artifact; the driver polls for the finished workflow run and reads the turn's outcome from that artifact. The workflow only ever starts by explicit dispatch — never on pushes or pull requests — and the driver dispatches it by file name in the project's own repository, so a repository using the `actions` run target carries a copy of this workflow (this copy serves The Framework's own repository).
2+
3+
## User story
4+
5+
The user picks the `actions` run target so an agent executes on a GitHub Actions runner instead of occupying their device. The agent still spends the user's own Claude subscription, the dashboard still replays the turn's events, and the turn's work comes back as a branch on the repository that the next turn continues on.
6+
7+
## Glossary
8+
9+
- **correlation id** — the unique id the driver generates for each turn and passes as the `correlation_id` input. The workflow echoes it into the workflow run's display name and into the artifact name; matching on it is the only way the driver can find the workflow run it started.
10+
- **run branch** — the branch a workflow run pushes the turn's work to, named by the driver in the `branch` input. The driver keeps the name stable across an agent's turns, so consecutive workflow runs chain their work on it.
11+
12+
## Business logic — TL;DR
13+
14+
- **One workflow run is one agent turn** - each dispatch carries one prompt; the runner checks out the repository with full history and lets Claude Code run one full turn, capped at 60 minutes.
15+
- **The correlation id is how the driver finds its workflow run** - dispatching returns no run id, so the workflow interpolates the driver's correlation id into the run name and the artifact name, and the driver matches on it.
16+
- **The prompt never crosses a shell** - the prompt reaches the coding agent verbatim as an action input, and the optional model and resume ids are assembled through environment variables, so no dispatch input can become a command on the runner.
17+
- **The turn spends the subscription, never an API key** - the run authenticates with a `claude setup-token` OAuth token held as a repository secret.
18+
- **The work survives only as the run branch** - the runner vanishes when the job ends, so the workflow commits anything the agent left uncommitted and pushes to the run branch; a no-op turn pushes nothing.
19+
- **The artifact is the only channel out** - the turn's transcript, the branch actually pushed, and the session id are uploaded even when the turn failed, kept for 7 days.
20+
21+
## Business logic
22+
23+
### One workflow run is one agent turn
24+
25+
#### User story
26+
27+
See `## User story`.
28+
29+
#### Business logic
30+
31+
The workflow runs only when dispatched. The dispatch carries: the prompt (required), the correlation id (required), and optionally a model id (empty means the coding agent's default), a prior session id to resume instead of starting fresh, and the run branch to push work to. The runner checks out the repository with its full git history, so the agent can read the log to understand what it is changing, and the job is capped at 60 minutes — far under GitHub's 6-hour limit, because a turn running that long has gone wrong.
32+
33+
The driver chains turns into one continuing session: it dispatches each next turn onto the run branch the previous turn pushed and passes the previous turn's session id for resumption. The workflow itself is stateless; all continuity lives in the run branch and the resumed session.
34+
35+
### The correlation id is how the driver finds its workflow run
36+
37+
#### User story
38+
39+
The dashboard shows the turn's events and outcome, so the driver must locate and read exactly the workflow run it started — among any other runs dispatched concurrently.
40+
41+
#### Business logic
42+
43+
Dispatching a workflow returns only an acknowledgement, never a run id. The driver therefore generates a correlation id unique to the turn; the workflow interpolates it into the workflow run's display name (`framework-agent <correlation id>`) and into the artifact name (`framework-run-<correlation id>`). The driver polls the repository's recently dispatched workflow runs for the name containing its correlation id, waits for it to complete, then downloads the artifact matching the same id. Both interpolations are load-bearing: dropping either leaves the driver unable to find its workflow run or its artifact.
44+
45+
### The prompt and arguments never cross a shell
46+
47+
#### User story
48+
49+
The prompt is arbitrary text — task descriptions, ticket bodies, live chat. Nothing in a dispatch may be able to execute commands on the runner.
50+
51+
#### Business logic
52+
53+
The prompt is handed to the coding agent verbatim as an action input; it never passes through a shell. The optional model id and resume session id are folded into the agent's command-line arguments via environment variables rather than templated into a shell script, so a crafted value cannot become a command; the driver additionally refuses to dispatch a model or resume id containing anything beyond plain identifier characters. The agent itself runs with every permission gate disabled: an unattended turn must be able to edit files and run commands, and by default the coding agent's non-interactive mode allows neither.
54+
55+
#### Rationale
56+
57+
Disabling the permission gates is safe precisely because the runner is disposable: it holds nothing but a throwaway checkout that is destroyed when the job ends. The same flag on a developer's machine would be reckless.
58+
59+
### The turn spends the subscription, never an API key
60+
61+
#### User story
62+
63+
The Framework never makes model calls of its own: every agent, wherever it runs, draws on the user's own Claude subscription.
64+
65+
#### Business logic
66+
67+
The repository holds a `claude setup-token` OAuth token as the secret `CLAUDE_CODE_OAUTH_TOKEN`; the workflow run authenticates the agent with it, so the turn draws down that account's subscription quota rather than metered API billing. The job must be allowed to mint an OIDC token: the action exchanges one to authenticate the OAuth token, and without that permission every workflow run fails before the agent starts. The job also holds write access to repository contents (to push the run branch) and to pull requests (the agent may open one).
68+
69+
### The work survives only as the run branch
70+
71+
#### User story
72+
73+
After the turn, the driver reads the agent's files off the repository and dispatches the next turn on top of them — but the runner and its checkout no longer exist.
74+
75+
#### Business logic
76+
77+
When the dispatch named a run branch, the workflow — even after a failed agent step — first commits anything the agent left uncommitted, so it is not lost with the runner, then pushes the result to the run branch. A turn that advanced nothing past the ref it checked out pushes nothing, so a no-op turn never creates an empty branch (when that base ref cannot be resolved, the workflow errs toward pushing). Pushing is the workflow's job, not the agent's — mirroring the local flow, where the framework pushes the agent branch and the agent only commits.
78+
79+
#### Rationale
80+
81+
The driver names the run branch in the dispatch, rather than discovering it afterwards, because the action reports no branch of its own for a dispatched run. The push authenticates through an explicitly tokenized URL because the agent step reconfigures git for itself and leaves the checkout's stored credentials unusable.
82+
83+
### The artifact is the only channel out
84+
85+
#### User story
86+
87+
The dashboard replays the whole turn — the agent's messages, its actions, and the final message carrying the framework's signals — and the driver needs the session id to resume the next turn.
88+
89+
#### Business logic
90+
91+
After the agent step, succeeded or failed, the workflow uploads one artifact, `framework-run-<correlation id>`, kept for 7 days. It holds the turn's full transcript (`execution.json`, the coding agent's complete message log — replaced by an empty log when the agent step crashed, so the driver reads an empty turn instead of failing on a missing file) and `meta.json` with the run branch actually pushed (empty when nothing was pushed) and the agent's session id. A finished workflow run exposes nothing else to the API, and the artifact is uploaded even for a failed turn — which is exactly when the transcript matters most.
92+
93+
#### Rationale
94+
95+
The files are staged in a directory whose name is not dot-prefixed: the artifact uploader silently drops files under hidden path segments, and a hidden staging directory would upload an artifact the driver finds empty.
96+
97+
## Before modifying/creating SPEC.md files
98+
99+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md

packages/chrome-extension/SPEC.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
The extension half of the Claude web bridge: a Chrome extension that carries the question a cloud session is parked on into the user's local dashboard, and types the pick confirmed there back into the session on claude.ai.
2+
3+
It exists because an agent with run target `web` is hands-off: the daemon hands the whole task to a cloud session on claude.ai and the agent ends at the hand-off. When that session later parks on a gate, nothing streams back — the question is stranded on a claude.ai page nobody may be looking at. The daemon cannot reach claude.ai itself, but the user's own browser is already signed in there, so the extension turns that browser into the bridge's far end, with the daemon's `/_bridge/*` endpoints as the near end.
4+
5+
Four parts: the content script (the page half — reads claude.ai, types answers), the service worker (the daemon half — holds the token, makes every daemon call, manages tabs), the options page (setup and connection proof), and an offline check harness that proves the reading and typing against synthetic pages without a browser.
6+
7+
## User story
8+
9+
- I started an agent on the `web` target and walked away. When its cloud session asks something, the question appears in my dashboard — I never have to keep claude.ai open, or even know the session exists.
10+
- I pick an option in the dashboard and confirm it; exactly that option is typed into the session and submitted. Until the extension collects it, I can withdraw the pick.
11+
- Nothing on any web page — claude.ai included — can learn the secret that talks to my daemon, and the extension never speaks for me beyond the pick I confirmed.
12+
13+
## Glossary
14+
15+
- **bridge token** — the shared secret the daemon demands on every bridge call; the user copies it from The Framework into the extension's options page, and it lives in extension storage.
16+
- **composer** — claude.ai's message input box, the place a delivered answer is typed.
17+
- **answer** — a confirmed pick on its way back: the daemon queues the picked option's label under a delivery id, and the extension types that label into the composer.
18+
19+
## Business logic — TL;DR
20+
21+
- **A stranded question's round trip** - the content script extracts the parked question from the page, the service worker reports it to the daemon, the dashboard shows it; the confirmed pick is queued, collected, typed into the composer, submitted, and the outcome acknowledged.
22+
- **Only what the session offered, only when confirmed** - the daemon queues nothing but a label of the parked question's own options, picks are confirmed in the dashboard and withdrawable until collected, and the extension otherwise only observes.
23+
- **Tabs nobody has to think about** - the daemon publishes which cloud sessions to watch; the extension keeps one pinned, inactive tab per session (opt-in), closes its own stale tabs, and never reopens one the user closed.
24+
- **The trust boundary** - the bridge token and all daemon traffic live in the service worker; the content script, which shares its tab with claude.ai, holds no secret and calls no daemon.
25+
- **Version lockstep** - every daemon call states the extension's version, and a daemon expecting another refuses it outright, naming both versions; the two halves must ship the same number.
26+
- **Where it runs and why each permission exists** - a content script on every claude.ai page and frame; host access to the localhost origins for the worker's CORS-free daemon calls; storage, tabs and alarms for the token, the tab bookkeeping, and polls that survive the worker's idle termination.
27+
28+
## Business logic
29+
30+
### A stranded question's round trip
31+
32+
#### User story
33+
34+
See `## User story`, first and second items.
35+
36+
#### Business logic
37+
38+
On every claude.ai session page, the content script watches the DOM and extracts the choice the session rendered per the await protocol — a JSON block with a title, options, and an optional recommended label — keyed by the cloud session id parsed from the page URL, which is what the daemon joins back to the agent's record. The service worker posts it to the daemon (`POST /_bridge/question`), deduplicating repeats. The dashboard shows the question; when the user confirms a pick, the daemon queues it as the answer. The worker polls `GET /_bridge/answer` on a fast beat, hands a queued answer to the content script in that session's tab, and the content script types the label into the composer and submits it; the worker reports the outcome (`POST /_bridge/answered`), and only a delivery the extension confirmed makes the daemon treat the question as resolved. Alongside questions, the content script mirrors the session's transcript to the daemon (`POST /_bridge/events`) and sends a self-report of what the injected script is and sees (`POST /_bridge/hello`), so the dashboard can show what the session did and diagnosis never needs a screenshot. A daemon with the bridge switched off answers no bridge route at all — turning it on is an explicit choice, since it is the one daemon surface meant to be reached from another origin.
39+
40+
### Only what the session offered, only when confirmed
41+
42+
#### User story
43+
44+
See `## User story`, second and third items.
45+
46+
#### Business logic
47+
48+
Three properties bound the write path. The daemon refuses to queue any answer whose label is not one of the parked question's own options, so the only text the bridge can ever put in a composer is one the session itself offered — never free text. A pick becomes an answer only when confirmed in the dashboard, and stays withdrawable until the extension collects it. And the extension acts only on delivery: everything else it does is read-only, and its one manual write control — a "Fill composer (does not send)" button on its in-page panel — fills without submitting, proving the write path exists without the extension ever speaking for the user.
49+
50+
### Tabs nobody has to think about
51+
52+
#### User story
53+
54+
See `## User story`, first item.
55+
56+
#### Business logic
57+
58+
The extension only sees pages it is injected into, so it cannot know an agent started. The daemon publishes which cloud sessions are worth watching, and the extension keeps one pinned, inactive background tab open per watched session — content scripts run in background tabs, so the bridge works while Chrome merely runs. Opening tabs is opt-in from the options page. A session whose tab the user closed is dismissed and never reopened; tabs the extension opened are closed once the daemon stops watching their session; tabs the user opened themselves are never touched.
59+
60+
### The trust boundary
61+
62+
#### User story
63+
64+
See `## User story`, third item.
65+
66+
#### Business logic
67+
68+
The daemon deliberately answers no CORS headers on the bridge — a wildcard would let any site the user visits post to their dashboard — so a fetch carrying a page's origin is refused, and only the extension's service worker, exempt from CORS through its host permissions, can reach the daemon. That forces the healthy shape: the bridge token lives in extension storage, is read only by the worker and the options page, and never enters a content script; the content script, which shares its tab with claude.ai, talks only to the worker. Nothing the extension stores is readable by any web page. What the extension can post is small and fixed — questions, transcript text, self-reports, delivery acknowledgements — never a path, command, or prompt.
69+
70+
### Version lockstep
71+
72+
#### User story
73+
74+
See `## User story`, first item — a bridge that half-works is worse than one that says it is broken.
75+
76+
#### Business logic
77+
78+
Every daemon call states the extension's version in the `x-tf-extension-version` header. A daemon expecting a different version refuses the call outright with an error naming both versions and the way out, because a version-skewed extension does not fail loudly — it half-works, which reads as dashboard bugs. The options page shows that refusal verbatim. The extension and the daemon must therefore ship the same version number.
79+
80+
### Where it runs and why each permission exists
81+
82+
#### User story
83+
84+
See `## User story`, third item.
85+
86+
#### Business logic
87+
88+
The content script is injected into every `https://claude.ai/*` page, child frames included, once the page settles — frames are covered so a question rendered inside one is still found and reported up to the top frame. Host permissions cover `http://localhost/*` and `http://127.0.0.1/*`: the daemon origins the service worker must fetch without CORS. Three extension permissions carry the rest: storage (the dashboard URL, bridge token and tab-opening preference, plus the dismissed-session and opened-tab bookkeeping — extension storage no web page can read), tabs (finding, opening, pinning and closing session tabs, and messaging their content scripts), and alarms (the recurring sweeps; an idle service worker is terminated and plain timers die with it). The options page is the setup surface: dashboard URL (default `http://localhost:4200`), the bridge token, the tab-opening toggle, and a connection test. Declaring host permissions does not grant them — Chrome can leave site access off, particularly for an unpacked extension — so the options page checks the grants and names any missing one.
89+
90+
## Before modifying/creating SPEC.md files
91+
92+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md

packages/the-framework.ai/SPEC.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
The public marketing website for The Framework, served at https://the-framework.ai. Its one job: pitch the product — "Babysit AI" struck through, replaced by "Autonomous AI", under the tagline "Make the important decisions, let AI do the rest." — and convert visitors into three exits: trying the `framework` npm package in their terminal, joining the Discord, and starring the GitHub repository. Throughout, the site leans on three trust badges: 100% Open Source, 100% Free, 100% Local.
2+
3+
Four pages tell the story:
4+
5+
- `/` — the landing page: the full pitch, from hero to call-to-action (see `pages/index/SPEC.md` for the narrative arc).
6+
- `/go-to-dashboard` — how to open the dashboard: it runs on the visitor's own machine, so the website hands them the terminal commands (run, install, or one-shot try).
7+
- `/press` — brand material for anyone writing about The Framework: logo, naming, banner, asset sources.
8+
- `/banner` — the 1200×630 social-share banner, existing solely to be screenshotted into the `banner.jpg` that every page's Open Graph tags point at.
9+
10+
The whole site is prerendered to static HTML at build time — no server, no backend. Every push to `main` that touches the site rebuilds it and deploys the static output to GitHub Pages under the `the-framework.ai` custom domain.
11+
12+
## Before modifying/creating SPEC.md files
13+
14+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md

0 commit comments

Comments
 (0)