@matt82198/aesop 0.1.0-beta.5 → 0.1.0-rc.1

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +146 -5
  2. package/LICENSE +66 -21
  3. package/README.md +97 -33
  4. package/aesop.config.example.json +6 -0
  5. package/bin/CLAUDE.md +12 -3
  6. package/bin/cli.js +211 -40
  7. package/daemons/CLAUDE.md +1 -1
  8. package/daemons/backup-fleet.sh +209 -51
  9. package/daemons/run-watchdog.sh +208 -62
  10. package/dash/dash-extra.mjs +73 -4
  11. package/dash/watchdog-gui.sh +41 -35
  12. package/docs/ARCHITECTURE.md +296 -0
  13. package/docs/CONCEPTS.md +254 -0
  14. package/docs/CONFIGURE.md +256 -0
  15. package/docs/FIRST-WAVE.md +276 -0
  16. package/docs/INSTALL.md +224 -0
  17. package/docs/README.md +176 -27
  18. package/docs/autonomous-swe.md +149 -0
  19. package/docs/reproduce.md +121 -0
  20. package/hooks/CLAUDE.md +28 -0
  21. package/hooks/install-waveguard.sh +68 -0
  22. package/hooks/pre-commit-waveguard.sh +26 -0
  23. package/hooks/pre-push-policy.sh +253 -15
  24. package/monitor/CLAUDE.md +3 -3
  25. package/monitor/collect-signals.mjs +133 -20
  26. package/package.json +15 -7
  27. package/scan/CLAUDE.md +30 -0
  28. package/skills/CLAUDE.md +1 -0
  29. package/state_store/ingest.py +18 -1
  30. package/state_store/projections.py +78 -4
  31. package/state_store/store.py +102 -7
  32. package/tools/CLAUDE.md +25 -18
  33. package/tools/alert_bridge.py +14 -10
  34. package/tools/bench_runner.py +438 -0
  35. package/tools/buildlog.py +4 -7
  36. package/tools/ci_merge_wait.py +215 -34
  37. package/tools/common.py +62 -0
  38. package/tools/cost_ceiling.py +191 -0
  39. package/tools/dash.js +102 -0
  40. package/tools/doctor.js +220 -0
  41. package/tools/fleet_ledger.py +189 -17
  42. package/tools/halt.py +172 -0
  43. package/tools/healthcheck.py +24 -21
  44. package/tools/heartbeat.py +4 -7
  45. package/tools/metrics_gate.py +8 -2
  46. package/tools/orchestrator_status.py +4 -9
  47. package/tools/reconcile.py +277 -0
  48. package/tools/rotate_logs.py +152 -62
  49. package/tools/scanner_selftest.py +28 -3
  50. package/tools/secret_scan.py +359 -90
  51. package/tools/self_stats.py +292 -4
  52. package/tools/status.js +187 -0
  53. package/tools/verify_agent_inspector.py +289 -0
  54. package/tools/verify_prboard.py +344 -0
  55. package/tools/watch.js +49 -0
  56. package/ui/CLAUDE.md +2 -0
  57. package/ui/agents.py +332 -33
  58. package/ui/api/tracker.py +15 -7
  59. package/ui/collectors.py +125 -32
  60. package/ui/config.py +22 -2
  61. package/ui/cost.py +98 -3
  62. package/ui/csrf.py +2 -1
  63. package/ui/handler.py +209 -21
  64. package/ui/sse.py +102 -16
  65. package/ui/wave_prs.py +230 -0
  66. package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
  67. package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
  68. package/ui/web/dist/index.html +2 -2
  69. package/monitor/.signal-state.json +0 -3
  70. package/monitor/ACTIONS.log +0 -1
  71. package/monitor/BRIEF.md +0 -24
  72. package/monitor/SIGNALS.json +0 -54
  73. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  75. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  76. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  77. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  78. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  96. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  97. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  98. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  99. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  100. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  101. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  102. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  103. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  104. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  105. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  106. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  107. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  108. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  109. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  110. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  111. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  112. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  113. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  114. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  115. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  116. package/ui/web/.gitattributes +0 -13
  117. package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
  118. package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
  119. package/ui/web/index.html +0 -13
  120. package/ui/web/package-lock.json +0 -2225
  121. package/ui/web/package.json +0 -26
  122. package/ui/web/src/App.test.tsx +0 -74
  123. package/ui/web/src/App.tsx +0 -142
  124. package/ui/web/src/CONTRIBUTING-UI.md +0 -49
  125. package/ui/web/src/components/AgentRow.css +0 -187
  126. package/ui/web/src/components/AgentRow.test.tsx +0 -209
  127. package/ui/web/src/components/AgentRow.tsx +0 -207
  128. package/ui/web/src/components/AgentsPanel.css +0 -108
  129. package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
  130. package/ui/web/src/components/AgentsPanel.tsx +0 -58
  131. package/ui/web/src/components/AlertsPanel.css +0 -88
  132. package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
  133. package/ui/web/src/components/AlertsPanel.tsx +0 -67
  134. package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
  135. package/ui/web/src/components/BacklogPanel.tsx +0 -122
  136. package/ui/web/src/components/CostChart.css +0 -110
  137. package/ui/web/src/components/CostChart.test.tsx +0 -144
  138. package/ui/web/src/components/CostChart.tsx +0 -152
  139. package/ui/web/src/components/CostTable.css +0 -93
  140. package/ui/web/src/components/CostTable.test.tsx +0 -165
  141. package/ui/web/src/components/CostTable.tsx +0 -94
  142. package/ui/web/src/components/EventsFeed.css +0 -68
  143. package/ui/web/src/components/EventsFeed.test.tsx +0 -36
  144. package/ui/web/src/components/EventsFeed.tsx +0 -31
  145. package/ui/web/src/components/HealthHeader.css +0 -137
  146. package/ui/web/src/components/HealthHeader.test.tsx +0 -278
  147. package/ui/web/src/components/HealthHeader.tsx +0 -281
  148. package/ui/web/src/components/InboxForm.css +0 -135
  149. package/ui/web/src/components/InboxForm.test.tsx +0 -208
  150. package/ui/web/src/components/InboxForm.tsx +0 -116
  151. package/ui/web/src/components/MessagesTail.module.css +0 -144
  152. package/ui/web/src/components/MessagesTail.test.tsx +0 -176
  153. package/ui/web/src/components/MessagesTail.tsx +0 -94
  154. package/ui/web/src/components/ReposPanel.css +0 -90
  155. package/ui/web/src/components/ReposPanel.test.tsx +0 -45
  156. package/ui/web/src/components/ReposPanel.tsx +0 -67
  157. package/ui/web/src/components/Scorecard.css +0 -106
  158. package/ui/web/src/components/Scorecard.test.tsx +0 -117
  159. package/ui/web/src/components/Scorecard.tsx +0 -85
  160. package/ui/web/src/components/Timeline.module.css +0 -151
  161. package/ui/web/src/components/Timeline.test.tsx +0 -215
  162. package/ui/web/src/components/Timeline.tsx +0 -99
  163. package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
  164. package/ui/web/src/components/TrackerBoard.tsx +0 -107
  165. package/ui/web/src/components/TrackerCard.test.tsx +0 -180
  166. package/ui/web/src/components/TrackerCard.tsx +0 -160
  167. package/ui/web/src/components/TrackerForm.test.tsx +0 -189
  168. package/ui/web/src/components/TrackerForm.tsx +0 -144
  169. package/ui/web/src/lib/api.ts +0 -218
  170. package/ui/web/src/lib/format.test.ts +0 -89
  171. package/ui/web/src/lib/format.ts +0 -103
  172. package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
  173. package/ui/web/src/lib/sanitizeUrl.ts +0 -38
  174. package/ui/web/src/lib/types.ts +0 -230
  175. package/ui/web/src/lib/useHashRoute.test.ts +0 -60
  176. package/ui/web/src/lib/useHashRoute.ts +0 -23
  177. package/ui/web/src/lib/useSSE.ts +0 -175
  178. package/ui/web/src/main.tsx +0 -10
  179. package/ui/web/src/styles/global.css +0 -179
  180. package/ui/web/src/styles/theme.css +0 -184
  181. package/ui/web/src/styles/work.css +0 -572
  182. package/ui/web/src/test/fixtures.ts +0 -385
  183. package/ui/web/src/test/setup.ts +0 -49
  184. package/ui/web/src/views/Activity.module.css +0 -43
  185. package/ui/web/src/views/Activity.test.tsx +0 -89
  186. package/ui/web/src/views/Activity.tsx +0 -31
  187. package/ui/web/src/views/Cost.css +0 -87
  188. package/ui/web/src/views/Cost.test.tsx +0 -142
  189. package/ui/web/src/views/Cost.tsx +0 -54
  190. package/ui/web/src/views/Overview.css +0 -51
  191. package/ui/web/src/views/Overview.test.tsx +0 -76
  192. package/ui/web/src/views/Overview.tsx +0 -46
  193. package/ui/web/src/views/Work.test.tsx +0 -82
  194. package/ui/web/src/views/Work.tsx +0 -79
  195. package/ui/web/src/vite-env.d.ts +0 -10
  196. package/ui/web/tsconfig.json +0 -22
  197. package/ui/web/vite.config.ts +0 -25
  198. package/ui/web/vitest.config.ts +0 -12
