@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- 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/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -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/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -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/agents.py +183 -0
- package/ui/api/__init__.py +84 -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/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
package/dash/dash-extra.mjs
CHANGED
|
@@ -1,18 +1,62 @@
|
|
|
1
1
|
// Detect and render running Claude agents from transcripts.
|
|
2
2
|
// Usage: node dash-extra.mjs [--json]
|
|
3
|
-
// --json: output JSON array of agents (for web dashboard
|
|
3
|
+
// --json: output JSON array of agents with rich metadata (for web dashboard)
|
|
4
4
|
// (default): render TUI text output (for terminal dashboard)
|
|
5
|
+
//
|
|
6
|
+
// Enhanced JSONL parsing: extracts dispatch prompt, token counts, runtime, task label
|
|
7
|
+
// Robust: tolerates malformed lines, caps read size on large files
|
|
5
8
|
|
|
6
9
|
import fs from 'node:fs';
|
|
7
10
|
import path from 'node:path';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
|
|
13
|
+
// Configuration: env var > config file > built-in default
|
|
14
|
+
// Helper: load aesop.config.json if it exists
|
|
15
|
+
function loadConfigFile(aesopRoot) {
|
|
16
|
+
try {
|
|
17
|
+
const configPath = path.join(aesopRoot, 'aesop.config.json');
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Parse error or file doesn't exist; ignore
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper: expand ~ and environment variables in paths for portability
|
|
28
|
+
function expandPath(pathStr) {
|
|
29
|
+
if (!pathStr) return pathStr;
|
|
30
|
+
// Expand ~ to home directory
|
|
31
|
+
if (pathStr.startsWith('~')) {
|
|
32
|
+
return path.join(os.homedir(), pathStr.slice(1));
|
|
33
|
+
}
|
|
34
|
+
// Expand environment variables like $VAR or %VAR%
|
|
35
|
+
return pathStr.replace(/\$\{?([A-Z_]+)\}?/gi, (match, varName) => {
|
|
36
|
+
return process.env[varName] || match;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
8
39
|
|
|
9
|
-
// Configuration from environment
|
|
10
40
|
const AESOP_ROOT = process.env.AESOP_ROOT || path.join(process.env.HOME || '.', 'aesop');
|
|
41
|
+
const config = loadConfigFile(AESOP_ROOT);
|
|
42
|
+
|
|
11
43
|
const TRANSCRIPTS_ROOT = path.resolve(
|
|
12
|
-
|
|
44
|
+
expandPath(
|
|
45
|
+
process.env.AESOP_TRANSCRIPTS_ROOT ||
|
|
46
|
+
config.transcripts_root ||
|
|
47
|
+
path.join(process.env.HOME || '.', '.claude', 'projects')
|
|
48
|
+
)
|
|
13
49
|
);
|
|
14
|
-
|
|
15
|
-
|
|
50
|
+
|
|
51
|
+
// Resolve state_root: env var > config > default
|
|
52
|
+
const STATE_ROOT = path.resolve(
|
|
53
|
+
expandPath(
|
|
54
|
+
process.env.AESOP_STATE_ROOT ||
|
|
55
|
+
config.state_root ||
|
|
56
|
+
path.join(AESOP_ROOT, 'state')
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
const ALERTS_LOG = path.join(STATE_ROOT, 'SECURITY-ALERTS.log');
|
|
16
60
|
|
|
17
61
|
// ANSI colors for TUI output
|
|
18
62
|
const c = {
|
|
@@ -29,6 +73,110 @@ const c = {
|
|
|
29
73
|
const now = Date.now();
|
|
30
74
|
const out = [];
|
|
31
75
|
|
|
76
|
+
// Activity window: only include files modified within the last 12 minutes
|
|
77
|
+
const ACTIVITY_WINDOW = 12 * 60 * 1000;
|
|
78
|
+
|
|
79
|
+
// Depth limit to match monitor's equivalent (prevent unbounded recursion on growing tree)
|
|
80
|
+
const MAX_DEPTH = 6;
|
|
81
|
+
|
|
82
|
+
// Parse JSONL agent transcript and extract rich metadata
|
|
83
|
+
function parseAgentJsonl(filePath) {
|
|
84
|
+
const metadata = {
|
|
85
|
+
promptFull: '',
|
|
86
|
+
taskLabel: '',
|
|
87
|
+
tokensUsed: 0,
|
|
88
|
+
startedAt: null,
|
|
89
|
+
lastActivity: null,
|
|
90
|
+
runtimeSeconds: 0,
|
|
91
|
+
project: ''
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Derive project from path: transcripts_root/PROJECT/session/...
|
|
96
|
+
const relPath = path.relative(TRANSCRIPTS_ROOT, filePath);
|
|
97
|
+
const parts = relPath.split(path.sep);
|
|
98
|
+
if (parts.length > 0) {
|
|
99
|
+
metadata.project = parts[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
|
|
105
|
+
// Cap reading: first 50 + last 50 lines to avoid expensive parsing on huge files
|
|
106
|
+
const linesToRead = [];
|
|
107
|
+
if (lines.length <= 100) {
|
|
108
|
+
linesToRead.push(...lines);
|
|
109
|
+
} else {
|
|
110
|
+
linesToRead.push(...lines.slice(0, 50));
|
|
111
|
+
linesToRead.push(...lines.slice(-50));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let firstTimestamp = null;
|
|
115
|
+
let lastTimestamp = null;
|
|
116
|
+
|
|
117
|
+
for (const line of linesToRead) {
|
|
118
|
+
if (!line.trim()) continue;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const obj = JSON.parse(line);
|
|
122
|
+
|
|
123
|
+
// Extract dispatch prompt from first user message
|
|
124
|
+
if (obj.type === 'user' && obj.message && !metadata.promptFull) {
|
|
125
|
+
const content = obj.message.content;
|
|
126
|
+
if (typeof content === 'string') {
|
|
127
|
+
metadata.promptFull = content;
|
|
128
|
+
// Extract task label: first line of prompt, capped at 80 chars
|
|
129
|
+
const firstLine = content.split('\n')[0];
|
|
130
|
+
metadata.taskLabel = firstLine.substring(0, 80);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Track timestamps
|
|
135
|
+
if (obj.timestamp) {
|
|
136
|
+
const ts = new Date(obj.timestamp).getTime();
|
|
137
|
+
if (!firstTimestamp) {
|
|
138
|
+
firstTimestamp = ts;
|
|
139
|
+
metadata.startedAt = obj.timestamp;
|
|
140
|
+
}
|
|
141
|
+
lastTimestamp = ts;
|
|
142
|
+
metadata.lastActivity = obj.timestamp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// (token accumulation is a separate FULL-file pass below — sampling
|
|
146
|
+
// only 100 lines under-counts tokens badly on long transcripts)
|
|
147
|
+
} catch {
|
|
148
|
+
// Silently skip malformed JSON lines
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Token total: scan EVERY line (usage records are spread throughout the
|
|
154
|
+
// transcript, not just the sampled head/tail). Cheap substring pre-filter
|
|
155
|
+
// keeps this fast on huge files by JSON-parsing only candidate lines.
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (!line.includes('"usage"')) continue;
|
|
158
|
+
try {
|
|
159
|
+
const obj = JSON.parse(line);
|
|
160
|
+
if (obj.type === 'assistant' && obj.usage) {
|
|
161
|
+
const { input_tokens = 0, output_tokens = 0 } = obj.usage;
|
|
162
|
+
metadata.tokensUsed += input_tokens + output_tokens;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Calculate runtime in seconds
|
|
170
|
+
if (firstTimestamp && lastTimestamp) {
|
|
171
|
+
metadata.runtimeSeconds = Math.floor((lastTimestamp - firstTimestamp) / 1000);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// If file read fails, return empty metadata
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return metadata;
|
|
178
|
+
}
|
|
179
|
+
|
|
32
180
|
// Read alerts log if present
|
|
33
181
|
let slog = [];
|
|
34
182
|
try {
|
|
@@ -37,14 +185,22 @@ try {
|
|
|
37
185
|
}
|
|
38
186
|
} catch {}
|
|
39
187
|
|
|
40
|
-
// Recursively walk directory tree to find agent-*.jsonl files
|
|
41
|
-
|
|
188
|
+
// Recursively walk directory tree to find agent-*.jsonl files.
|
|
189
|
+
// Respects depth limit. Activity window filtering happens per-file, not per-directory,
|
|
190
|
+
// to avoid hiding active agents in dirs whose mtime was frozen at creation.
|
|
191
|
+
function walk(dir, accumulator, depth = 0) {
|
|
192
|
+
// Stop recursion if depth exceeds limit
|
|
193
|
+
if (depth > MAX_DEPTH) return;
|
|
194
|
+
|
|
42
195
|
try {
|
|
43
196
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
44
197
|
for (const entry of entries) {
|
|
45
198
|
const fullPath = path.join(dir, entry.name);
|
|
46
199
|
if (entry.isDirectory()) {
|
|
47
|
-
|
|
200
|
+
// Always descend into subdirectories (respecting depth limit only)
|
|
201
|
+
// Do NOT prune based on directory mtime, as directories don't update
|
|
202
|
+
// when files inside are appended to.
|
|
203
|
+
walk(fullPath, accumulator, depth + 1);
|
|
48
204
|
} else if (/^agent-.*\.jsonl$/.test(entry.name)) {
|
|
49
205
|
accumulator.push(fullPath);
|
|
50
206
|
}
|
|
@@ -68,8 +224,13 @@ files = files
|
|
|
68
224
|
return { f, mtime };
|
|
69
225
|
})
|
|
70
226
|
.filter(x => now - x.mtime < 12 * 60 * 1000)
|
|
71
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
72
|
-
|
|
227
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
228
|
+
|
|
229
|
+
// True active total. The 8 below is a TUI terminal-space limit ONLY — not the
|
|
230
|
+
// real count. The web (--json) emits ALL active agents; the TUI shows 8 + "N more".
|
|
231
|
+
const totalActive = files.length;
|
|
232
|
+
const runningCount = files.filter(x => now - x.mtime < 120 * 1000).length;
|
|
233
|
+
const TUI_ROW_CAP = 8;
|
|
73
234
|
|
|
74
235
|
// Extract description/hint from first ~60KB of agent transcript
|
|
75
236
|
function label(filePath) {
|
|
@@ -93,14 +254,13 @@ function label(filePath) {
|
|
|
93
254
|
// TUI output: render heading
|
|
94
255
|
out.push(`${c.B} FLEET AGENTS${c.X} ${c.D}(green=running · severity-colored if flagged)${c.X}`);
|
|
95
256
|
|
|
96
|
-
if (
|
|
257
|
+
if (totalActive === 0) {
|
|
97
258
|
out.push(` ${c.D}(no active fleet agents in last 12 min)${c.X}`);
|
|
98
259
|
}
|
|
99
260
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const { f, mtime } of files) {
|
|
261
|
+
// Render each agent (TUI caps rows to TUI_ROW_CAP for terminal space; the counts
|
|
262
|
+
// above are the true totals across all active agents)
|
|
263
|
+
for (const { f, mtime } of files.slice(0, TUI_ROW_CAP)) {
|
|
104
264
|
const basename = path.basename(f);
|
|
105
265
|
const ageSeconds = Math.round((now - mtime) / 1000);
|
|
106
266
|
|
|
@@ -110,8 +270,6 @@ for (const { f, mtime } of files) {
|
|
|
110
270
|
let statusColor = ageSeconds < 120 ? c.G : c.D;
|
|
111
271
|
let statusText = ageSeconds < 120 ? 'running' : 'idle';
|
|
112
272
|
|
|
113
|
-
if (ageSeconds < 120) runningCount++;
|
|
114
|
-
|
|
115
273
|
// Recolor based on alert severity
|
|
116
274
|
if (alertsForAgent.some(l => l.includes('SUSPICIOUS'))) {
|
|
117
275
|
statusColor = c.R;
|
|
@@ -140,8 +298,12 @@ for (const { f, mtime } of files) {
|
|
|
140
298
|
);
|
|
141
299
|
}
|
|
142
300
|
|
|
143
|
-
if (
|
|
144
|
-
out.push(` ${c.D}
|
|
301
|
+
if (totalActive > TUI_ROW_CAP) {
|
|
302
|
+
out.push(` ${c.D}… +${totalActive - TUI_ROW_CAP} more active (showing ${TUI_ROW_CAP} of ${totalActive})${c.X}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (totalActive > 0) {
|
|
306
|
+
out.push(` ${c.D}${runningCount} running, ${totalActive - runningCount} idle (last 12 min)${c.X}`);
|
|
145
307
|
}
|
|
146
308
|
|
|
147
309
|
// Output
|
|
@@ -170,11 +332,22 @@ if (process.argv.includes('--json')) {
|
|
|
170
332
|
.replace(/\.jsonl$/, '')
|
|
171
333
|
.slice(0, 13);
|
|
172
334
|
|
|
335
|
+
// Parse JSONL for rich metadata
|
|
336
|
+
const metadata = parseAgentJsonl(f);
|
|
337
|
+
|
|
173
338
|
agents.push({
|
|
174
339
|
id: agentId,
|
|
175
|
-
|
|
340
|
+
project: metadata.project,
|
|
176
341
|
status: status,
|
|
177
|
-
|
|
342
|
+
age_s: ageSeconds,
|
|
343
|
+
hint: label(f).slice(0, 60),
|
|
344
|
+
// New enriched fields
|
|
345
|
+
startedAt: metadata.startedAt,
|
|
346
|
+
lastActivity: metadata.lastActivity,
|
|
347
|
+
runtimeSeconds: metadata.runtimeSeconds,
|
|
348
|
+
tokensUsed: metadata.tokensUsed,
|
|
349
|
+
taskLabel: metadata.taskLabel,
|
|
350
|
+
promptFull: metadata.promptFull
|
|
178
351
|
});
|
|
179
352
|
}
|
|
180
353
|
process.stdout.write(JSON.stringify(agents) + '\n');
|
package/dash/watchdog-gui.sh
CHANGED
|
@@ -21,6 +21,7 @@ SLOG="$AESOP_ROOT/state/SECURITY-ALERTS.log"
|
|
|
21
21
|
HB="$AESOP_ROOT/state/.watchdog-heartbeat"
|
|
22
22
|
REPOS_FILE="$AESOP_ROOT/state/.watchdog-repos.json"
|
|
23
23
|
HB_DIR="$AESOP_ROOT/state/.heartbeats"
|
|
24
|
+
CONFIG_FILE="$AESOP_ROOT/aesop.config.json"
|
|
24
25
|
|
|
25
26
|
SPINNER=0
|
|
26
27
|
FIRST_FRAME=1
|
|
@@ -29,15 +30,31 @@ FIRST_FRAME=1
|
|
|
29
30
|
trap 'printf "\033[?25h"; exit 0' INT TERM
|
|
30
31
|
printf '\033[?25l'
|
|
31
32
|
|
|
33
|
+
# Load heartbeat thresholds from config; fallback to built-in defaults
|
|
34
|
+
load_hb_thresholds() {
|
|
35
|
+
if [ -f "$CONFIG_FILE" ] && command -v jq >/dev/null 2>&1; then
|
|
36
|
+
HB_THRESHOLD_MONITOR=$(jq -r '.monitor.heartbeat_thresholds.monitor // 3600' "$CONFIG_FILE" 2>/dev/null || echo 3600)
|
|
37
|
+
HB_THRESHOLD_WATCHDOG=$(jq -r '.monitor.heartbeat_thresholds.watchdog // 300' "$CONFIG_FILE" 2>/dev/null || echo 300)
|
|
38
|
+
HB_THRESHOLD_DEFAULT=$(jq -r '.monitor.heartbeat_thresholds.default // 1800' "$CONFIG_FILE" 2>/dev/null || echo 1800)
|
|
39
|
+
else
|
|
40
|
+
HB_THRESHOLD_MONITOR=3600
|
|
41
|
+
HB_THRESHOLD_WATCHDOG=300
|
|
42
|
+
HB_THRESHOLD_DEFAULT=1800
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
get_hb_threshold() {
|
|
33
47
|
local name="$1"
|
|
34
48
|
case "$name" in
|
|
35
|
-
*monitor*) echo
|
|
36
|
-
*watchdog*) echo
|
|
37
|
-
*) echo
|
|
49
|
+
*monitor*) echo "$HB_THRESHOLD_MONITOR";;
|
|
50
|
+
*watchdog*) echo "$HB_THRESHOLD_WATCHDOG";;
|
|
51
|
+
*) echo "$HB_THRESHOLD_DEFAULT";;
|
|
38
52
|
esac
|
|
39
53
|
}
|
|
40
54
|
|
|
55
|
+
# Load thresholds at startup
|
|
56
|
+
load_hb_thresholds
|
|
57
|
+
|
|
41
58
|
render_frame() {
|
|
42
59
|
local FRAME=""
|
|
43
60
|
FRAME="${FRAME}${B}${C}== FLEET WATCHDOG${X}\n"
|
|
@@ -122,8 +139,8 @@ while true; do
|
|
|
122
139
|
WD="${R}STALE${X}"
|
|
123
140
|
fi
|
|
124
141
|
|
|
125
|
-
|
|
126
|
-
|
|
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"
|
|
127
144
|
|
|
128
145
|
if [ "$FIRST_FRAME" -eq 1 ]; then
|
|
129
146
|
printf '\033[2J'
|
|
@@ -133,7 +150,7 @@ while true; do
|
|
|
133
150
|
fi
|
|
134
151
|
|
|
135
152
|
render_frame | while IFS= read -r line; do
|
|
136
|
-
printf '%
|
|
153
|
+
printf '%s\033[K\n' "$line"
|
|
137
154
|
done
|
|
138
155
|
printf '\033[J'
|
|
139
156
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Behavioral PR Review Checklist
|
|
2
|
+
|
|
3
|
+
For pull requests that modify operational rules, agent behavior, monitoring, or orchestration policy, follow this checklist **before merging**.
|
|
4
|
+
|
|
5
|
+
## 1. Rule & Cardinal Alignment
|
|
6
|
+
|
|
7
|
+
- [ ] Rule named explicitly (e.g., "Rule 3: Reliability core" or "Cardinal Rule 1: Dispatch model")
|
|
8
|
+
- [ ] PR title and description clearly state which rule(s) are affected
|
|
9
|
+
- [ ] Change aligns with (or explicitly updates) `docs/CARDINAL-RULES.md`
|
|
10
|
+
- [ ] No contradictions with other cardinal rules or guardrails
|
|
11
|
+
|
|
12
|
+
## 2. Blast Radius & Impact
|
|
13
|
+
|
|
14
|
+
- [ ] Impact scope documented (e.g., "affects all new monitor cycles", "affects heartbeat thresholds only", "no impact on running agents")
|
|
15
|
+
- [ ] Backward compatibility assessed (breaking changes require migration plan)
|
|
16
|
+
- [ ] Dependent systems identified (e.g., if monitor behavior changes, does ACTIONS.log parsing break?)
|
|
17
|
+
- [ ] Risk of silent failures assessed (e.g., does fallback-to-default mask issues?)
|
|
18
|
+
|
|
19
|
+
## 3. Verification & Testing
|
|
20
|
+
|
|
21
|
+
- [ ] Reproducible test steps provided (not "tested locally" — state what was tested)
|
|
22
|
+
- [ ] Test output attached or described (e.g., "ran `node monitor/collect-signals.mjs` twice, no duplicate PROPOSALS.md entries")
|
|
23
|
+
- [ ] Idempotency verified if applicable (safe to run multiple times in same cycle?)
|
|
24
|
+
- [ ] Edge cases covered (what if config missing? what if file unreadable?)
|
|
25
|
+
|
|
26
|
+
## 4. Rollback & Recovery
|
|
27
|
+
|
|
28
|
+
- [ ] Rollback plan documented (e.g., "revert commit + clear `.signal-state.json` + restart monitor")
|
|
29
|
+
- [ ] Rollback tested if possible (e.g., apply change, verify it works, revert, verify rollback works)
|
|
30
|
+
- [ ] No irreversible side effects (don't delete files without append-only logging first)
|
|
31
|
+
|
|
32
|
+
## 5. Single-Writer & Proposal Provenance
|
|
33
|
+
|
|
34
|
+
- [ ] If PROPOSALS.md or ACTIONS.log touched: confirm monitor still writes atomically (append-only)
|
|
35
|
+
- [ ] If new proposal emission logic added: confirm idempotency (no duplicate proposals from re-runs)
|
|
36
|
+
- [ ] If proposal came from monitor-collected signal: CHARTER.md action tier justified (AUTO vs PROPOSE)
|
|
37
|
+
- [ ] If proposal came from human: documented in PROPOSALS.md (what human, when, why)
|
|
38
|
+
|
|
39
|
+
## 6. Documentation & Handoff
|
|
40
|
+
|
|
41
|
+
- [ ] Related doc updated (CHARTER.md, CARDINAL-RULES.md, GOVERNANCE.md, etc.)
|
|
42
|
+
- [ ] Code comments added for non-obvious rule enforcement logic
|
|
43
|
+
- [ ] Affected stakeholders notified (e.g., if monitor behavior changes, note in CONTRIBUTING.md)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Worked Example: Cost-cap-per-repo Change
|
|
48
|
+
|
|
49
|
+
**PR Title:** `feat(dispatch): Enforce Haiku cost cap per-repo instead of global`
|
|
50
|
+
|
|
51
|
+
**Rule affected:** Cardinal Rule 1 (dispatch model), Rule 6 (commit discipline)
|
|
52
|
+
|
|
53
|
+
**Blast radius:** All new agent dispatches via claude-code-cli; existing running agents unaffected
|
|
54
|
+
|
|
55
|
+
**Verification:**
|
|
56
|
+
- Ran dispatch-test-harness.sh with 3 mock repos, 5 Haiku spawns per repo
|
|
57
|
+
- Confirmed ledger recorded per-repo cost separately
|
|
58
|
+
- Rolled back, confirmed ledger reverted to single global entry
|
|
59
|
+
- No test script in package.json; test via integration harness
|
|
60
|
+
|
|
61
|
+
**Rollback:** Revert commit, clear `~/.claude/projects/*/FLEET-LEDGER.md`, restart orchestrator
|
|
62
|
+
|
|
63
|
+
**Documentation:** Updated DISPATCH-MODEL.md section "Cost tracking" with new per-repo tracking format
|
|
64
|
+
|
|
65
|
+
**Result:** ✓ Merged after review
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## When to Request Changes (🔴 Critical)
|
|
70
|
+
|
|
71
|
+
- Missing verification or test steps
|
|
72
|
+
- Rollback plan untested or implausible
|
|
73
|
+
- Proposal came from monitor but action tier (AUTO/PROPOSE) unjustified
|
|
74
|
+
- Change introduces hardcoded paths, secrets, or usernames
|
|
75
|
+
- Backward-incompatible change with no migration documented
|
|
76
|
+
- Cardinal rule contradiction or guardrail violation
|
|
77
|
+
|
|
78
|
+
## When to Suggest (💡 Consider)
|
|
79
|
+
|
|
80
|
+
- Minor documentation clarification
|
|
81
|
+
- Additional edge case to test (not required, but good to consider)
|
|
82
|
+
- Related code that could benefit from similar fix (out of scope for this PR, open separate issue)
|
|
83
|
+
|
|
84
|
+
## Proposal lifecycle
|
|
85
|
+
|
|
86
|
+
Use `node tools/proposals.mjs list` to review pending proposals in monitor/PROPOSALS.md. To approve a proposal, run `node tools/proposals.mjs accept <signal-key>` (moves block to PROPOSALS-LOG.md under `## ACCEPTED <timestamp>`); to decline, run `reject <signal-key>` instead. Both commands are idempotent: re-running on an already-moved proposal is a no-op.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Durable Checkpointing & State Management
|
|
2
|
+
|
|
3
|
+
**TL;DR**: Maintain STATE.md (intent, phase, NEXT STEPS) + BUILDLOG.md (append-only progress log). On resume after context loss, read both, verify from disk, then proceed. Survives session wipes, enables rapid context handoff, and keeps orchestrator cheap (no re-reading full history).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## STATE.md: Authoritative intent & decisions
|
|
8
|
+
|
|
9
|
+
**Purpose**: Single source of truth for current intent, locked decisions, data model/contracts, key file paths, current phase, and explicit NEXT STEPS.
|
|
10
|
+
|
|
11
|
+
**Structure**:
|
|
12
|
+
- What we're trying to build (intent, not mechanism)
|
|
13
|
+
- Locked decisions (technology choices, data models, architecture trade-offs)
|
|
14
|
+
- Current phase (e.g., "Phase 2: API integration")
|
|
15
|
+
- Explicit NEXT STEPS (enumerated, actionable, owner assigned)
|
|
16
|
+
- Key paths (file locations, service boundaries, data contracts)
|
|
17
|
+
|
|
18
|
+
**Discipline**: Hand-update STATE.md at phase boundaries. Never leave it stale; always update BEFORE moving to the next phase. An out-of-date STATE.md is worse than no STATE.md (it's actively misleading).
|
|
19
|
+
|
|
20
|
+
**Recovery use case**: If context is lost or a session is interrupted, STATE.md is the first file to read. It answers "where were we?" and "what's next?" without re-reading logs or transcripts.
|
|
21
|
+
|
|
22
|
+
## BUILDLOG.md: Append-only progress snapshots
|
|
23
|
+
|
|
24
|
+
**Purpose**: Timestamped, immutable record of progress. Never overwrite or edit earlier entries (append-only discipline).
|
|
25
|
+
|
|
26
|
+
**Pattern**: Each entry contains:
|
|
27
|
+
- Timestamp (ISO 8601 or human-readable)
|
|
28
|
+
- Work completed (features, bugs, integrations, tests)
|
|
29
|
+
- Status (green / yellow / blocked / pending decision)
|
|
30
|
+
- Next step or blocker (for orchestrator reference)
|
|
31
|
+
|
|
32
|
+
**Example**:
|
|
33
|
+
```
|
|
34
|
+
[2026-07-11 13:00] Phase 1 complete: scaffolding + unit tests passing
|
|
35
|
+
[2026-07-11 14:30] API integration: 2 endpoints done, 1 blocked on schema review
|
|
36
|
+
[2026-07-11 15:45] BLOCKED: waiting for design review on request body format
|
|
37
|
+
[2026-07-11 16:20] Design approved; resume API endpoint 3 implementation
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Append-only rule**: Agents and loops append one line per work unit. The orchestrator never edits earlier entries. This prevents accidental data loss and keeps the log auditable.
|
|
41
|
+
|
|
42
|
+
## Recovery workflow: On context loss or resume
|
|
43
|
+
|
|
44
|
+
When you resume work (context wipe, new session, task restart), follow this protocol:
|
|
45
|
+
|
|
46
|
+
1. **Read STATE.md**: Understand intent, locked decisions, and current phase
|
|
47
|
+
2. **Skim BUILDLOG.md**: Review recent progress and current blockers
|
|
48
|
+
3. **Run git log**: Verify actual file state and recent commits vs. STATE.md
|
|
49
|
+
4. **Sync**: If STATE.md is stale, update it before proceeding; if reality diverged from STATE.md, note the delta and surface it
|
|
50
|
+
|
|
51
|
+
**Never invent state.** Verify everything from disk (git log, file timestamps, handoff files) before trusting in-memory recollection. Disk is the source of truth.
|
|
52
|
+
|
|
53
|
+
## Dated archive strategy: Log rotation
|
|
54
|
+
|
|
55
|
+
When BUILDLOG.md grows beyond **~200 lines or ~20 KB**:
|
|
56
|
+
|
|
57
|
+
1. Identify the cut point (e.g., last entry from yesterday or the last "green" state marker)
|
|
58
|
+
2. Move older entries to `BUILDLOG-YYYY-MM.md` (dated archive)
|
|
59
|
+
3. Keep only recent entries in the live `BUILDLOG.md`
|
|
60
|
+
4. Commit both files
|
|
61
|
+
5. Update any orchestrator references to point to the live file
|
|
62
|
+
|
|
63
|
+
**Why rotation?** The orchestrator reads the live BUILDLOG.md frequently; a 50-line log is faster and cheaper than 500 lines. Full history remains available in archives for auditing and debugging.
|
|
64
|
+
|
|
65
|
+
**Example**:
|
|
66
|
+
- Live file: `BUILDLOG.md` (entries from 2026-07-08 onward)
|
|
67
|
+
- Archive: `BUILDLOG-2026-07.md` (entries through 2026-07-07)
|
|
68
|
+
|
|
69
|
+
## Handoff metadata across team boundaries
|
|
70
|
+
|
|
71
|
+
When handing off work to another team member or agent:
|
|
72
|
+
|
|
73
|
+
- Include the git commit hash (anchors history)
|
|
74
|
+
- Snapshot key file paths and their state
|
|
75
|
+
- Document any external dependencies or waiting states
|
|
76
|
+
- List any environment assumptions (database schema versions, external service versions, etc.)
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
```
|
|
80
|
+
[2026-07-12 09:00] Handing off to feature-team: commit abc1234
|
|
81
|
+
- Database schema: v3.2 (see db/migrations/)
|
|
82
|
+
- API contracts frozen in openapi.yaml
|
|
83
|
+
- Waiting for design review on component layout
|
|
84
|
+
- Next: implement sign-up form based on design feedback
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Multi-session coordination
|
|
88
|
+
|
|
89
|
+
For work spanning multiple people or sessions:
|
|
90
|
+
|
|
91
|
+
1. **Orchestrator updates STATE.md** after each major decision or phase change
|
|
92
|
+
2. **Subagents append to BUILDLOG.md** after each work unit
|
|
93
|
+
3. **On session resume**, orchestrator syncs from disk and briefs the incoming worker
|
|
94
|
+
4. **Hand-updates to STATE.md** happen only at phase boundaries (not every commit)
|
|
95
|
+
|
|
96
|
+
This keeps overhead low (one STATE.md update per phase, one BUILDLOG line per work unit) while preserving a clear audit trail.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
**Why STATE.md + BUILDLOG.md?** They ensure work survives context loss, enables rapid resume without re-reading history, and provides an audit trail for debugging. Together with git history and the heartbeat protocol, they form the **checkpointing core** that makes orchestration systems reliable and recoverable.
|
package/docs/DISPATCH-MODEL.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Aesop Dispatch Model — Cost & Orchestration Patterns
|
|
2
2
|
|
|
3
|
+
**TL;DR**: Spawn 5–8 cheap Haiku subagents in parallel (not serial Opus). Result: **lower cost (Haiku ~1/3 Sonnet) plus a parallel speedup** via parallelism. Rule: subagents ALWAYS Haiku unless scoped work genuinely exceeds its capability (rare).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
3
7
|
## The fable conceit
|
|
4
8
|
|
|
5
9
|
In Aesop's Fables, Aesop (the narrator) directs the characters (tortoise, hare, fox, etc.) through moral tales. Here, **Aesop (the orchestrator)** directs a fleet of **Fables (the subagents)** toward reliable task completion.
|
|
@@ -21,7 +25,7 @@ The tortoise represents deliberate, resourceful thinking; the hare, quick speed.
|
|
|
21
25
|
1 orchestrator (Opus, 5 min thinking) = 5 × 60 = 300 tokens
|
|
22
26
|
5 subagents (Haiku, 2 min each) = 5 × 2 × 20 = 200 tokens
|
|
23
27
|
Total = 500 tokens/session
|
|
24
|
-
=
|
|
28
|
+
= a large saving vs all-Opus (Haiku is 1/5 the per-token cost of Opus)
|
|
25
29
|
```
|
|
26
30
|
|
|
27
31
|
### Scaling to 10 parallel domains
|
|
@@ -30,14 +34,14 @@ Total = 500 tokens/session
|
|
|
30
34
|
1 orchestrator (Opus, 8 min) = 480 tokens
|
|
31
35
|
10 subagents (Haiku, 3 min each) = 600 tokens
|
|
32
36
|
Total = 1,080 tokens
|
|
33
|
-
=
|
|
37
|
+
= a large saving vs all-Opus (most domains run on Haiku at 1/5 Opus cost)
|
|
34
38
|
```
|
|
35
39
|
|
|
36
40
|
## MAIN RULE: Subagents are ALWAYS the cheap tier
|
|
37
41
|
|
|
38
42
|
This is **the single most important cost lever** in the entire system. Every subagent spawned must default to Haiku, never Sonnet or Opus. This rule, more than any other, multiplies your savings at scale.
|
|
39
43
|
|
|
40
|
-
**Why**: Haiku is 1/3 the cost of Sonnet with sufficient capability for scoped domain work. Scaling from 1 subagent to 6–8 in parallel multiplies the per-domain savings —
|
|
44
|
+
**Why**: Haiku is 1/3 the cost of Sonnet with sufficient capability for scoped domain work. Scaling from 1 subagent to 6–8 in parallel multiplies the per-domain savings — most of the work runs on Haiku at 1/5 the per-token cost of Opus. Violating this rule (spawning Sonnet/Opus subagents) erases the economic advantage of the entire dispatch model.
|
|
41
45
|
|
|
42
46
|
**The exception**: Only use Sonnet/Opus for a subagent if:
|
|
43
47
|
1. The task genuinely exceeds Haiku capability (rare for scoped domains), **AND**
|
|
@@ -79,7 +83,7 @@ Orchestrator → Phase 1
|
|
|
79
83
|
└─ Haiku-4: Integration test
|
|
80
84
|
|
|
81
85
|
Orchestrator reassembles + QA
|
|
82
|
-
Total cost:
|
|
86
|
+
Total cost: 1 Opus orchestrator + 4 Haiku implementers — the four implementers run at 1/5 the per-token cost of Opus
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
### Pattern 3: Hierarchical (supervisor + workers)
|
|
@@ -96,7 +100,7 @@ Orchestrator
|
|
|
96
100
|
│ └─ Haiku-5: config module
|
|
97
101
|
└─ (Sonnet synthesizes reviews into final report)
|
|
98
102
|
|
|
99
|
-
Cost: 1 Opus + 1 Sonnet + 5 Haiku =
|
|
103
|
+
Cost: 1 Opus + 1 Sonnet + 5 Haiku = ~1/3 the cost of running every domain on Opus
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
## Decision flowchart
|
|
@@ -118,7 +122,7 @@ Is this task scoped to <5 min reasoning?
|
|
|
118
122
|
|
|
119
123
|
### ❌ All-Opus fleet
|
|
120
124
|
|
|
121
|
-
**Cost**:
|
|
125
|
+
**Cost**: higher — Sonnet/Opus per-token rates. **Why**: throws CPU power at problems that are scoped.
|
|
122
126
|
|
|
123
127
|
**Fix**: Decompose into Haiku-sized tasks.
|
|
124
128
|
|
|
@@ -204,4 +208,4 @@ Surfacing to the user after 3 retries ensures visibility and prevents silent cos
|
|
|
204
208
|
|
|
205
209
|
---
|
|
206
210
|
|
|
207
|
-
**Summary**: Aesop's dispatch model trades a small amount of orchestration complexity for **
|
|
211
|
+
**Summary**: Aesop's dispatch model trades a small amount of orchestration complexity for **lower cost** (Haiku ~1/3 Sonnet) and **parallel speedup** (via parallelism). It works by keeping subagents small, orchestrator lean, and feedback constant.
|