@matt82198/aesop 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +5 -2
- package/docs/INSTALL.md +104 -14
- package/docs/MICROKERNEL.md +452 -0
- package/docs/README.md +4 -0
- package/docs/THE-AESOP-HYPOTHESIS.md +140 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +1 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# Aesop as an AI Micro-Kernel: Two Swappable Seats
|
|
2
|
+
|
|
3
|
+
**Status**: HS-1 (unified two-seat config) + HS-2 (live orchestrator-seat swap)
|
|
4
|
+
shipped. This doc is the conceptual centerpiece for both — what the seam
|
|
5
|
+
actually is, what is proven about it today, and how to swap a seat's model
|
|
6
|
+
in about a minute.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What it is
|
|
11
|
+
|
|
12
|
+
Aesop's wave loop has exactly two places where a large language model makes
|
|
13
|
+
a decision:
|
|
14
|
+
|
|
15
|
+
1. **The worker seat** — does the work: writes files, runs commands, reports
|
|
16
|
+
a structured result. Interface: `AgentDriver`.
|
|
17
|
+
2. **The orchestrator seat** — judges the work: decides whether a
|
|
18
|
+
test-verified item is safe to ship. Interface: `OrchestratorBackend`
|
|
19
|
+
(called through `OrchestratorDriver.decide()`).
|
|
20
|
+
|
|
21
|
+
Both seats are **swappable parts**, not the engine itself. The engine —
|
|
22
|
+
`driver/wave_loop.py`'s `run_wave()` — never talks to a model directly; it
|
|
23
|
+
calls exactly two abstract interfaces and does not care what sits behind
|
|
24
|
+
either one. This is the micro-kernel idea applied to an agent harness: keep
|
|
25
|
+
the kernel (the loop, the state contract, the human-facing report) tiny and
|
|
26
|
+
stable, and make every model a replaceable driver plugged into a fixed seam.
|
|
27
|
+
|
|
28
|
+
**Identity lives in files, not memory.** Aesop is crash-only: there is no
|
|
29
|
+
in-process state that a restart loses. Recovery is by reading `STATE.md`,
|
|
30
|
+
`tracker.json`, the recovery journal, and the Report JSON off disk — the same
|
|
31
|
+
files a *different* model in either seat would read and write. That is what
|
|
32
|
+
makes a seat swap safe: the model is not the thing being recovered, the
|
|
33
|
+
files are.
|
|
34
|
+
|
|
35
|
+
**What is invariant across a seat swap** — the two things a human or a
|
|
36
|
+
downstream tool actually depends on:
|
|
37
|
+
|
|
38
|
+
- **The Report JSON** — the wave scheduler's output contract (`phase`,
|
|
39
|
+
`wave_id`, `items_selected`/`items_shipped`, `blocked`,
|
|
40
|
+
`orchestrator_gate`, …, documented in full in `driver/wave_scheduler.py`'s
|
|
41
|
+
module docstring). Swapping either seat adds *at most* two well-known
|
|
42
|
+
optional keys (`orchestrator_review`, per-item `final_catch`) and changes
|
|
43
|
+
no existing key's shape.
|
|
44
|
+
- **The state layer** — `STATE.md`, `tracker.json`, receipts, and the
|
|
45
|
+
recovery journal. Same file names, same key sets, regardless of which
|
|
46
|
+
model is deciding.
|
|
47
|
+
|
|
48
|
+
What is **not** invariant, and isn't meant to be: which model made a given
|
|
49
|
+
decision, and (for the orchestrator seat) whether a decision was even routed
|
|
50
|
+
through an API model at all versus made by the live harness. That is exactly
|
|
51
|
+
the part that's supposed to change when you swap a seat.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## The two seats
|
|
56
|
+
|
|
57
|
+
### Worker seat — `AgentDriver`
|
|
58
|
+
|
|
59
|
+
Selected by `seats.worker` in `aesop.config.json`. Concrete backends today:
|
|
60
|
+
|
|
61
|
+
| `backend` | What it is | Notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `claude` (default) | Claude Code CLI harness | No API key; two ops run as concrete Python, three are serviced by the harness itself |
|
|
64
|
+
| `codex` | OpenAI Chat Completions | Requires a `json_schema`-capable model (`gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`; `gpt-3.5-turbo` is rejected at construction unless you pass `allow_unverified_models=True`) |
|
|
65
|
+
| `openai-compatible` | Any OpenAI-compatible HTTP endpoint | Ollama, OpenRouter, Together, etc. — **requires `base_url`** |
|
|
66
|
+
|
|
67
|
+
**Live wiring**: `driver/wave_scheduler.py`'s `resolve_worker_driver()` reads
|
|
68
|
+
`seats.worker` from `aesop.config.json` and calls `build_driver()` on it
|
|
69
|
+
(`driver/backend_config.py`). `--driver claude|codex` on the CLI overrides
|
|
70
|
+
the config. No `seats` block, or a bare legacy flat `{"backend": ...}` block
|
|
71
|
+
with no `seats` wrapper, keeps behavior byte-identical to a pre-0.4.0
|
|
72
|
+
install: `ClaudeCodeDriver`, no key needed. (The legacy flat block still
|
|
73
|
+
*parses*, and direct `build_driver()` callers still honor it — it is only
|
|
74
|
+
*inert* in the scheduler's default dispatch path, so migrate it into
|
|
75
|
+
`seats.worker` to actually activate it.)
|
|
76
|
+
|
|
77
|
+
### Orchestrator seat — `OrchestratorBackend`
|
|
78
|
+
|
|
79
|
+
Selected by `seats.orchestrator`. This is the decision seat: the thing that
|
|
80
|
+
calls `OrchestratorDriver.decide()` to produce a structured verdict.
|
|
81
|
+
|
|
82
|
+
| `backend` | What it is |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `harness` (default, also accepts `"claude"`) | The **null** `HarnessOrchestratorBackend`. `decide_call()` raises on purpose — there is no Python code path that "calls" the harness. This is the honest way of saying: the live Claude Code session driving this loop IS the orchestrator seat, and no swapped backend exists. |
|
|
85
|
+
| `openai-compatible` | `OpenAICompatibleOrchestratorBackend` — a real OpenAI-compatible HTTP call, same `model`/`base_url`/`api_key_env`/`is_local` shape as the worker seat, plus `timeout_s`. `base_url` is optional here (defaults to the hosted OpenAI endpoint) — unlike the worker seat, which requires it. |
|
|
86
|
+
|
|
87
|
+
**Live wiring**: `driver/wave_loop.py`'s `run_wave(..., orchestrator_backend=...)`
|
|
88
|
+
Phase 6 — the pre-ship gate — routes **one `final_catch` decision per
|
|
89
|
+
test-verified item** through the configured backend when one is live;
|
|
90
|
+
`driver/wave_scheduler.py`'s `resolve_orchestrator_backend()` is what builds
|
|
91
|
+
that backend from `seats.orchestrator` and passes it through. With no
|
|
92
|
+
`seats.orchestrator` block (or `backend: "harness"`/`"claude"`),
|
|
93
|
+
`resolve_orchestrator_backend()` returns `None` and Phase 6 stays exactly
|
|
94
|
+
what it was pre-HS-2: `adversarial_review = "deferred"`, no
|
|
95
|
+
`orchestrator_review` key, no OpenAI backend constructed, no key required.
|
|
96
|
+
|
|
97
|
+
Be precise about scope here, because it's easy to overstate: Phase 6's
|
|
98
|
+
`final_catch` is the **only** decision point HS-2 wired to the seat. Backlog
|
|
99
|
+
ranking, in-session adjudication, and PR merges are still made by the live
|
|
100
|
+
harness directly — see [What's proven vs bounded](#whats-proven-vs-bounded).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The invariant boundary
|
|
105
|
+
|
|
106
|
+
The claim "the Report JSON and state layer are unchanged across a seat
|
|
107
|
+
swap" is not just asserted in this doc — it's a committed, offline,
|
|
108
|
+
automated proof: `tests/test_hs2_swap_proof.py`. It drives the **same task**
|
|
109
|
+
through the public scheduler path twice — once with the default harness
|
|
110
|
+
orchestrator seat, once with a swapped `FakeOrchestratorBackend` — both on a
|
|
111
|
+
non-Claude fake worker seat, and asserts:
|
|
112
|
+
|
|
113
|
+
- Identical Report JSON key sets, top-level and per-item.
|
|
114
|
+
- Identical values for `slug`/`backend`/`tier`/`verified`/`testExit`.
|
|
115
|
+
- Identical tracker terminal state (`in_progress`) and structure.
|
|
116
|
+
- Identical journal file names and entry key sets.
|
|
117
|
+
- The swapped backend demonstrably decided (`call_count == 1`).
|
|
118
|
+
|
|
119
|
+
A companion **bounded live run** — `bench/results/hs2-swap-proof-2026-07-25.md`
|
|
120
|
+
/ `.json` — drove one real task (fix a broken `multiply`) through `run_wave`
|
|
121
|
+
twice against a live codex (gpt-4o-mini) worker seat: arm A the default
|
|
122
|
+
harness orchestrator seat, arm B `seats.orchestrator` = openai-compatible
|
|
123
|
+
gpt-4o-mini. Both arms dispatched, test-exit 0, `verified: True`; arm B's
|
|
124
|
+
gpt-4o-mini seat returned a schema-valid `final_catch` verdict (`merge`,
|
|
125
|
+
with evidence + confidence) on the first attempt. Result shape was
|
|
126
|
+
invariant modulo exactly the two documented opt-in keys
|
|
127
|
+
(`orchestrator_review`, `final_catch`). Total spend: 3 gpt-4o-mini calls
|
|
128
|
+
(~1.1k worker tokens + one decision call), well under the run's US$2 cap.
|
|
129
|
+
`git=None` — the live proof never shipped anything.
|
|
130
|
+
|
|
131
|
+
That is the whole evidentiary basis for "the seam is real and the boundary
|
|
132
|
+
holds": one offline proof covering the general mechanism plus one small,
|
|
133
|
+
real, bounded live run. See [Bounds](#whats-proven-vs-bounded) for what that
|
|
134
|
+
does and does not establish.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## How to swap a seat
|
|
139
|
+
|
|
140
|
+
Everything lives in one namespaced block, `seats`, in `aesop.config.json`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"seats": {
|
|
145
|
+
"worker": { "backend": "claude" },
|
|
146
|
+
"orchestrator": { "backend": "harness" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
That's the *default* — writing it explicitly changes nothing, and deleting
|
|
152
|
+
it changes nothing either. To swap a seat, replace its block. See the
|
|
153
|
+
[quickstart](#swap-a-seats-model-in-60-seconds) below for copy-paste
|
|
154
|
+
examples.
|
|
155
|
+
|
|
156
|
+
Fields, common to both seats' `openai-compatible` backend:
|
|
157
|
+
|
|
158
|
+
| Field | Required? | Meaning |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `backend` | yes | `"claude"` / `"codex"` / `"openai-compatible"` (worker); `"harness"` / `"claude"` / `"openai-compatible"` (orchestrator) |
|
|
161
|
+
| `model` | required for `codex` and `openai-compatible` | Model id |
|
|
162
|
+
| `base_url` | **required** for worker `openai-compatible`; optional for orchestrator (defaults to `https://api.openai.com/v1`) | The HTTP endpoint |
|
|
163
|
+
| `api_key_env` | optional | Env var name holding the API key — read at **call time**, never stored in the config |
|
|
164
|
+
| `is_local` | optional | See below |
|
|
165
|
+
|
|
166
|
+
### SECURITY notes (read these before pointing a seat at a real endpoint)
|
|
167
|
+
|
|
168
|
+
- **`api_key_env` is a heuristic allowlist, not a guarantee.** Known
|
|
169
|
+
LLM-provider names (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
|
|
170
|
+
`OPENROUTER_API_KEY`, `TOGETHER_API_KEY`, `GROQ_API_KEY`,
|
|
171
|
+
`MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`, `FIREWORKS_API_KEY`,
|
|
172
|
+
`OLLAMA_API_KEY`, `AZURE_OPENAI_API_KEY`, `GOOGLE_API_KEY`) pass
|
|
173
|
+
**silently**. A name that doesn't look like a key env var, or that
|
|
174
|
+
contains an obvious non-LLM secret fragment (`SECRET`, `TOKEN`,
|
|
175
|
+
`PASSWORD`, `PASSWD`, `CREDENTIAL`, `PRIVATE`, `ACCESS`, `SESSION`,
|
|
176
|
+
`COOKIE`, `SIGNING`), is **hard-rejected**. Any other key-shaped name
|
|
177
|
+
(a custom LLM gateway) is **allowed but prints a loud `NOTICE` to
|
|
178
|
+
stderr** naming the risk: its value *will* be sent as a Bearer token
|
|
179
|
+
to your configured `base_url`. This is deliberately best-effort — no
|
|
180
|
+
name check can prove an env var actually holds an LLM key. The NOTICE
|
|
181
|
+
is the real signal; review it whenever a non-provider name appears.
|
|
182
|
+
- **`is_local: true` requires a loopback `base_url`** (`localhost`,
|
|
183
|
+
`127.0.0.1`, or `::1`) — construction rejects it otherwise. `is_local`
|
|
184
|
+
waives the API-key requirement (a dummy `local-only` Bearer is sent
|
|
185
|
+
instead), so it must be pinned to loopback: `is_local` plus a remote
|
|
186
|
+
`base_url` would ship your prompt content to an arbitrary host with no
|
|
187
|
+
key needed at all.
|
|
188
|
+
- **`base_url` is SSRF-validated** (`driver/backend_config.py`'s
|
|
189
|
+
`validate_base_url`): scheme must be `http`/`https`, no embedded
|
|
190
|
+
credentials, and both IP literals *and* DNS-resolved hostnames are
|
|
191
|
+
checked against private/loopback/link-local/reserved ranges (including
|
|
192
|
+
IPv4-mapped IPv6 forms like `::ffff:169.254.169.254`, which would
|
|
193
|
+
otherwise bypass the IPv4 checks, and the `169.254.169.254` cloud
|
|
194
|
+
metadata address). **Residual, documented, not closed**: this is a
|
|
195
|
+
load/construct-time check with a bounded (5s) DNS resolution. A
|
|
196
|
+
TTL-0 DNS-rebinding attacker can pass validation and then re-point the
|
|
197
|
+
name at a private address before the actual HTTP call; closing that
|
|
198
|
+
fully requires connection-time address pinning in the transport, which
|
|
199
|
+
this does not do. An unresolvable hostname is allowed through (offline
|
|
200
|
+
config loading must not fail) and the eventual connection just fails
|
|
201
|
+
on its own.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## What's PROVEN vs BOUNDED
|
|
206
|
+
|
|
207
|
+
Scrupulously, so nothing here gets over-read:
|
|
208
|
+
|
|
209
|
+
### PROVEN
|
|
210
|
+
|
|
211
|
+
- **Worker swap is live.** `resolve_worker_driver()` builds a real,
|
|
212
|
+
configured `AgentDriver` from `seats.worker` and the scheduler dispatches
|
|
213
|
+
through it; codex and openai-compatible backends have offline test
|
|
214
|
+
coverage (`tests/test_codex_driver_e2e.py`, `tests/test_seats_config.py`).
|
|
215
|
+
- **Orchestrator swap is live** (HS-2). `run_wave`'s Phase 6 routes a real
|
|
216
|
+
`final_catch` decision through a configured `OrchestratorBackend` when one
|
|
217
|
+
is present; verdict has real effect (`block` stops the ship and
|
|
218
|
+
quarantines files; `merge` approves).
|
|
219
|
+
- **Swap transparency is proven** two ways: an offline test suite
|
|
220
|
+
(`tests/test_hs2_swap_proof.py`) establishing the no-op invariant and the
|
|
221
|
+
end-to-end Report/state shape invariance, *and* one bounded live run
|
|
222
|
+
(both seats real: worker + orchestrator gpt-4o-mini) where both arms went
|
|
223
|
+
green with an invariant result shape. **Small N** — see below.
|
|
224
|
+
|
|
225
|
+
### BOUNDED / NOT yet claimed
|
|
226
|
+
|
|
227
|
+
- **The proofs are small-N.** The offline swap-transparency test is one
|
|
228
|
+
synthetic task on a fake backend; the live run is one real task, one
|
|
229
|
+
model (gpt-4o-mini), one repeat. This proves the *plumbing* — config to
|
|
230
|
+
seat to real API call to schema-valid verdict to recorded effect — not
|
|
231
|
+
decision **quality**, and not anything at scale.
|
|
232
|
+
- **Seat decision quality is a separate, ongoing question**, studied
|
|
233
|
+
in the shadow-adjudication bench line (`bench/README.md`,
|
|
234
|
+
`tools/seated_shadow_adjudication.py`). The headline finding there —
|
|
235
|
+
**"Context at the Seam"**: decontextualized adjudication (facts only, no
|
|
236
|
+
file brain) leaves both a frontier model (gpt-5.6-sol) and a cheaper
|
|
237
|
+
model (gpt-5.5) abstaining (`undetermined`, ~80–100% of runs) on the
|
|
238
|
+
hardest synthesis-heavy corpus item (item 9, a whitelist-gate-weakening
|
|
239
|
+
false positive that requires chaining "health check is top-level only" +
|
|
240
|
+
"secret_scan.py scans recursively" into "no real coverage gap"). Giving
|
|
241
|
+
the *same* models **real seated context** (actual STATE.md/tracker.json/
|
|
242
|
+
BUILDLOG.md plus the real cited code) flips both models to the correct
|
|
243
|
+
`false_positive` verdict, **stable across repeated runs and across two
|
|
244
|
+
independently-run corpus variants** (`bench/results/SEATED-AB-2026-07-24.md`,
|
|
245
|
+
`bench/results/seated-redo-2026-07-24-gpt-5_6-sol_repeat3.md`,
|
|
246
|
+
`bench/results/seated-redo-2026-07-24--neutral-seated-sol_repeat3.md`).
|
|
247
|
+
This result is **robust in the sense that matters for this doc**: it
|
|
248
|
+
reproduced after an earlier round of this same bench line caught and fixed
|
|
249
|
+
a context-leak/seam confound (a dropped-prompt / schema-mismatch bug where
|
|
250
|
+
"seated" runs weren't actually receiving the real context they claimed
|
|
251
|
+
to) — the cited results here are the **re-verified**, post-fix runs, not
|
|
252
|
+
the confounded ones. Bound: N=3 per model, one item, two OpenAI-family
|
|
253
|
+
models, not a full-corpus or cross-lab claim.
|
|
254
|
+
- **Some orchestrator decisions are not routed through the seat at all.**
|
|
255
|
+
Backlog ranking, PR merges, and in-session adjudication by the live
|
|
256
|
+
harness are made outside `run_wave`'s Phase 6 and are unaffected by HS-2.
|
|
257
|
+
The seat swap covers exactly one gate: the pre-ship `final_catch` check.
|
|
258
|
+
- **`wave_loop.py`'s standalone `--manifest` CLI still hardcodes
|
|
259
|
+
`ClaudeCodeDriver`** and does not read `aesop.config.json` at all. The
|
|
260
|
+
config-driven entry point is `driver/wave_scheduler.py` — that is where
|
|
261
|
+
`seats` actually takes effect. If you invoke `wave_loop.py` directly by
|
|
262
|
+
its manifest CLI, no seat swap applies.
|
|
263
|
+
- **Repair stays mechanical on both seats by design.** Swapping who
|
|
264
|
+
decides `final_catch` never changes the bounded-retry repair semantics.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## The block gate
|
|
269
|
+
|
|
270
|
+
`run_wave`'s Phase 6 (`_orchestrator_final_catch` in `driver/wave_loop.py`)
|
|
271
|
+
reviews every **test-verified** item — a failed item never reaches the
|
|
272
|
+
seat at all — and acts on the verdict:
|
|
273
|
+
|
|
274
|
+
| Verdict | Effect |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `merge` | Approved; ships exactly as it would with the default harness seat. |
|
|
277
|
+
| `block` | `verified` is flipped to `False`; the item does **not** ship; the recovery journal is rewritten so a resume can't skip-and-ship it; and the item is marked **terminal** (tracker status `blocked`, never re-selected) and visible in the Report (`Report.blocked: [{slug, reason, quarantine}]`). |
|
|
278
|
+
| `escalate` / `undetermined` / `DECISION_FAILED` | Degrades to today's default behavior: ships to branch (merge stays manual downstream) with an honest per-item record. **A seat outage never fabricates a verdict and never blocks a test-proven item** — this is crash-only degradation, not silent failure. |
|
|
279
|
+
|
|
280
|
+
**Quarantine on block**: a blocked item's already-written files are
|
|
281
|
+
restored to their pre-build state — `git checkout --` for tracked files,
|
|
282
|
+
delete for untracked ones — so refused code doesn't linger in the working
|
|
283
|
+
tree for a later `git add -A` to accidentally ship. This acts on **file
|
|
284
|
+
paths only**: empty strings, `.`/`..`, and directory entries are rejected
|
|
285
|
+
with a per-file error record rather than acted on, specifically because a
|
|
286
|
+
directory or dot pathspec would revert *other* items' uncommitted verified
|
|
287
|
+
work, not just the blocked item's. An ambiguous untracked/tracked
|
|
288
|
+
determination (e.g. a git index lock) never deletes — fail-safe, not
|
|
289
|
+
fail-delete.
|
|
290
|
+
|
|
291
|
+
**Gate visibility**: if every single decision on a wave came back
|
|
292
|
+
`DECISION_FAILED`, the wave-level `orchestrator_review.gate_status` is
|
|
293
|
+
`"degraded"` (not `"active"`) — a 100%-failing seat is not allowed to look
|
|
294
|
+
like an approving one, even though ship semantics for already-verified
|
|
295
|
+
items are unaffected.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Honest engineering note
|
|
300
|
+
|
|
301
|
+
This seam did not work correctly on the first attempt at wiring it. Early
|
|
302
|
+
rounds of the shadow-adjudication bench line ran through a shim that
|
|
303
|
+
dropped the prompt before it reached the model, and a schema/prompt
|
|
304
|
+
mismatch meant "seated" runs weren't actually seeing the context they
|
|
305
|
+
claimed to have — green test output, wrong conclusion (**green is not
|
|
306
|
+
correct**). Each seat — worker and orchestrator — cleared **two rounds of
|
|
307
|
+
adversarial audit** before the swap-transparency claims in this doc were
|
|
308
|
+
considered solid enough to write down. The context-leak confound described
|
|
309
|
+
above under [Bounds](#whats-proven-vs-bounded) is exactly this class of
|
|
310
|
+
bug, caught, fixed, and re-verified before being cited here.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Swap a seat's model in 60 seconds
|
|
315
|
+
|
|
316
|
+
Every config block below was verified offline against this repo's actual
|
|
317
|
+
loader — `load_backend_config()` → `build_driver()` /
|
|
318
|
+
`build_orchestrator_backend()` — with no network call and no API key
|
|
319
|
+
(construction is always offline-safe; keys are read at call time only).
|
|
320
|
+
Copy, paste, done.
|
|
321
|
+
|
|
322
|
+
### Worker seat
|
|
323
|
+
|
|
324
|
+
**Ollama, local, no key:**
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
ollama serve # in one terminal
|
|
328
|
+
ollama pull mistral # in another
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
```json
|
|
332
|
+
{
|
|
333
|
+
"seats": {
|
|
334
|
+
"worker": {
|
|
335
|
+
"backend": "openai-compatible",
|
|
336
|
+
"base_url": "http://localhost:11434/v1",
|
|
337
|
+
"model": "mistral",
|
|
338
|
+
"is_local": true
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
No env var needed — `is_local: true` sends a dummy `local-only` Bearer.
|
|
345
|
+
Verification tier: 3 (heaviest — small local models get the most checking).
|
|
346
|
+
|
|
347
|
+
**OpenRouter, hosted:**
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
export OPENROUTER_API_KEY=sk-or-...
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```json
|
|
354
|
+
{
|
|
355
|
+
"seats": {
|
|
356
|
+
"worker": {
|
|
357
|
+
"backend": "openai-compatible",
|
|
358
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
359
|
+
"model": "openai/gpt-4-turbo",
|
|
360
|
+
"api_key_env": "OPENROUTER_API_KEY"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Verification tier: 2 (hosted, ~0.92 accuracy assumption).
|
|
367
|
+
|
|
368
|
+
**Codex (OpenAI Chat Completions), hosted:**
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
export OPENAI_API_KEY=sk-...
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"seats": {
|
|
377
|
+
"worker": {
|
|
378
|
+
"backend": "codex",
|
|
379
|
+
"model": "gpt-4o-mini"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`model` must support `response_format: json_schema` — `gpt-4o`,
|
|
386
|
+
`gpt-4o-mini`, and `gpt-4-turbo` are the known-capable set; anything else
|
|
387
|
+
(including `gpt-3.5-turbo`) is rejected at construction unless you pass
|
|
388
|
+
`allow_unverified_models=True` yourself. Verification tier: 2.
|
|
389
|
+
|
|
390
|
+
### Orchestrator seat
|
|
391
|
+
|
|
392
|
+
**OpenAI hosted (`gpt-4o-mini`) as the decision seat:**
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
export OPENAI_API_KEY=sk-...
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"seats": {
|
|
401
|
+
"orchestrator": {
|
|
402
|
+
"backend": "openai-compatible",
|
|
403
|
+
"model": "gpt-4o-mini",
|
|
404
|
+
"api_key_env": "OPENAI_API_KEY"
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Note `base_url` is omitted — the orchestrator seat defaults to
|
|
411
|
+
`https://api.openai.com/v1` (the worker seat has no such default; it
|
|
412
|
+
requires `base_url` explicitly).
|
|
413
|
+
|
|
414
|
+
**Ollama, local, no key, as the decision seat:**
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
ollama serve
|
|
418
|
+
ollama pull mistral
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"seats": {
|
|
424
|
+
"orchestrator": {
|
|
425
|
+
"backend": "openai-compatible",
|
|
426
|
+
"base_url": "http://localhost:11434/v1",
|
|
427
|
+
"model": "mistral",
|
|
428
|
+
"is_local": true
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Combine any worker + any orchestrator block** under one `seats` key —
|
|
435
|
+
they're independent. Running `driver/wave_scheduler.py --execute` against a
|
|
436
|
+
hosted (non-`is_local`) seat requires that seat's `api_key_env` to be set;
|
|
437
|
+
`--dry-run` never needs a key, because building a driver or backend is
|
|
438
|
+
always offline-safe.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## See also
|
|
443
|
+
|
|
444
|
+
- [docs/INSTALL.md](INSTALL.md) — "Using Non-Claude Backends" section: setup
|
|
445
|
+
prerequisites, verification-tier table, troubleshooting.
|
|
446
|
+
- `driver/CLAUDE.md` — the full technical contract for both seats.
|
|
447
|
+
- `bench/README.md` — the held-out benchmark measuring *quality*, separate
|
|
448
|
+
from the plumbing this doc covers.
|
|
449
|
+
- `bench/results/hs2-swap-proof-2026-07-25.md` — the bounded live
|
|
450
|
+
swap-transparency run.
|
|
451
|
+
- `bench/results/SEATED-AB-2026-07-24.md` — the seated-context adjudication
|
|
452
|
+
finding.
|
package/docs/README.md
CHANGED
|
@@ -89,6 +89,7 @@ Once you've completed the adopter journey, use these for operational reference:
|
|
|
89
89
|
|
|
90
90
|
### Wave Cycle & Orchestration
|
|
91
91
|
- **[HOW-THE-LOOP-WORKS.md](HOW-THE-LOOP-WORKS.md)** — Concrete walkthrough of one complete `/buildsystem` wave cycle (rank → fan-out → verify → merge → close)
|
|
92
|
+
- **[MICROKERNEL.md](MICROKERNEL.md)** — The two swappable seats (worker + orchestrator): what's invariant across a model swap, what's proven vs. bounded, and a 60-second quickstart
|
|
92
93
|
|
|
93
94
|
### Dispatch & Cost
|
|
94
95
|
- **[DISPATCH-MODEL.md](DISPATCH-MODEL.md)** — Haiku-first subagent dispatch, cost analysis, patterns (fan-out, sequential, hierarchical)
|
|
@@ -144,6 +145,9 @@ Once you've completed the adopter journey, use these for operational reference:
|
|
|
144
145
|
**I want to understand multi-instance coordination**
|
|
145
146
|
→ [TEAM-STATE.md](TEAM-STATE.md)
|
|
146
147
|
|
|
148
|
+
**I want to swap the worker or orchestrator model (Ollama, OpenRouter, OpenAI...)**
|
|
149
|
+
→ [MICROKERNEL.md](MICROKERNEL.md)
|
|
150
|
+
|
|
147
151
|
**I'm reviewing a PR that changes orchestration behavior**
|
|
148
152
|
→ [BEHAVIORAL-PR-REVIEW.md](BEHAVIORAL-PR-REVIEW.md)
|
|
149
153
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# The Aesop Hypothesis: Why Crash-Recoverable Systems Outrun Distributed Ones
|
|
2
|
+
|
|
3
|
+
**Expanded from the original essay:** https://medium.com/@matt82198/the-aesop-hypothesis-ai-agents-that-survive-because-theyre-designed-to-fail-de5f033369d4
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Hypothesis
|
|
8
|
+
|
|
9
|
+
**Agent behavior is source code.** Everything a fleet does — every decision, every checkpoint, every recovery path — lives in durable, human-diffable files: git history, plain-text STATE.md, append-only BUILDLOG.md, Python scripts, shell hooks. No vector embeddings, no distributed consensus, no magic. When a machine fails, you re-read from disk. When a human operator needs to audit a decision, they grep the git log or read the state file. When you need to reason about cost, you look at the dispatch rules in code.
|
|
10
|
+
|
|
11
|
+
This hypothesis rests on five pillars:
|
|
12
|
+
|
|
13
|
+
1. **Durable plain-text state** — git + POSIX text as the state layer, not Postgres or vector DBs.
|
|
14
|
+
2. **Stateless runtimes** — agents execute one request at a time; permanent state lives on disk.
|
|
15
|
+
3. **Cost-aware parallelism** — cheap Haiku subagents in parallel, not serial Opus.
|
|
16
|
+
4. **Guardrails in code, not prose** — pre-push secret gates, kill-switches, cost ceilings: all executable.
|
|
17
|
+
5. **Observable signals** — heartbeats, append-only logs, drift detectors, crash-recovery as the normal startup path.
|
|
18
|
+
|
|
19
|
+
The bet is this: **a small, crash-recoverable system running on git and plain text outperforms a distributed one** in latency, debuggability, cost, and trust — because the simpler system fails loudly and often, learns from every failure, and never hides state in a database you can't grep.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## (1) Git + POSIX Text as the State Layer
|
|
24
|
+
|
|
25
|
+
**Why not Postgres? Why not vector DBs?**
|
|
26
|
+
|
|
27
|
+
Aesop's core state lives in git-committed files: `STATE.md` (intent, phase, NEXT STEPS), `BUILDLOG.md` (append-only progress snapshots), Python scripts (cost rules, dispatch logic), shell hooks (pre-push gates). This is not a limitation. It is the whole idea.
|
|
28
|
+
|
|
29
|
+
**Durability.** Postgres fails when the connection pool is exhausted or the database is unreachable. Git fails when the filesystem is corrupted — a far rarer event on any modern machine. State committed to git survives machine wipes, container restarts, session loss. You clone a repository from 2026-07-18, and you know *exactly* what the system was doing on that date. No migration scripts, no schema versioning, no eventual consistency.
|
|
30
|
+
|
|
31
|
+
**Human-diffable forensics.** When something goes wrong, you run `git log -p` and read the actual changes that led to the broken state. You see not just what happened, but *why* the system made each decision (because the humans who designed it wrote it in code and commit messages). A vector DB stores embeddings; a BUILDLOG.md stores human-readable decisions.
|
|
32
|
+
|
|
33
|
+
**Single-box by explicit design choice.** Aesop is not "not distributed yet." Multi-instance coordination is *deliberately unscheduled*. The system runs on one machine. When team scale requires multi-instance support, the real work is not "add Postgres"; it is "redesign state to support leases and event-sourcing on SQLite." That redesign is on the roadmap, not an architectural gap. Postgres is a refactoring target *after* the single-box proves the core loop works. Premature distribution is premature optimization.
|
|
34
|
+
|
|
35
|
+
**Cite:** [`docs/CHECKPOINTING.md`](./CHECKPOINTING.md) — the durable state strategy; [`docs/CARDINAL-RULES.md`](./CARDINAL-RULES.md) § 5 — handoff discipline.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## (2) Stateless Runtimes + Persistent Filesystem Brain
|
|
40
|
+
|
|
41
|
+
**The architecture is simple:** agents are processes. Each agent receives a scoped task, reads the filesystem for context, makes decisions, writes results, and exits. The filesystem is the only source of truth.
|
|
42
|
+
|
|
43
|
+
When an agent crashes (or hits a timeout, or the user kills it), the next agent picks up from the checkpoint files on disk. There is no agent state in memory, no distributed transaction, no graceful shutdown protocol. Dead is dead; reading from disk is the recovery protocol *and the normal startup path*.
|
|
44
|
+
|
|
45
|
+
**Why this matters:** the system never invents state. An agent that hangs for 3 minutes and is forcibly killed is indistinguishable from one that exits normally — both leave state on disk, and the next reader validates what's there. No "check if this agent is still alive" logic, no heartbeat-based membership. The watchdog's job is simple: if a task hangs, kill it; the orchestrator will re-read the checkpoint and decide what to do next.
|
|
46
|
+
|
|
47
|
+
**Crash recovery is not a special path.** On resume after a crash (or user interruption, or session loss), the orchestrator reads STATE.md and BUILDLOG.md from disk, verifies them against git log, and proceeds. If STATE.md is stale, it updates it. If BUILDLOG.md shows a half-completed task, it completes it or rolls back and retries. This is the same code path that runs on normal startup. No two code paths, no special-case recovery hooks, no "was I shut down cleanly?" flag.
|
|
48
|
+
|
|
49
|
+
**Cite:** [`docs/CHECKPOINTING.md`](./CHECKPOINTING.md) — recovery workflow; [`docs/RELIABILITY.md`](./RELIABILITY.md) — inputs-always-produce-outputs principle.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## (3) Cost Architecture: Haiku-First, Flat Fan-Out, the Cancelled Hierarchical Design
|
|
54
|
+
|
|
55
|
+
**The cost model is the heart of the system.** Subagents are *always* Haiku (1/3 the per-token cost of Opus), spawned in parallel (5–8 agents per wave). This single rule, more than any other, determines whether agent-driven work scales or burns money.
|
|
56
|
+
|
|
57
|
+
**The A/B that killed hierarchical dispatch:** Earlier designs proposed a three-tier model — Fable orchestrator + Sonnet supervisors (splitting work into domains) + Haiku workers. Lab testing showed **4.3× cost increase for identical quality**. The hierarchical design was cancelled. (Cancelled architectures with published data is engineering honesty, not weakness.)
|
|
58
|
+
|
|
59
|
+
Today's dispatch is flat: one Opus/Fable orchestrator on the main thread, 5–8 parallel Haiku workers per wave, no intermediate supervisors. Cost per wave: roughly $0.01–0.02 USD. Scaling to 10 waves per day still costs less than a single Opus API call.
|
|
60
|
+
|
|
61
|
+
**The benchmark proves Haiku is good enough.** The held-out judgment benchmark (v3 = 28 additional tasks, building on v2 = 11 prior) tested Haiku, Sonnet, and Opus across 39 combined judgment tasks: bug-in-diff (with concurrency races and resource leaks), finding-inflation, acceptance-criteria coverage, severity calibration, root-cause-from-trace, refactor-equivalence, security issue spotting. All three models converged on identical answers for all 28 v3 tasks. Combined score: **Haiku 39/39** vs **Opus 38/39** (Opus erred on one severity call; Haiku did not). At ~1/3 the per-token cost.
|
|
62
|
+
|
|
63
|
+
**Honest limits on the benchmark:** Curated (N=39), not sampled from real fleet transcripts. No frontier-reaching task found where Opus beats Haiku. The benchmark maps a floor ("Haiku is sufficient for these judgment shapes"), not the absolute frontier. Cost is token-price ratio, not wall-clock latency. These are not hidden; they are load-bearing caveats.
|
|
64
|
+
|
|
65
|
+
**Cite:** [`docs/DISPATCH-MODEL.md`](./DISPATCH-MODEL.md) — cost model and patterns; [`bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md`](../bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md) — benchmark run and interpretation.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## (4) Guardrails Enforced in Code, Not Prose
|
|
70
|
+
|
|
71
|
+
**Safety rules live in executable code**, not documentation:
|
|
72
|
+
|
|
73
|
+
- **Pre-push secret gate** (`tools/secret_scan.py`): scans staged files for 50+ secret patterns (AWS keys, Anthropic keys, tokens). Exits with failure on file-read errors; never silently passes.
|
|
74
|
+
- **Kill-switch** (`tools/halt.py`): wired into the live dispatch path. When triggered, aborts all pending work with zero new workers spawned. Operator-triggered (manual brake), not autonomous.
|
|
75
|
+
- **Cost ceiling** (`tools/cost_ceiling.py`): halts dispatch when the configured per-wave budget is exceeded. Enforces a *configured* ceiling, not live-metered spend.
|
|
76
|
+
- **Pre-push branch checks**: run before every push (enforced via git hooks). No committing to main, no force-push without explicit approval.
|
|
77
|
+
|
|
78
|
+
The key insight: **fail-closed by default.** A secret-scan that silently passes when the file is unreadable is worse than a crash. A kill-switch that doesn't trip is useless. A cost ceiling that is "maybe" enforced wastes tokens. Aesop inverts the default: safety rules are executable and logged; unsafe paths are explicitly rejected; a gate that fails triggers an immediate backout.
|
|
79
|
+
|
|
80
|
+
**Cite:** [`docs/CARDINAL-RULES.md`](./CARDINAL-RULES.md) § 7 — security and version control; [`tools/halt.py`](../tools/halt.py), [`tools/cost_ceiling.py`](../tools/cost_ceiling.py) — implementations.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## (5) Observability: Heartbeats, Append-Only Logs, Drift Signals, and CI Sharding
|
|
85
|
+
|
|
86
|
+
**Every action produces a signal.** Daemons emit heartbeats every cycle (even on error). Logs are append-only; every task appended with a timestamp. Stalled agents trigger automatic watchdog restarts (3 retries, then escalate to human).
|
|
87
|
+
|
|
88
|
+
**Drift detection.** The orchestrator compares expected state (BUILDLOG.md) against reality (git log, filesystem timestamps). Drift = stale checkpoint, incomplete work, or a half-written file. On detection, the system does not guess: it re-reads from disk and either rolls forward (if work completed) or rolls back (if interrupted).
|
|
89
|
+
|
|
90
|
+
**CI sharding story.** Early on, Aesop's test suite ran serially on Windows, wall-clock time ~11 minutes. A single spawn-semantics bug hit Windows harder than Linux (process group cleanup behaved differently). Rather than paper over it with retries, the team:
|
|
91
|
+
1. Diagnosed the root cause (Windows process tree cleanup).
|
|
92
|
+
2. Fixed it (explicit cleanup in the test harness).
|
|
93
|
+
3. Added sharding (4-way split, ~3 min wall-clock with 80–180s per shard).
|
|
94
|
+
4. Made Windows a required check (previously optional).
|
|
95
|
+
|
|
96
|
+
The point: **observability means you see the real bottleneck**, and you fix it, not the symptom. Aesop's CI reports job timings for every shard; the orchestrator reads those and can rebalance if a shard drifts >20% off baseline.
|
|
97
|
+
|
|
98
|
+
**Cite:** [`docs/CARDINAL-RULES.md`](./CARDINAL-RULES.md) § 3 — reliability core and heartbeats; [`docs/RELIABILITY.md`](./RELIABILITY.md) — inputs-always-produce-outputs, never-wait discipline.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Proof: What Ships with the System
|
|
103
|
+
|
|
104
|
+
These are not claims about what Aesop *could* do. They are receipts:
|
|
105
|
+
|
|
106
|
+
- **1,088 commits, 251 merged PRs, 30 waves** (verified by anyone who clones; `tools/self_stats.py`).
|
|
107
|
+
- **143,403 lines of code** across 546 files, delivered end-to-end: from feature intake to merge.
|
|
108
|
+
- **Benchmark results** committed in `bench/results/` — 39 judgment tasks, all models scored by deterministic Python scoring (no LLM in the grading loop).
|
|
109
|
+
- **Kill-switch proof** — `tools/halt.py` is wired into the live dispatch path and was exercised on a real wave.
|
|
110
|
+
- **Cost ceiling** — implemented in `tools/cost_ceiling.py`, enforced per-wave.
|
|
111
|
+
- **Windows CI sharding** — reduced wall-clock time from ~11 min to ~3 min (4-way shard); now a required check.
|
|
112
|
+
- **Durable state** — STATE.md, BUILDLOG.md, and all orchestration rules are git-committed and human-readable.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Honest Limits
|
|
117
|
+
|
|
118
|
+
This is not a universal solution. The system has explicit boundaries:
|
|
119
|
+
|
|
120
|
+
1. **Single-box by design.** Aesop runs on one machine. Multi-instance coordination is on the roadmap, not shipped. If you need 100-machine scale today, this is not the tool.
|
|
121
|
+
2. **Small-N benchmarks.** 39 judgment tasks is directional evidence, not statistical proof. Frontier reasoning (where Opus depth might matter 3×) is not tested here.
|
|
122
|
+
3. **Lab-measured multi-writer throughput.** 800 events/sec is measured in a stress test, not production. Team scale beyond one machine requires additional work (leases, event-sourcing, distributed consensus).
|
|
123
|
+
4. **No third-party verification yet.** The artifacts are committed so a skeptic can reproduce — that is transparency, not independent replication.
|
|
124
|
+
5. **Release candidate.** APIs, config, and dashboard contracts may still shift. Pin the exact version if you need stability.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## The Bet
|
|
129
|
+
|
|
130
|
+
**Simple systems that fail loudly and often outrun complex ones that hide state in databases.**
|
|
131
|
+
|
|
132
|
+
Aesop bets on:
|
|
133
|
+
- **Transparency over abstraction.** Every decision is code. Every state is a file you can read and diff.
|
|
134
|
+
- **Crash recovery as design principle.** If you build for recovery from scratch, you build for reliability. Distributed systems hide failures; crash-recoverable ones surface them.
|
|
135
|
+
- **Small is faster than smart.** Flat fan-out (5–8 Haiku agents) beats hierarchical dispatch (4.3× cost), even at scale, because the simpler system has fewer failure modes.
|
|
136
|
+
- **Cost as a first-class constraint.** The whole system is designed around $0.01–0.02 per wave. Expensive paths are rejected before they ship.
|
|
137
|
+
|
|
138
|
+
The evidence is in the receipts: 1,088 commits, 251 PRs, 30 waves, zero hallucinated audits (via adversarial verification), and a benchmark that proves Haiku is good enough.
|
|
139
|
+
|
|
140
|
+
**Read more:** [`docs/autonomous-swe.md`](./autonomous-swe.md) — honest account of what shipped, what didn't, and where the gaps are.
|