@matt82198/aesop 0.1.0 → 0.2.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 +89 -2
- package/README.md +63 -12
- package/bin/cli.js +164 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Team-Shared State: Architecture & Migration
|
|
2
|
+
|
|
3
|
+
**Status**: Design document. Prototype complete (`state_store/` SQLite module shipped). Migration path defined. NOT YET INTEGRATED into orchestrator reader/writer paths.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem Statement
|
|
8
|
+
|
|
9
|
+
Aesop currently stores orchestration state in **two git-tracked files**:
|
|
10
|
+
- `STATE.md` — intent, decisions, phase, NEXT STEPS (single-writer orchestrator)
|
|
11
|
+
- `BUILDLOG.md` — append-only progress snapshots (agents append one line per work unit)
|
|
12
|
+
|
|
13
|
+
This works **for single-user/single-orchestrator** workflows but does not scale to teams:
|
|
14
|
+
|
|
15
|
+
1. **Hot-file merge conflicts** — Multiple agents appending to the same files requires rebasing and re-coordination; a team of 3+ users editing these simultaneously causes conflicts.
|
|
16
|
+
2. **No transactions/concurrency** — Git has no concept of atomic multi-file updates or per-agent locks; coordination is manual and error-prone.
|
|
17
|
+
3. **No real-time status** — Readers must `git fetch` to see the latest state; no subscription/push model for live updates.
|
|
18
|
+
4. **Single-writer bottleneck** — The orchestrator is the only writer to STATE.md; other workers cannot safely report their own state.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Current Truth: Git-as-State (Single-Instance)
|
|
23
|
+
|
|
24
|
+
### Data model
|
|
25
|
+
|
|
26
|
+
State lives as **human-readable JSON** in two git-tracked files:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
orchestrator (Fable/Opus) ←→ STATE.md (single-writer, locked decisions + phase + NEXT STEPS)
|
|
30
|
+
←→ BUILDLOG.md (append-only, agents append one line per work unit)
|
|
31
|
+
|
|
32
|
+
↓ (read-only consumers)
|
|
33
|
+
|
|
34
|
+
dashboards, monitoring, forensics
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Invariants**:
|
|
38
|
+
- Only the orchestrator writes to STATE.md (enforced by discipline, not by code)
|
|
39
|
+
- BUILDLOG.md is append-only; orchestrator never rewrites earlier entries
|
|
40
|
+
- All state is human-diffable (intentional — enables review, forensics, inheritance)
|
|
41
|
+
- Durability via git: push after each update, recover from `git log` on session restart
|
|
42
|
+
- Git is the **source of truth** for current state
|
|
43
|
+
|
|
44
|
+
### Limitations for team scale
|
|
45
|
+
|
|
46
|
+
- **No concurrent writers** — Two orchestrators or agents trying to write simultaneously causes merge conflicts
|
|
47
|
+
- **No transactions** — A phase update + next-steps change is two separate commits, not atomic
|
|
48
|
+
- **High latency** — State changes require `git push`; observability is on a push-frequency, not real-time
|
|
49
|
+
- **No locking** — State can be read/modified while orchestrator is deciding; no optimistic concurrency
|
|
50
|
+
- **No history/events** — Only final state (current file contents) is stored; intermediate states are lost
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Target: SQLite WAL as Live Substrate, Git as Export
|
|
55
|
+
|
|
56
|
+
### Proposed data model
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
orchestrator/agents write events ─→ SQLite WAL event log (LIVE substrate, fast, transactional)
|
|
60
|
+
↓
|
|
61
|
+
state_store/ EventStore
|
|
62
|
+
(append() atomic across processes/threads)
|
|
63
|
+
↓
|
|
64
|
+
[snapshots for O(n) tail-replay]
|
|
65
|
+
↓
|
|
66
|
+
projections/StateAPI (project tracker state)
|
|
67
|
+
↓
|
|
68
|
+
export_tracker() ─────────→ STATE.md / BUILDLOG.md (git-tracked EXPORT, human-diffable, immutable)
|
|
69
|
+
↓
|
|
70
|
+
[dashboards, monitoring, forensics read git export]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Key design principles
|
|
74
|
+
|
|
75
|
+
1. **Event sourcing**: State is a fold of immutable events, not mutable files. Each write is a single `append(stream, event_type, payload, actor)` call — atomic, ordered, versionable.
|
|
76
|
+
|
|
77
|
+
2. **Append-only**: No deletes or mutations. History is preserved forever. Replayability + auditing for free.
|
|
78
|
+
|
|
79
|
+
3. **Per-stream versioning**: Each "stream" (e.g., `"tracker"`, `"orchestrator_status"`) has its own 1-based, gapless version. Enables deterministic snapshots and tail-replay (see `state_store/store.py` line 81–92 and `state_store/projections.py` line 117–127).
|
|
80
|
+
|
|
81
|
+
4. **Concurrency-safe writes**: SQLite WAL mode + `BEGIN IMMEDIATE` transactions guarantee that two writers on the same file never collide or duplicate a version (see `state_store/store.py` line 30–94). Tested in `tests/test_state_store.py::EventStoreTest::test_concurrent_appends_have_no_dupes_or_gaps`.
|
|
82
|
+
|
|
83
|
+
5. **Snapshots for O(n) tail-replay**: Instead of replaying all events on every read, save materialized state snapshots at key versions; future replays fold only newer events (see `state_store/store.py` line 130–196 and `state_store/projections.py` line 84–114).
|
|
84
|
+
|
|
85
|
+
6. **Git as export, not source**: The SQLite store is the system-of-record. `export_tracker(api, out_path)` renders the current projection back to human-readable JSON for git durability and review (see `state_store/export.py`). Git is now **read-only for state** (except for manual edits, which become new events on next `ingest_tracker_json`).
|
|
86
|
+
|
|
87
|
+
7. **Projection-based readers**: Callers do not query the raw event log; instead, they call `api.project(view)` to get the current state as a dict. New projections (e.g., `project_orchestrator_status`) can be registered in `state_store/api.py` without changing the event store (see line 12–35).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Architecture: The Three Layers
|
|
92
|
+
|
|
93
|
+
### 1. Event Store (SQLite WAL)
|
|
94
|
+
|
|
95
|
+
**File**: `state_store/store.py`
|
|
96
|
+
|
|
97
|
+
**Class**: `EventStore(db_path)`
|
|
98
|
+
|
|
99
|
+
**Public API**:
|
|
100
|
+
- `append(stream: str, event_type: str, payload: dict, actor: str = "system") -> int` — Append one event, return its per-stream version. Atomic: multiple writers see no dupes or gaps (tests line 90–108).
|
|
101
|
+
- `read(stream: str) -> list` — All events in a stream, ascending by version. Returns list of dicts with `{"id", "ts", "actor", "stream", "type", "payload", "version"}`.
|
|
102
|
+
- `read_all() -> list` — All events across all streams, ascending by global id.
|
|
103
|
+
- `save_snapshot(stream: str, event_version: int, projection: dict) -> None` — Persist a materialized projection at a specific version (line 130–153).
|
|
104
|
+
- `read_snapshot(stream: str) -> tuple | None` — Read the most recent snapshot; returns `(event_version, projection_dict, checksum)` or None (line 155–197).
|
|
105
|
+
|
|
106
|
+
**Concurrency model**:
|
|
107
|
+
- Multiple writers: `BEGIN IMMEDIATE` locks the write lock up front, so read-max-version-then-insert is atomic (line 80).
|
|
108
|
+
- Multiple readers: SQLite WAL mode allows readers to run concurrently with writers on separate .db-wal shadow files (no blocking).
|
|
109
|
+
- Process/thread-safe: each `append()` call opens its own connection; `busy_timeout=5000` (5 seconds) waits if the DB is locked (line 32–94).
|
|
110
|
+
- Tested with two threads, each with its own EventStore instance, appending 100 events concurrently with no dupes (tests line 90–108).
|
|
111
|
+
|
|
112
|
+
### 2. Projections (Fold Events into State)
|
|
113
|
+
|
|
114
|
+
**File**: `state_store/projections.py`
|
|
115
|
+
|
|
116
|
+
**Functions**:
|
|
117
|
+
- `_fold_events(events, order=None, items=None) -> (order, items)` — Fold events into an accumulated state tuple. Mutates order + items in place. Helper used by both full and snapshot-aware projections (line 25–71).
|
|
118
|
+
|
|
119
|
+
- `project_tracker(events: list) -> dict` — Original full-replay projection: fold all events → `{"version": 1, "items": [items in first-seen order]}`. Defines tracker event schema:
|
|
120
|
+
- `item_created` (payload = full item dict): establishes item, kept in first-seen order
|
|
121
|
+
- `item_updated` (payload = `{"id": ..., ...partial fields}`): merges partial fields onto existing item
|
|
122
|
+
- `item_archived` (payload = `{"id": ...}`): sets `status` to `"archived"`, optionally merges `completed_at`
|
|
123
|
+
- Unknown event types: ignored (line 74–81).
|
|
124
|
+
|
|
125
|
+
- `project_tracker_with_snapshot(store, stream: str, events: list) -> dict` — Snapshot-aware projection: load latest snapshot; fold only tail events (after snapshot version) for O(n) tail-replay. Falls back to full replay if snapshot is missing or corrupt (line 84–114).
|
|
126
|
+
|
|
127
|
+
- `save_snapshot(store, stream: str, event_version: int, projection: dict) -> None` — Wrapper over `store.save_snapshot()` (line 117–126).
|
|
128
|
+
|
|
129
|
+
**Event schema for tracker**:
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"type": "item_created",
|
|
133
|
+
"payload": {"id": "abc", "title": "...", "lane": "proposed", "status": "todo", ...}
|
|
134
|
+
}
|
|
135
|
+
{
|
|
136
|
+
"type": "item_updated",
|
|
137
|
+
"payload": {"id": "abc", "lane": "in-progress"}
|
|
138
|
+
}
|
|
139
|
+
{
|
|
140
|
+
"type": "item_archived",
|
|
141
|
+
"payload": {"id": "abc", "completed_at": "2026-07-17T...Z"}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Tolerance: unknown ids on update/archive are ignored (no error); unknown event types are skipped (line 62–70).
|
|
146
|
+
|
|
147
|
+
### 3. Facade API (Single Seam for Backend Swaps)
|
|
148
|
+
|
|
149
|
+
**File**: `state_store/api.py`
|
|
150
|
+
|
|
151
|
+
**Class**: `StateAPI(db_path)`
|
|
152
|
+
|
|
153
|
+
**Public API**:
|
|
154
|
+
- `append(stream: str, event_type: str, payload: dict, actor: str = "system") -> int` — Append event, return version.
|
|
155
|
+
- `get(stream: str) -> list` — Read all events in a stream.
|
|
156
|
+
- `project(view: str) -> dict` — Fold the named stream through its projector into current state. Raises `ValueError` if projector not registered (line 15–35).
|
|
157
|
+
|
|
158
|
+
**Backend swaps**: The backend (SQLite now, Postgres later) is abstracted here. Callers **never** touch `EventStore` directly; they use `StateAPI` only. On cutover to Postgres, only `api.py` changes; call sites stay the same.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## New State Streams (Beyond Tracker)
|
|
163
|
+
|
|
164
|
+
The tracker stream is the first proof of concept. The event log supports **arbitrary streams**; new streams are registered by adding a projector to `_PROJECTORS` in `state_store/api.py` (line 12).
|
|
165
|
+
|
|
166
|
+
### Proposed: `orchestrator_status` stream
|
|
167
|
+
|
|
168
|
+
**Purpose**: Record orchestrator phase, NEXT STEPS, decisions, and audit results in real-time without git writes.
|
|
169
|
+
|
|
170
|
+
**Events**:
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"type": "phase_changed",
|
|
174
|
+
"payload": {"phase": "wave-rc.3", "reason": "stable release shipped"}
|
|
175
|
+
}
|
|
176
|
+
{
|
|
177
|
+
"type": "next_steps_updated",
|
|
178
|
+
"payload": {"steps": ["reconcile state_store SI", "add orchestrator_status stream", ...]}
|
|
179
|
+
}
|
|
180
|
+
{
|
|
181
|
+
"type": "decision_locked",
|
|
182
|
+
"payload": {"key": "model_dispatch_core", "value": "out-of-repo (structural)", "justification": "..."}
|
|
183
|
+
}
|
|
184
|
+
{
|
|
185
|
+
"type": "audit_findings_recorded",
|
|
186
|
+
"payload": {"level": "wave-rc.2", "findings_count": 3, "summary": "..."}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Projection** (pseudo-code):
|
|
191
|
+
```python
|
|
192
|
+
def project_orchestrator_status(events):
|
|
193
|
+
status = {
|
|
194
|
+
"phase": None,
|
|
195
|
+
"decisions": {},
|
|
196
|
+
"next_steps": [],
|
|
197
|
+
"latest_audit": None,
|
|
198
|
+
}
|
|
199
|
+
for ev in events:
|
|
200
|
+
if ev["type"] == "phase_changed":
|
|
201
|
+
status["phase"] = ev["payload"]["phase"]
|
|
202
|
+
elif ev["type"] == "decision_locked":
|
|
203
|
+
key = ev["payload"]["key"]
|
|
204
|
+
status["decisions"][key] = ev["payload"]["value"]
|
|
205
|
+
elif ev["type"] == "next_steps_updated":
|
|
206
|
+
status["next_steps"] = ev["payload"]["steps"]
|
|
207
|
+
elif ev["type"] == "audit_findings_recorded":
|
|
208
|
+
status["latest_audit"] = ev["payload"]
|
|
209
|
+
return status
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Readers** (same-turn):
|
|
213
|
+
- Dashboard queries `api.project("orchestrator_status")` → sees latest phase, decisions, next steps live (no git fetch).
|
|
214
|
+
- Monitor daemon queries `api.project("orchestrator_status")` → sees what the orchestrator decided last turn; compares against local observations.
|
|
215
|
+
- Agents query `api.project("orchestrator_status")` → see their assignment (brief payload in next_steps).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Migration Path: Which Readers/Writers Move First
|
|
220
|
+
|
|
221
|
+
### Phase 1: Add orchestrator_status Stream (EARLY CUTOVER)
|
|
222
|
+
|
|
223
|
+
**What moves**: Orchestrator WRITES phase + next steps to `orchestrator_status` events instead of `STATE.md`. Readers stay on git (BUILDLOG.md + STATE.md fetches).
|
|
224
|
+
|
|
225
|
+
**Why first**: Orchestrator is single-writer; no merge conflicts to worry about. Proof that the event store works under real orchestrator load.
|
|
226
|
+
|
|
227
|
+
**Changes**:
|
|
228
|
+
1. Add `orchestrator_status` projector to `state_store/projections.py` (new file or extend existing).
|
|
229
|
+
2. Orchestrator calls `api.append("orchestrator_status", "phase_changed", {...}, actor="orchestrator")` instead of git commits.
|
|
230
|
+
3. Export job (`export_orchestrator_status()`) renders to a new `STATE.md` periodically (e.g., per-phase boundary) for git durability.
|
|
231
|
+
4. Readers still read git; no behavior change yet.
|
|
232
|
+
|
|
233
|
+
**Concurrency model**: Single orchestrator writer, many readers (dashboards, agents). No conflicts.
|
|
234
|
+
|
|
235
|
+
**Git-authoritative?**: NO. `STATE.md` becomes a rendered snapshot, not source-of-truth. On recovery, orchestrator reads latest `STATE.md` but reconciles with `orchestrator_status` events to fill any gaps.
|
|
236
|
+
|
|
237
|
+
### Phase 2: Dual-Read Tracker (MIDDLE CUTOVER)
|
|
238
|
+
|
|
239
|
+
**What moves**: Tracker CRUD (item creation, updates, archives) writes to the event log. Readers read from either git or the live event log (both running simultaneously).
|
|
240
|
+
|
|
241
|
+
**Why here**: Tracker is the most-read state; dual-read lets us test the event store under high read load while keeping git as a safety net.
|
|
242
|
+
|
|
243
|
+
**Changes**:
|
|
244
|
+
1. Add dual-read logic to tracker consumers: try `api.project("tracker")` first; fall back to git if the event store is unavailable.
|
|
245
|
+
2. Tracker CRUD emitters (`create_item()`, `update_item()`, `archive_item()`) append events instead of mutating git.
|
|
246
|
+
3. Keep the export job running: `export_tracker(api, state/tracker.json)` renders live projections back to git.
|
|
247
|
+
4. Tests compare git and event-store projections; must match exactly (see tests line 165–179).
|
|
248
|
+
|
|
249
|
+
**Concurrency model**: Multiple writers (agents creating/updating items), many readers. SQLite WAL + `BEGIN IMMEDIATE` handles concurrency safely.
|
|
250
|
+
|
|
251
|
+
**Git-authoritative?**: NO. Event store is source-of-truth. Git is an export. Reconciliation required on recovery.
|
|
252
|
+
|
|
253
|
+
### Phase 3: Flip Readers (CUTOVER COMPLETE)
|
|
254
|
+
|
|
255
|
+
**What moves**: All readers flip to the event store; git becomes purely an audit trail.
|
|
256
|
+
|
|
257
|
+
**Changes**:
|
|
258
|
+
1. Remove dual-read fallback; all tracker reads go through `api.project("tracker")`.
|
|
259
|
+
2. Remove git-read paths from dashboards, forensics, monitoring.
|
|
260
|
+
3. Keep export jobs running for durability + review.
|
|
261
|
+
|
|
262
|
+
**Concurrency model**: No change; SQLite WAL already supports N readers + 1 writer per connection.
|
|
263
|
+
|
|
264
|
+
**Git-authoritative?**: NO. Event store is source-of-truth.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Locking & Concurrency Model
|
|
269
|
+
|
|
270
|
+
### Write path (multiple writers allowed)
|
|
271
|
+
|
|
272
|
+
1. Caller: `api.append("tracker", "item_created", {"id": "x", ...}, actor="agent-1")`
|
|
273
|
+
2. `StateAPI` delegates to `EventStore.append()`.
|
|
274
|
+
3. `EventStore.append()` opens a new connection, sets `busy_timeout=5000` (wait up to 5 seconds if locked).
|
|
275
|
+
4. Executes `BEGIN IMMEDIATE` (acquires write lock up front).
|
|
276
|
+
5. Reads max version for the stream (e.g., currently 42).
|
|
277
|
+
6. Inserts the new event with version 43.
|
|
278
|
+
7. Commits (releases lock).
|
|
279
|
+
8. Returns version 43.
|
|
280
|
+
|
|
281
|
+
**Guarantee**: No two writers see the same max version. Two concurrent writers always get consecutive versions (43, 44) with no dupes or gaps.
|
|
282
|
+
|
|
283
|
+
**Tested**: `tests/test_state_store.py::EventStoreTest::test_concurrent_appends_have_no_dupes_or_gaps` — two threads, 50 appends each, verified versions 1–100 are consecutive.
|
|
284
|
+
|
|
285
|
+
### Read path (many readers in parallel)
|
|
286
|
+
|
|
287
|
+
1. Caller: `api.project("tracker")`
|
|
288
|
+
2. `StateAPI` gets all events: `store.read("tracker")`
|
|
289
|
+
3. `EventStore.read()` opens a connection, sets `busy_timeout=5000`.
|
|
290
|
+
4. Queries all events for the stream (ascending by version). In WAL mode, readers see a consistent snapshot (MVCC).
|
|
291
|
+
5. Returns list of dicts.
|
|
292
|
+
6. `project_tracker()` folds events into a projection (in-process, no DB access).
|
|
293
|
+
|
|
294
|
+
**Guarantee**: Readers never block writers (WAL mode). Readers see a consistent snapshot at the moment they start the read. Dirty reads are impossible; serialization is guaranteed by SQLite's MVCC.
|
|
295
|
+
|
|
296
|
+
### Snapshot coordination
|
|
297
|
+
|
|
298
|
+
1. `project_tracker_with_snapshot()` reads the latest snapshot (one extra query).
|
|
299
|
+
2. Folds only events after the snapshot version.
|
|
300
|
+
3. If the snapshot is corrupt, falls back to full replay (graceful degradation).
|
|
301
|
+
|
|
302
|
+
**Snapshot updates are asynchronous**: A background job can call `api._store.save_snapshot(stream="tracker", event_version=100, projection=...)` without blocking readers. The snapshot is just an optimization.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## What Stays Git-Authoritative (and Why)
|
|
307
|
+
|
|
308
|
+
### 1. Durable Checkpoint History
|
|
309
|
+
|
|
310
|
+
Git history (commit log, diffs, tags) remains **definitive for auditing and recovery**:
|
|
311
|
+
|
|
312
|
+
- Each time an export job runs, it writes a new commit to git.
|
|
313
|
+
- The commit hash anchors a specific version of all state streams.
|
|
314
|
+
- On session restart, `git log --oneline` shows when state changed, who changed it, and why.
|
|
315
|
+
- `git show <commit>` lets you see exactly what was decided at that moment.
|
|
316
|
+
|
|
317
|
+
**Why not move to SQLite?** SQLite has no built-in diff/review/approval workflow. Code review, pull requests, and merge discipline are git patterns. For team scale, retaining git as the audit log preserves these workflows.
|
|
318
|
+
|
|
319
|
+
### 2. Durability & Offline Safety
|
|
320
|
+
|
|
321
|
+
SQLite stores state **in the working tree**, which may get wiped, corrupted, or lost.
|
|
322
|
+
|
|
323
|
+
Git stores state **on remote servers** (GitHub, GitLab, etc.), which have redundancy, backups, and audit logs built in.
|
|
324
|
+
|
|
325
|
+
**Recommendation**: Always run `export_tracker()` and `export_orchestrator_status()` jobs after each major state change. Push the exports to git. If the SQLite store is corrupted, `ingest_tracker_json(git_version)` reconstitutes it.
|
|
326
|
+
|
|
327
|
+
### 3. Team Sync & Cross-Machine Continuity
|
|
328
|
+
|
|
329
|
+
When a developer or orchestrator switches machines or sessions:
|
|
330
|
+
|
|
331
|
+
1. Clone/pull the repo (gets the latest git state).
|
|
332
|
+
2. Reconstitute the event store: `ingest_tracker_json(api, state/tracker.json)` (tests line 165–179).
|
|
333
|
+
3. Continue from there.
|
|
334
|
+
|
|
335
|
+
**Why this works**: The ingest path is idempotent (see `state_store/ingest.py` line 14–41). Running it twice on the same events is a no-op (second run has no items to ingest, since they're already in the event log).
|
|
336
|
+
|
|
337
|
+
### 4. Configuration & Secrets
|
|
338
|
+
|
|
339
|
+
Aesop configuration (aesop.config.json, credentials, API keys) **does NOT go in state streams**. These remain git-ignored and managed separately.
|
|
340
|
+
|
|
341
|
+
Event payloads must not include secrets (enforced at the append call site, not in the store).
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Multi-Writer Concurrency: Measured (2026-07-18)
|
|
346
|
+
|
|
347
|
+
**Multi-writer safety is now verified under production load.** A concurrent-writer stress test (4 writer processes, 5s duration, WAL mode + `busy_timeout=5000` + `BEGIN IMMEDIATE`) appended 800 events across the tracker stream with zero dupes, zero gaps, zero "database is locked" failures. Measured throughput: ~704 events/sec. All projections converged (consistent state). This moves multi-writer support from "design" to "validated."
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Not Yet Built
|
|
352
|
+
|
|
353
|
+
### 1. **Orchestrator Reader/Writer Integration**
|
|
354
|
+
|
|
355
|
+
Currently, the orchestrator only writes to git (STATE.md). To move to the event store:
|
|
356
|
+
|
|
357
|
+
- Orchestrator `phase_changed` → write to stream `"orchestrator_status"` instead of STATE.md
|
|
358
|
+
- Orchestrator `next_steps_updated` → append event instead of git commit
|
|
359
|
+
- Orchestrator recovery: read `api.project("orchestrator_status")` to bootstrap phase + next steps
|
|
360
|
+
|
|
361
|
+
**Effort**: ~1 sprint. Touch: `skills/power/*, monitor/` event loop, orchestrator dispatch template.
|
|
362
|
+
|
|
363
|
+
### 2. **Dashboard Real-Time Subscription**
|
|
364
|
+
|
|
365
|
+
Currently, dashboards query the git export (STATE.md, tracker.json). For real-time updates:
|
|
366
|
+
|
|
367
|
+
- Add `subscribe(stream: str, callback)` to StateAPI
|
|
368
|
+
- Implement with SQLite's FTS/updates or a simple file-watch on the .db-wal file
|
|
369
|
+
- Dashboard websocket sends live updates to browsers
|
|
370
|
+
|
|
371
|
+
**Effort**: ~1 sprint. Touch: `ui/`, `state_store/api.py`.
|
|
372
|
+
|
|
373
|
+
### 3. **Team-Shared Database Setup**
|
|
374
|
+
|
|
375
|
+
The current design assumes a **local SQLite file** (filesystem-shared at best). For a true team:
|
|
376
|
+
|
|
377
|
+
- Backend swap: `StateAPI` points to Postgres instead of SQLite (seamless; only `api.py` changes).
|
|
378
|
+
- Setup: Postgres server, connection pooling, schema migration job.
|
|
379
|
+
- Tuning: Query optimization for high read/write load.
|
|
380
|
+
|
|
381
|
+
**Effort**: ~2 sprints. Mostly infrastructure/testing; application code is abstracted.
|
|
382
|
+
|
|
383
|
+
### 4. **Reconciliation & Conflict Resolution**
|
|
384
|
+
|
|
385
|
+
If two teams merge their state stores (e.g., two subagent fleets run independently, then reconcile):
|
|
386
|
+
|
|
387
|
+
- **Event deduplication**: No duplicate event ids; version numbers are per-stream (don't collide between stores).
|
|
388
|
+
- **Metadata tagging**: Each event carries an `actor` and `ts` (timestamp); reconciliation can detect/log conflicts.
|
|
389
|
+
- **Projection idempotence**: Refolding events is deterministic; the projection will converge to the same state.
|
|
390
|
+
|
|
391
|
+
**Current support**: `actor` field in every event. Reconciliation logic is **not yet implemented** (would live in `state_store/reconcile.py` or a separate tool).
|
|
392
|
+
|
|
393
|
+
**Effort**: ~1–2 sprints, depending on conflict resolution strategy (append-only, last-write-wins, domain-specific merge).
|
|
394
|
+
|
|
395
|
+
### 5. **Model-Dispatch Correlation**
|
|
396
|
+
|
|
397
|
+
The orchestrator needs to correlate which Haiku agent was given which backlog item and track per-item cost/success.
|
|
398
|
+
|
|
399
|
+
**Current event schema**: `orchestrator_status` events can carry item assignments, but **no current mechanism to append per-item traces** (which agent, which LLM, cost, tokens, result).
|
|
400
|
+
|
|
401
|
+
**Needed**:
|
|
402
|
+
- New stream: `"agent_traces"` with events like `agent_dispatched`, `agent_completed`, `agent_failed`.
|
|
403
|
+
- Agent brief template includes event-append credentials (stream + actor).
|
|
404
|
+
- Aggregation job correlates agent traces + cost logs + PR outcomes.
|
|
405
|
+
|
|
406
|
+
**Effort**: ~1–2 sprints. Touch: `dispatch template`, `monitor/`, cost logger.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Guarantees & Semantics
|
|
411
|
+
|
|
412
|
+
### Atomicity
|
|
413
|
+
|
|
414
|
+
Each `append()` call is atomic: either the event is inserted (and you get a version back) or it fails (exception). No partial writes.
|
|
415
|
+
|
|
416
|
+
Example:
|
|
417
|
+
```python
|
|
418
|
+
version = api.append("tracker", "item_created", {"id": "x", "title": "Y"}, actor="agent-1")
|
|
419
|
+
# version is now 43 (for stream "tracker")
|
|
420
|
+
# event is durable; will survive process restart
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Consistency
|
|
424
|
+
|
|
425
|
+
The event log is append-only; once an event is inserted, it never changes. The projection is deterministic: folding the same events always yields the same result.
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
```python
|
|
429
|
+
events_1 = api.get("tracker") # [v1, v2, v3]
|
|
430
|
+
proj_1 = project_tracker(events_1)
|
|
431
|
+
|
|
432
|
+
# Later:
|
|
433
|
+
events_2 = api.get("tracker") # [v1, v2, v3, v4, v5]
|
|
434
|
+
proj_2 = project_tracker(events_2)
|
|
435
|
+
|
|
436
|
+
# proj_2 differs from proj_1 only by v4+v5's changes (if any)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Isolation
|
|
440
|
+
|
|
441
|
+
Readers see a consistent snapshot (SQLite MVCC). No dirty reads. Concurrent writes are serialized by `BEGIN IMMEDIATE`.
|
|
442
|
+
|
|
443
|
+
Example:
|
|
444
|
+
```python
|
|
445
|
+
# Thread 1: reader
|
|
446
|
+
snapshot_1 = api.get("tracker") # sees [v1..v50]
|
|
447
|
+
|
|
448
|
+
# Thread 2: writer (concurrent)
|
|
449
|
+
v51 = api.append(...) # inserts v51
|
|
450
|
+
|
|
451
|
+
# Thread 1: reader (still sees [v1..v50], consistent snapshot)
|
|
452
|
+
snapshot_1 # unchanged; snapshot_1 is immutable
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Durability
|
|
456
|
+
|
|
457
|
+
Events are flushed to disk (SQLite journal + WAL file). On process restart, all events from the last committed transaction are recovered.
|
|
458
|
+
|
|
459
|
+
Example:
|
|
460
|
+
```python
|
|
461
|
+
version = api.append("tracker", "item_created", {...}) # returns 43, durable
|
|
462
|
+
# If process crashes here, event v43 is still there on next start
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Idempotence (Export Fidelity)
|
|
466
|
+
|
|
467
|
+
The `ingest_tracker_json()` + `project_tracker()` + `export_tracker()` round-trip preserves item fidelity: ingesting a tracker.json and exporting the projection reproduces the same items.
|
|
468
|
+
|
|
469
|
+
Tested in `tests/test_state_store.py::ApiAndExportTest::test_ingest_project_export_round_trips_real_tracker`:
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
original = json.loads("state/tracker.json")
|
|
473
|
+
api.ingest_tracker_json("state/tracker.json")
|
|
474
|
+
projected = api.project("tracker")
|
|
475
|
+
assert projected["items"] == original["items"] # exact match
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## File Organization
|
|
481
|
+
|
|
482
|
+
**Event store layer** (new):
|
|
483
|
+
- `state_store/store.py` — SQLite WAL append-only event log.
|
|
484
|
+
- `state_store/projections.py` — Fold events into tracker state.
|
|
485
|
+
- `state_store/api.py` — StateAPI facade (append/get/project).
|
|
486
|
+
- `state_store/export.py` — Render projections to git-tracked JSON.
|
|
487
|
+
- `state_store/ingest.py` — Backfill events from existing JSON.
|
|
488
|
+
- `state_store/__init__.py` — Public exports.
|
|
489
|
+
|
|
490
|
+
**Tests** (new):
|
|
491
|
+
- `tests/test_state_store.py` — EventStore + projection + round-trip tests.
|
|
492
|
+
- `tests/test_state_store_snapshots.py` — Snapshot save/load fidelity.
|
|
493
|
+
- `tests/test_state_store_hardening.py` — Concurrency, corruption recovery.
|
|
494
|
+
- `tests/test_api_state.py` — StateAPI facade tests.
|
|
495
|
+
|
|
496
|
+
**Git exports** (existing, to be extended):
|
|
497
|
+
- `state/tracker.json` — Current tracker state (now an export, not source-of-truth).
|
|
498
|
+
- `STATE.md` — (Will become an export; currently single-writer orchestrator file.)
|
|
499
|
+
- `BUILDLOG.md` — (Will be merged into event streams; currently append-only progress log.)
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Deployment Checklist (for future waves)
|
|
504
|
+
|
|
505
|
+
- [ ] **Wave N+1**: Add `orchestrator_status` stream to `state_store/projections.py`.
|
|
506
|
+
- [ ] Orchestrator transition: phase/next-steps writes to event store (no git commits).
|
|
507
|
+
- [ ] Export job: periodically render `orchestrator_status` → `STATE.md` for git durability.
|
|
508
|
+
- [ ] Dashboard: read `api.project("orchestrator_status")` + git for fallback.
|
|
509
|
+
- [ ] Tests: verify orchestrator recovery from event stream (not git) works.
|
|
510
|
+
|
|
511
|
+
- [ ] **Wave N+2**: Tracker dual-read.
|
|
512
|
+
- [ ] Tracker CRUD: write events instead of git.
|
|
513
|
+
- [ ] Add dual-read logic: try event store, fall back to git.
|
|
514
|
+
- [ ] Export job: `export_tracker()` keeps `state/tracker.json` in sync.
|
|
515
|
+
- [ ] Tests: compare git + event-store projections; they must match.
|
|
516
|
+
|
|
517
|
+
- [ ] **Wave N+3**: Flip readers.
|
|
518
|
+
- [ ] Remove dual-read fallback.
|
|
519
|
+
- [ ] All reads go through `api.project()`.
|
|
520
|
+
- [ ] Git becomes audit trail only.
|
|
521
|
+
|
|
522
|
+
- [ ] **Wave N+4**: Postgres backend (optional, depends on team scale).
|
|
523
|
+
- [ ] Postgres schema + migration job.
|
|
524
|
+
- [ ] Backend swap in `StateAPI.__init__()`.
|
|
525
|
+
- [ ] Load testing + performance tuning.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## References & Code Citations
|
|
530
|
+
|
|
531
|
+
All design claims are grounded in actual implementation:
|
|
532
|
+
|
|
533
|
+
- **Concurrency safety**: `state_store/store.py` lines 30–94 (BEGIN IMMEDIATE + busy_timeout).
|
|
534
|
+
- **Snapshot replay**: `state_store/projections.py` lines 84–114 (project_tracker_with_snapshot).
|
|
535
|
+
- **Tracker projection**: `state_store/projections.py` lines 25–81 (event folding logic).
|
|
536
|
+
- **Round-trip fidelity**: `tests/test_state_store.py` lines 165–179 (ingest + project + export test).
|
|
537
|
+
- **Concurrent append test**: `tests/test_state_store.py` lines 90–108 (two threads, no dupes).
|
|
538
|
+
- **API facade**: `state_store/api.py` lines 15–35 (StateAPI.project seam for backend swaps).
|
|
539
|
+
|
|
540
|
+
See `state_store/CLAUDE.md` for domain-specific setup and next steps.
|