@@ -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
- // Parse JSONL agent transcript and extract rich metadata
83
- function parseAgentJsonl(filePath) {
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
  }
@@ -114,45 +114,51 @@ render_frame() {
114
114
  echo -ne "$FRAME"
115
115
  }
116
116
 
117
- while true; do
118
- SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
119
- SPINNER_IDX=$(( SPINNER % 10 ))
120
- SPIN_CHAR="${SPINNER_CHARS:$SPINNER_IDX:1}"
121
- ((SPINNER++))
122
-
123
- now=$(date +%s)
124
- hb=$(cat "$HB" 2>/dev/null)
125
- case "$hb" in
126
- ''|*[!0-9]*) hb=0 ;;
127
- esac
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
- if [ "$hb" -gt 0 ]; then
130
- AGE=$(( now - hb ))
131
- else
132
- AGE=99999
133
- fi
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
- WD_THRESH=$(get_hb_threshold "watchdog")
136
- if [ "$AGE" -lt "$WD_THRESH" ]; then
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
- set -- $(awk '/^RESOLVED-FP/ { next } / HIGH / { hi++ } / MED / { me++ } END { print hi+0 " " me+0 }' "$SLOG" 2>/dev/null || echo '0 0')
143
- HI="$1"; ME="$2"
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
- if [ "$FIRST_FRAME" -eq 1 ]; then
146
- printf '\033[2J'
147
- FIRST_FRAME=0
148
- else
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
- render_frame | while IFS= read -r line; do
153
- printf '%s\033[K\n' "$line"
158
+ sleep 4
154
159
  done
155
- printf '\033[J'
160
+ }
156
161
 
157
- sleep 4
158
- done
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