@matt82198/aesop 0.1.0-beta.5 → 0.1.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 +158 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
package/dash/dash-extra.mjs
CHANGED
|
@@ -79,8 +79,38 @@ const ACTIVITY_WINDOW = 12 * 60 * 1000;
|
|
|
79
79
|
// Depth limit to match monitor's equivalent (prevent unbounded recursion on growing tree)
|
|
80
80
|
const MAX_DEPTH = 6;
|
|
81
81
|
|
|
82
|
-
//
|
|
83
|
-
|
|
82
|
+
// Cache file for transcript metadata (path, size, mtime -> token counts, etc.)
|
|
83
|
+
const CACHE_FILE = path.join(STATE_ROOT, '.dash-extra-cache.json');
|
|
84
|
+
|
|
85
|
+
// Load persistent cache from disk (keyed by "path|size|mtime")
|
|
86
|
+
function loadCache() {
|
|
87
|
+
try {
|
|
88
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
89
|
+
const data = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
90
|
+
return JSON.parse(data);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Parse error or file doesn't exist; start with empty cache
|
|
94
|
+
}
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Save cache to disk (safe: ignore write errors, cache is optimization only)
|
|
99
|
+
function saveCache(cache) {
|
|
100
|
+
try {
|
|
101
|
+
const dir = path.dirname(CACHE_FILE);
|
|
102
|
+
if (!fs.existsSync(dir)) {
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf8');
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore write errors; cache loss is non-fatal
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse JSONL agent transcript and extract rich metadata.
|
|
112
|
+
// Uses cache keyed by (path, size, mtime) to avoid re-reading unchanged files.
|
|
113
|
+
function parseAgentJsonl(filePath, cache) {
|
|
84
114
|
const metadata = {
|
|
85
115
|
promptFull: '',
|
|
86
116
|
taskLabel: '',
|
|
@@ -99,6 +129,27 @@ function parseAgentJsonl(filePath) {
|
|
|
99
129
|
metadata.project = parts[0];
|
|
100
130
|
}
|
|
101
131
|
|
|
132
|
+
// Get file stat for cache lookup
|
|
133
|
+
let stat;
|
|
134
|
+
try {
|
|
135
|
+
stat = fs.statSync(filePath);
|
|
136
|
+
} catch {
|
|
137
|
+
return metadata; // File disappeared; return empty
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Cache key combines path, size, and mtime for exact identity
|
|
141
|
+
const cacheKey = `${filePath}|${stat.size}|${stat.mtimeMs}`;
|
|
142
|
+
|
|
143
|
+
// Check if we have cached metadata for this exact file state
|
|
144
|
+
if (cache[cacheKey]) {
|
|
145
|
+
const cached = cache[cacheKey];
|
|
146
|
+
return {
|
|
147
|
+
...metadata,
|
|
148
|
+
...cached
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// File is new or changed; parse it
|
|
102
153
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
103
154
|
const lines = content.split('\n');
|
|
104
155
|
|
|
@@ -170,6 +221,17 @@ function parseAgentJsonl(filePath) {
|
|
|
170
221
|
if (firstTimestamp && lastTimestamp) {
|
|
171
222
|
metadata.runtimeSeconds = Math.floor((lastTimestamp - firstTimestamp) / 1000);
|
|
172
223
|
}
|
|
224
|
+
|
|
225
|
+
// Store in cache for next invocation
|
|
226
|
+
cache[cacheKey] = {
|
|
227
|
+
tokensUsed: metadata.tokensUsed,
|
|
228
|
+
taskLabel: metadata.taskLabel,
|
|
229
|
+
promptFull: metadata.promptFull,
|
|
230
|
+
startedAt: metadata.startedAt,
|
|
231
|
+
lastActivity: metadata.lastActivity,
|
|
232
|
+
runtimeSeconds: metadata.runtimeSeconds,
|
|
233
|
+
project: metadata.project
|
|
234
|
+
};
|
|
173
235
|
} catch {
|
|
174
236
|
// If file read fails, return empty metadata
|
|
175
237
|
}
|
|
@@ -177,6 +239,9 @@ function parseAgentJsonl(filePath) {
|
|
|
177
239
|
return metadata;
|
|
178
240
|
}
|
|
179
241
|
|
|
242
|
+
// Load persistent cache (keyed by path|size|mtime)
|
|
243
|
+
const cache = loadCache();
|
|
244
|
+
|
|
180
245
|
// Read alerts log if present
|
|
181
246
|
let slog = [];
|
|
182
247
|
try {
|
|
@@ -332,8 +397,8 @@ if (process.argv.includes('--json')) {
|
|
|
332
397
|
.replace(/\.jsonl$/, '')
|
|
333
398
|
.slice(0, 13);
|
|
334
399
|
|
|
335
|
-
// Parse JSONL for rich metadata
|
|
336
|
-
const metadata = parseAgentJsonl(f);
|
|
400
|
+
// Parse JSONL for rich metadata (uses cache to skip unchanged files)
|
|
401
|
+
const metadata = parseAgentJsonl(f, cache);
|
|
337
402
|
|
|
338
403
|
agents.push({
|
|
339
404
|
id: agentId,
|
|
@@ -350,8 +415,12 @@ if (process.argv.includes('--json')) {
|
|
|
350
415
|
promptFull: metadata.promptFull
|
|
351
416
|
});
|
|
352
417
|
}
|
|
418
|
+
// Persist cache for next invocation
|
|
419
|
+
saveCache(cache);
|
|
353
420
|
process.stdout.write(JSON.stringify(agents) + '\n');
|
|
354
421
|
} else {
|
|
355
422
|
// TUI mode: emit colored text
|
|
423
|
+
// Persist cache for next invocation
|
|
424
|
+
saveCache(cache);
|
|
356
425
|
process.stdout.write(out.join('\n') + '\n');
|
|
357
426
|
}
|
package/dash/watchdog-gui.sh
CHANGED
|
@@ -114,45 +114,51 @@ render_frame() {
|
|
|
114
114
|
echo -ne "$FRAME"
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
main() {
|
|
118
|
+
while true; do
|
|
119
|
+
SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
120
|
+
SPINNER_IDX=$(( SPINNER % 10 ))
|
|
121
|
+
SPIN_CHAR="${SPINNER_CHARS:$SPINNER_IDX:1}"
|
|
122
|
+
((SPINNER++))
|
|
123
|
+
|
|
124
|
+
now=$(date +%s)
|
|
125
|
+
hb=$(cat "$HB" 2>/dev/null)
|
|
126
|
+
case "$hb" in
|
|
127
|
+
''|*[!0-9]*) hb=0 ;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
130
|
+
if [ "$hb" -gt 0 ]; then
|
|
131
|
+
AGE=$(( now - hb ))
|
|
132
|
+
else
|
|
133
|
+
AGE=99999
|
|
134
|
+
fi
|
|
128
135
|
|
|
129
|
-
|
|
130
|
-
AGE
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
WD_THRESH=$(get_hb_threshold "watchdog")
|
|
137
|
+
if [ "$AGE" -lt "$WD_THRESH" ]; then
|
|
138
|
+
WD="${G}ALIVE${X} ${D}(${AGE}s)${X}"
|
|
139
|
+
else
|
|
140
|
+
WD="${R}STALE${X}"
|
|
141
|
+
fi
|
|
134
142
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
WD="${G}ALIVE${X} ${D}(${AGE}s)${X}"
|
|
138
|
-
else
|
|
139
|
-
WD="${R}STALE${X}"
|
|
140
|
-
fi
|
|
143
|
+
set -- $(awk '/^RESOLVED-FP/ { next } / HIGH / { hi++ } / MED / { me++ } END { print hi+0 " " me+0 }' "$SLOG" 2>/dev/null || echo '0 0')
|
|
144
|
+
HI="$1"; ME="$2"
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
if [ "$FIRST_FRAME" -eq 1 ]; then
|
|
147
|
+
printf '\033[2J'
|
|
148
|
+
FIRST_FRAME=0
|
|
149
|
+
else
|
|
150
|
+
printf '\033[H'
|
|
151
|
+
fi
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
printf '\033[H'
|
|
150
|
-
fi
|
|
153
|
+
render_frame | while IFS= read -r line; do
|
|
154
|
+
printf '%s\033[K\n' "$line"
|
|
155
|
+
done
|
|
156
|
+
printf '\033[J'
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
printf '%s\033[K\n' "$line"
|
|
158
|
+
sleep 4
|
|
154
159
|
done
|
|
155
|
-
|
|
160
|
+
}
|
|
156
161
|
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
163
|
+
main "$@"
|
|
164
|
+
fi
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Aesop's architecture is designed for **low cost, high parallelism, and durable state**. This page shows the system architecture with one wave cycle diagram.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## System Architecture Diagram
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
graph TD
|
|
11
|
+
A[Ranked Backlog]
|
|
12
|
+
B["Orchestrator<br/>(Fable/Opus main thread)"]
|
|
13
|
+
C["Parallel Haiku Fleet<br/>(worktree-isolated agents)"]
|
|
14
|
+
D1["Haiku-1<br/>(feat branch)"]
|
|
15
|
+
D2["Haiku-2<br/>(feat branch)"]
|
|
16
|
+
D3["Haiku-3<br/>(feat branch)"]
|
|
17
|
+
D4["..."]
|
|
18
|
+
E["Watchdog<br/>(heartbeat + respawn)"]
|
|
19
|
+
F["Integration Branch<br/>(merge train)"]
|
|
20
|
+
G["Checkpoint<br/>(STATE.md +<br/>BUILDLOG.md)"]
|
|
21
|
+
H["Closing Audit<br/>(findings)"]
|
|
22
|
+
I["Next Backlog"]
|
|
23
|
+
J["Monitor Daemon<br/>(signals)"]
|
|
24
|
+
K["Dashboard<br/>(live status)"]
|
|
25
|
+
|
|
26
|
+
A -->|spawns| B
|
|
27
|
+
B -->|dispatches| C
|
|
28
|
+
C --> D1
|
|
29
|
+
C --> D2
|
|
30
|
+
C --> D3
|
|
31
|
+
C --> D4
|
|
32
|
+
D1 -->|commit + push| F
|
|
33
|
+
D2 -->|commit + push| F
|
|
34
|
+
D3 -->|commit + push| F
|
|
35
|
+
D4 -->|commit + push| F
|
|
36
|
+
E -->|polls heartbeats| C
|
|
37
|
+
E -->|respawns on stall| C
|
|
38
|
+
F -->|orchestrator reviews| B
|
|
39
|
+
B -->|approves + merges| G
|
|
40
|
+
G -->|git-committed| H
|
|
41
|
+
H -->|findings → backlog| I
|
|
42
|
+
J -->|health checks| K
|
|
43
|
+
J -->|feeds| H
|
|
44
|
+
K -->|displays| B
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Component Overview
|
|
50
|
+
|
|
51
|
+
### 1. Ranked Backlog
|
|
52
|
+
|
|
53
|
+
The input to a wave. Backlog items are:
|
|
54
|
+
|
|
55
|
+
- **Ranked** by priority (P1: blockers, P2: quality, P3: debt)
|
|
56
|
+
- **Sized** for Haiku (3–10 min per agent)
|
|
57
|
+
- **Scoped** (one agent per item, no dependencies)
|
|
58
|
+
|
|
59
|
+
**Owned by**: You (at wave start)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### 2. Orchestrator (Main Thread)
|
|
64
|
+
|
|
65
|
+
**Fable or Opus** orchestrator on the main thread.
|
|
66
|
+
|
|
67
|
+
**Responsibilities**:
|
|
68
|
+
- Reads STATE.md, BUILDLOG.md, MEMORY.md (durable state)
|
|
69
|
+
- Ranks backlog into actionable items
|
|
70
|
+
- Spawns Haiku agents in parallel
|
|
71
|
+
- Monitors watchdog signals
|
|
72
|
+
- Reviews PRs and approves merges
|
|
73
|
+
- Conducts closing audit
|
|
74
|
+
|
|
75
|
+
**Why main thread?** Preserves context, warms prompt cache, avoids orchestrator-as-subagent (too expensive).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### 3. Parallel Haiku Fleet
|
|
80
|
+
|
|
81
|
+
**5–8 Haiku agents** running in parallel, each in its own worktree.
|
|
82
|
+
|
|
83
|
+
**Each agent**:
|
|
84
|
+
- Gets a **scoped brief** (what to build, acceptance criteria)
|
|
85
|
+
- Works in a **sibling worktree** (no conflicts with primary tree or other agents)
|
|
86
|
+
- Commits + pushes to a **feature branch**
|
|
87
|
+
- Opens a **PR** when done
|
|
88
|
+
|
|
89
|
+
**Cost benefit**: Haiku is ~1/3 Sonnet, 1/5 Opus. Running 5 in parallel = faster + much cheaper than serial Opus.
|
|
90
|
+
|
|
91
|
+
**Worktree isolation**: Each agent uses `git worktree add ../wt-<agent-name>` to ensure:
|
|
92
|
+
- No conflicts (agents don't interfere)
|
|
93
|
+
- Safe parallelism (each has its own git checkout)
|
|
94
|
+
- Easy cleanup (delete worktree on completion)
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### 4. Watchdog (Background Daemon)
|
|
99
|
+
|
|
100
|
+
**Purpose**: Detect hung agents and respawn them automatically.
|
|
101
|
+
|
|
102
|
+
**Polling**:
|
|
103
|
+
- Every 10s: check agent heartbeats
|
|
104
|
+
- Every 150s: run full backup (secret-scan, drift check, state backup)
|
|
105
|
+
|
|
106
|
+
**On stall** (>200s idle):
|
|
107
|
+
1. Ping agent (is it responsive?)
|
|
108
|
+
2. If no response: TaskStop + relaunch (auto-retry 1–3)
|
|
109
|
+
3. After 3 retries: mark BLOCKED and surface to user
|
|
110
|
+
|
|
111
|
+
**Why**: Prevents silent hangs, keeps fleet healthy, gives humans visibility into failures.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### 5. Integration Branch (Merge Train)
|
|
116
|
+
|
|
117
|
+
All feature branches are merged into a **integration branch** (e.g., `integration/wave-23`) in sequence.
|
|
118
|
+
|
|
119
|
+
**Orchestrator**:
|
|
120
|
+
1. Reviews each PR (code review, test coverage)
|
|
121
|
+
2. Approves if green
|
|
122
|
+
3. Merges to integration branch
|
|
123
|
+
4. Runs integration tests on the combined result
|
|
124
|
+
|
|
125
|
+
**Why ordered merge?** If one PR causes an integration failure, you know exactly which one. Easier to debug than merging all-at-once.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 6. Checkpoint (STATE.md + BUILDLOG.md)
|
|
130
|
+
|
|
131
|
+
After all merges succeed, **commit STATE.md and BUILDLOG.md to git**.
|
|
132
|
+
|
|
133
|
+
**STATE.md** (hand-maintained):
|
|
134
|
+
- Current phase (e.g., "Phase 4: Closing audit")
|
|
135
|
+
- Locked decisions
|
|
136
|
+
- Explicit NEXT STEPS
|
|
137
|
+
|
|
138
|
+
**BUILDLOG.md** (append-only):
|
|
139
|
+
- Timestamped work snapshots
|
|
140
|
+
- Status (green / blocked / pending)
|
|
141
|
+
- Recent blocker or next action
|
|
142
|
+
|
|
143
|
+
**Why git-committed?** Survives machine wipes. On resume, read them and sync from disk (no data loss).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 7. Closing Audit
|
|
148
|
+
|
|
149
|
+
Before the next wave, **audit** this wave:
|
|
150
|
+
|
|
151
|
+
**Checks**:
|
|
152
|
+
- Did all agents follow rules? (branch discipline, secret-scan gate, test coverage)
|
|
153
|
+
- How many retries did watchdog need?
|
|
154
|
+
- What bottlenecked? (long tasks, slow external APIs, etc.)
|
|
155
|
+
|
|
156
|
+
**Findings** → feed into next wave's backlog:
|
|
157
|
+
- "Config loader took 35 min; consider pre-writing scaffold" → P2 for wave 24
|
|
158
|
+
- "Dashboard tests are thin; expand coverage" → P2 for wave 24
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 8. Monitor Daemon (Side Observer)
|
|
163
|
+
|
|
164
|
+
**Background process** that collects health signals:
|
|
165
|
+
|
|
166
|
+
- Agent heartbeats and task status
|
|
167
|
+
- Fleet cost tracking
|
|
168
|
+
- Security events (secret-scan blocks, branch violations)
|
|
169
|
+
- Custom health checks (extensible via `monitor/collect-signals.mjs`)
|
|
170
|
+
|
|
171
|
+
**Outputs**:
|
|
172
|
+
- `BRIEF.md` — Snapshot of fleet health
|
|
173
|
+
- `SIGNALS.json` — Structured data for dashboard
|
|
174
|
+
- `.monitor-heartbeat` — Liveness marker
|
|
175
|
+
|
|
176
|
+
**Why separate?** Decouples observability from orchestration. Signals feed into audit, not directly into agent dispatch.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### 9. Dashboard (Live Status)
|
|
181
|
+
|
|
182
|
+
**Web UI** at http://localhost:8770 (runs `python ui/serve.py`).
|
|
183
|
+
|
|
184
|
+
**Feeds from**:
|
|
185
|
+
- Monitor daemon signals
|
|
186
|
+
- Orchestrator state (STATE.md, BUILDLOG.md)
|
|
187
|
+
- Cost ledger (token spend, per-model breakdown)
|
|
188
|
+
|
|
189
|
+
**Views**:
|
|
190
|
+
- **Overview** — Fleet agents, recent events, security alerts
|
|
191
|
+
- **Work** (#/work) — Task kanban (proposed → ranked → in-progress → done)
|
|
192
|
+
- **Activity** (#/activity) — Agent timeline, main-thread reasoning
|
|
193
|
+
- **Cost** (#/cost) — Per-model spend, per-day chart, verdict scorecard
|
|
194
|
+
|
|
195
|
+
**Why observability matters?** You can see exactly what's happening without reading logs. Faster decisions, faster recovery.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### 10. Next Backlog (Feedback Loop)
|
|
200
|
+
|
|
201
|
+
Findings from closing audit feed into the **next wave's backlog**:
|
|
202
|
+
|
|
203
|
+
- Learnings from this wave
|
|
204
|
+
- Tech-debt opportunities discovered during implementation
|
|
205
|
+
- Performance/UX improvements suggested by agents
|
|
206
|
+
- Scaling improvements (e.g., "cache this lookup")
|
|
207
|
+
|
|
208
|
+
**Result**: Each wave improves on the last. The system learns continuously.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Durable State Model
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
STATE.md ──┐
|
|
216
|
+
BUILDLOG.md├──→ git commit ──→ git push ──→ Survives machine wipe
|
|
217
|
+
MEMORY.md ─┘
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
On resume:
|
|
221
|
+
1. Read STATE.md → understand intent, current phase, NEXT STEPS
|
|
222
|
+
2. Skim BUILDLOG.md → review recent progress
|
|
223
|
+
3. Run git log → verify actual file state
|
|
224
|
+
4. Sync → if STATE.md is stale, update before proceeding
|
|
225
|
+
|
|
226
|
+
**Zero data loss**: Durable state on disk (git) is source of truth.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Cost Model
|
|
231
|
+
|
|
232
|
+
| Component | Cost | Notes |
|
|
233
|
+
|-----------|------|-------|
|
|
234
|
+
| Orchestrator (Opus) | ~$0.02 per wave | Main thread, lean context, 5–10 min reasoning |
|
|
235
|
+
| Haiku fleet (5 agents) | ~$0.01–0.02 per wave | 1/5 cost per token vs Opus |
|
|
236
|
+
| **Total** | **~$0.03–0.05 per wave** | 4–5x cheaper than all-Opus fleet |
|
|
237
|
+
|
|
238
|
+
**Key lever**: Haiku-first dispatch. Every subagent defaults to cheap tier (Haiku). Orchestrator stays lean. Result: maximum savings at scale.
|
|
239
|
+
|
|
240
|
+
See [DISPATCH-MODEL.md](DISPATCH-MODEL.md) for detailed cost analysis and patterns.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Security Model
|
|
245
|
+
|
|
246
|
+
### Pre-push hook (local enforcement)
|
|
247
|
+
|
|
248
|
+
Every push is gated:
|
|
249
|
+
|
|
250
|
+
1. **Branch check**: Reject pushes to `main` / `master` (must use `feature/*`)
|
|
251
|
+
2. **Secret-scan**: Scan staged files for detected credentials (API keys, tokens, passwords)
|
|
252
|
+
|
|
253
|
+
**Script**: `hooks/pre-push-policy.sh`
|
|
254
|
+
|
|
255
|
+
### GitHub branch protection (real enforcement)
|
|
256
|
+
|
|
257
|
+
For production, pair with GitHub:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
Settings > Branches > main
|
|
261
|
+
✓ Require pull request reviews
|
|
262
|
+
✓ Require status checks to pass
|
|
263
|
+
✓ Dismiss stale PR approvals
|
|
264
|
+
✓ Restrict pushes to (Admins only)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
See [HOOK-INSTALL.md](HOOK-INSTALL.md) for setup.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Scaling Characteristics
|
|
272
|
+
|
|
273
|
+
### Parallelism
|
|
274
|
+
|
|
275
|
+
- 5 agents in parallel = ~3x wall-clock speedup vs serial
|
|
276
|
+
- 8 agents in parallel = ~4x speedup (diminishing returns beyond 10 agents due to orchestrator overhead)
|
|
277
|
+
|
|
278
|
+
### Cost
|
|
279
|
+
|
|
280
|
+
- Haiku-first dispatch = ~1/5 cost per token vs Opus
|
|
281
|
+
- 5 Haiku + 1 Opus orchestrator = ~25% cost of all-Opus fleet
|
|
282
|
+
|
|
283
|
+
### Reliability
|
|
284
|
+
|
|
285
|
+
- Durable state (git-committed) = zero data loss on wipe
|
|
286
|
+
- Watchdog (heartbeat + respawn) = auto-recovery from hung agents
|
|
287
|
+
- Append-only logs = auditable, no corruption
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Next Steps
|
|
292
|
+
|
|
293
|
+
- **[HOW-THE-LOOP-WORKS.md](HOW-THE-LOOP-WORKS.md)** — Detailed wave cycle walkthrough
|
|
294
|
+
- **[DISPATCH-MODEL.md](DISPATCH-MODEL.md)** — Cost analysis and dispatch patterns
|
|
295
|
+
- **[CHECKPOINTING.md](CHECKPOINTING.md)** — STATE.md / BUILDLOG.md lifecycle
|
|
296
|
+
- **[FIRST-WAVE.md](FIRST-WAVE.md)** — Run your first wave and see the architecture in action
|