Skip to content

Commit cdea1c9

Browse files
docs: expand README with v0.1.0 contents, status diagram and contract change process
1 parent 20fbca9 commit cdea1c9

1 file changed

Lines changed: 191 additions & 15 deletions

File tree

‎README.md‎

Lines changed: 191 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,207 @@
11
# codehands-contracts
22

3-
The shapes every Code Hands AI repo agrees on: session rows, session states, runner events and the `Runner` interface.
3+
The shapes every Code Hands AI repo agrees on: session rows, session statuses and the legal moves between them, runner events, and the `Runner` interface.
44

5-
| File | What | Consumed by |
5+
This package holds types plus a few small constants and helpers. There is no runtime logic here. It exists so the parts can be built in separate repos by separate people and still fit together on Friday.
6+
7+
**Owners:** Midhat and Tahira. Every repo that uses a contract approves changes to it.
8+
**Status:** v0.1.0, proposed Mon Oct 19 and frozen Fri Oct 23, 2026.
9+
**Part of:** [Code Hands AI](https://github.com/devweekends/codehands-docs). Read the design in that README first.
10+
11+
## Why it exists
12+
13+
Code Hands AI is split into one repo per part ([decision 0001](https://github.com/devweekends/codehands-docs/blob/main/decisions/0001-separate-repos.md)). Separate repos only work if every shape two repos share is written down in one place, typed and versioned. That place is this repo.
14+
15+
```mermaid
16+
flowchart LR
17+
contracts["codehands-contracts"]
18+
desktop["codehands-desktop (Tania)"]
19+
daemon["daemon (Midhat)"]
20+
agent["codehands-agent: runner (Tahira)"]
21+
backend["codehands-backend: state API and sync (Memoona)"]
22+
contracts -. types .-> desktop
23+
contracts -. types .-> daemon
24+
contracts -. types .-> agent
25+
contracts -. types .-> backend
26+
daemon -- "C2: runner.start(...)" --> agent
27+
agent -- "C2: RunnerEvents" --> daemon
28+
```
29+
30+
## What is in v0.1.0
31+
32+
| File | Exports | Used by |
33+
| --- | --- | --- |
34+
| [src/session.ts](src/session.ts) | `SessionStatus`, `AgentKind`, `CommandKind`, `Device`, `Project`, `Session`, `Command`, `SESSION_TRANSITIONS`, `canTransition()` | Everyone |
35+
| [src/runner.ts](src/runner.ts) | `RUNNER_EVENT_TYPES`, `RunnerEventType`, `RunnerEvent`, `StartOptions`, `Runner` | The daemon and the runner |
36+
| [src/index.ts](src/index.ts) | Re-exports both files | |
37+
38+
### Session statuses
39+
40+
A session is one task on one repo. It has exactly seven statuses, and only the moves below are legal. `canTransition(from, to)` is the single check every repo uses, so the runner, the daemon and the apps can never disagree about what may happen next.
41+
42+
```mermaid
43+
stateDiagram-v2
44+
[*] --> queued
45+
queued --> running
46+
queued --> failed
47+
running --> stopped
48+
running --> awaiting_approval
49+
running --> failed
50+
stopped --> running: resume
51+
stopped --> failed
52+
awaiting_approval --> approved
53+
awaiting_approval --> rejected
54+
awaiting_approval --> failed
55+
approved --> [*]
56+
rejected --> [*]
57+
failed --> [*]
58+
```
59+
60+
| Status | Meaning |
61+
| --- | --- |
62+
| `queued` | Created, not started yet |
63+
| `running` | The agent is working in its own worktree |
64+
| `stopped` | Stopped by a person. The worktree is kept so it can resume |
65+
| `awaiting_approval` | The agent finished. A change is waiting for a person |
66+
| `approved` | The change was committed on the task's branch. Final |
67+
| `rejected` | The change was thrown away, worktree and branch deleted. Final |
68+
| `failed` | Something went wrong. The reason is on the `task.failed` event. Final |
69+
70+
```ts
71+
import { canTransition } from "@codehands/contracts";
72+
73+
canTransition("running", "awaiting_approval"); // true
74+
canTransition("running", "approved"); // false: approve only after the agent finished
75+
```
76+
77+
### Runner events
78+
79+
The runner turns whatever the agent CLI prints into a stream of `RunnerEvent`s. The daemon stores them and the apps draw them.
80+
81+
```ts
82+
interface RunnerEvent {
83+
sessionId: string;
84+
seq: number; // 1, 2, 3 … per session, never reused, continues after a resume
85+
type: RunnerEventType;
86+
at: string; // ISO time
87+
payload: Record<string, unknown>;
88+
raw?: unknown; // the agent's original line, untouched
89+
}
90+
```
91+
92+
`seq` is what makes a resend harmless: a reader that already has event 14 ignores a second event 14, and a reader that reconnects asks for everything after the last `seq` it saw.
93+
94+
| Type | When | Typical payload |
695
| --- | --- | --- |
7-
| `src/session.ts` | `Session`, `Command`, `SessionStatus`, the legal state moves | api, cli, desktop, web, runner |
8-
| `src/runner.ts` | `RunnerEvent`, event names, `StartOptions`, `Runner` | cli (daemon), runner |
96+
| `task.started` | The agent process is up | `agent`, `model`, `cwd`, `agentSessionId` |
97+
| `message` | The agent said something, or a tool was blocked | `text`, `denied?`, `tool?` |
98+
| `file.read` | The agent read or searched files | `path`, `pattern?`, `tool` |
99+
| `file.changed` | The agent edited or wrote a file | `path`, `tool` |
100+
| `command.started` | The agent ran a shell command | `command`, `tool` |
101+
| `command.finished` | That command ended | `command`, `isError`, `denied`, `output` |
102+
| `test.result` | The command was a test run | `command`, `passed` |
103+
| `task.finished` | The agent finished. Now awaiting approval | `total_cost_usd`, `usage`, `num_turns`, `result`, `diffStat` |
104+
| `task.failed` | The run cannot continue | `reason`, plus details |
105+
| `task.stopped` | A person stopped it | |
106+
107+
Payloads are open on purpose in v0.1.0. The fields above are what the runner sends today; read them defensively.
108+
109+
### The `Runner` interface
110+
111+
This is the boundary between the daemon and the runner (contract C2). The daemon calls these methods; the runner owns the worktree and the agent process.
112+
113+
```ts
114+
interface Runner {
115+
start(o: StartOptions): AsyncIterable<RunnerEvent>;
116+
stop(sessionId: string): Promise<void>;
117+
resume(sessionId: string, prompt?: string): AsyncIterable<RunnerEvent>;
118+
diff(sessionId: string): Promise<{ patch: string; stat: string }>;
119+
approve(sessionId: string, message: string): Promise<{ branch: string; commit: string }>;
120+
reject(sessionId: string): Promise<void>;
121+
}
9122

10-
**Status:** proposed Mon Oct 19, frozen Fri Oct 23, 2026.
123+
interface StartOptions {
124+
sessionId: string; // a UUID, also reused as Claude Code's --session-id
125+
repoPath: string;
126+
prompt: string;
127+
agent: "claude-code" | "codex";
128+
model?: string;
129+
budgetUsd?: number;
130+
}
131+
```
132+
133+
`start` and `resume` return an async iterable, so the caller just loops:
134+
135+
```ts
136+
for await (const event of runner.start(options)) {
137+
saveEvent(event);
138+
}
139+
```
140+
141+
## Contracts still to write
142+
143+
The design in [codehands-docs](https://github.com/devweekends/codehands-docs#contracts) names four contracts. v0.1.0 covers C5 and the first half of C2. The rest arrive as minor versions.
144+
145+
| ID | Contract | In v0.1.0 | Still to add | Owner |
146+
| --- | --- | --- | --- | --- |
147+
| L1 | Local DB: the tables in `~/.codehands/state.db` and which process may write each | No | Table types for `tasks`, `commands`, `sessions`, `events`, `provider_status`, and the writer of each | Midhat |
148+
| C2 | Runner | `Runner`, `StartOptions`, events | Model per phase, preferences and memory in `StartOptions`; a `provider.status` event | Tahira |
149+
| C3 | Cloud API | No | Sign up, log in, log out, device tokens; `push(changes)`, `pull(cursor)`; record versions | Memoona |
150+
| C5 | Sessions | Yes | `Project` keyed by repo identity (a hash of the origin URL) instead of a folder path | Midhat, Tahira |
151+
152+
The provider failure states the C2 event will carry are fixed already: `rate_limited`, `auth_expired`, `usage_limit`, `subscription_inactive`, `down`. Their meaning is in the [docs README](https://github.com/devweekends/codehands-docs#provider-failures).
11153

12154
## Using it from another repo
13155

156+
While two repos are being built against each other, clone them side by side and point at the folder:
157+
158+
```text
159+
CodeHandsAI/
160+
codehands-contracts/
161+
codehands-desktop/ "@codehands/contracts": "file:../codehands-contracts"
162+
codehands-agent/
163+
runner/ "@codehands/contracts": "file:../../codehands-contracts"
164+
```
165+
166+
Build this repo once, because consumers import from `dist/`:
167+
168+
```bash
169+
cd codehands-contracts
170+
pnpm install
171+
pnpm build
172+
```
173+
174+
Once a version is tagged, pin the tag. Never depend on `main`:
175+
14176
```jsonc
15-
// while developing, with both repos cloned side by side
16-
"@codehands/contracts": "file:../codehands-contracts"
17-
// after it is pushed, pin a tag
18-
"@codehands/contracts": "github:imtahirabatool/codehands-contracts#v0.1.0"
177+
"@codehands/contracts": "github:devweekends/codehands-contracts#v0.1.0"
19178
```
20179

21-
## Changing it
180+
The `prepare` script runs `tsc`, so installing from a git tag builds `dist/` for you.
181+
182+
## Changing a contract
183+
184+
A change here can break four repos at once, so it follows a fixed path.
22185

23-
1. Open a PR here. Every consuming repo's owner approves it.
24-
2. Bump the version (minor for additions, major for renames or removals) and tag it.
25-
3. Update the matching stand-in (the runner's fake runner, the fake daemon) in the same week.
186+
1. **Open a PR here.** Say which repos are affected and how.
187+
2. **Every consuming owner approves it.** After the Oct 23 freeze, changes are batched into the Wednesday sync.
188+
3. **Bump the version and tag it.** Minor (`v0.2.0`) for additions, major for renames or removals.
189+
4. **Update the matching stand-in the same week:** `createFakeRunner()` in codehands-agent for C2, the fake writer for L1, the mock server for C3.
190+
5. **Consumers bump their pinned tag** in their own PR, when they are ready.
191+
192+
Prefer adding a field to renaming one. Optional fields let consumers move at their own pace.
193+
194+
## Development
195+
196+
Node 22 and pnpm 10.
26197

27198
```bash
28199
pnpm install
29-
pnpm build
30-
pnpm test
200+
pnpm build # tsc into dist/
201+
pnpm typecheck # tsc --noEmit
202+
pnpm test # vitest: legal moves and event names
31203
```
204+
205+
CI runs `pnpm build` and `pnpm test` on every push and PR ([.github/workflows/ci.yml](.github/workflows/ci.yml)). `main` stays green.
206+
207+
What belongs here: types, string unions, constant lists, and pure helpers like `canTransition`. What does not: anything that does I/O, imports a runtime dependency, or only one repo uses.

0 commit comments

Comments
 (0)