@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/tools/doctor.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Aesop doctor — preflight checklist for adopter onboarding
|
|
5
|
+
*
|
|
6
|
+
* Runs diagnostic checks and prints a readiness table.
|
|
7
|
+
* Exit code 0 = all checks passed; 1 = at least one failed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
const net = require('net');
|
|
14
|
+
|
|
15
|
+
const CURRENT_DIR = process.cwd();
|
|
16
|
+
|
|
17
|
+
// ANSI color helpers
|
|
18
|
+
const COLORS = {
|
|
19
|
+
GREEN: '\x1b[32m',
|
|
20
|
+
RED: '\x1b[31m',
|
|
21
|
+
RESET: '\x1b[0m',
|
|
22
|
+
BOLD: '\x1b[1m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function colorPass() {
|
|
26
|
+
return `${COLORS.GREEN}✓ PASS${COLORS.RESET}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function colorFail() {
|
|
30
|
+
return `${COLORS.RED}✗ FAIL${COLORS.RESET}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check Node.js version >= 18
|
|
34
|
+
function checkNodeVersion() {
|
|
35
|
+
const version = parseInt(process.versions.node.split('.')[0], 10);
|
|
36
|
+
const passed = version >= 18;
|
|
37
|
+
const hint = passed ? '' : `Found Node.js v${process.versions.node}, need >=18`;
|
|
38
|
+
return { passed, hint };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check Python available (python3 or python)
|
|
42
|
+
function checkPython() {
|
|
43
|
+
// Try python3 first
|
|
44
|
+
const result3 = spawnSync('python3', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
45
|
+
if (result3.error && result3.error.code === 'ENOENT') {
|
|
46
|
+
// python3 not found, try python fallback
|
|
47
|
+
const result = spawnSync('python', ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
48
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
49
|
+
// Neither python3 nor python found
|
|
50
|
+
return { passed: false, hint: 'python3 or python not found on PATH' };
|
|
51
|
+
}
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
// python exists but returned non-zero exit code
|
|
54
|
+
return { passed: false, hint: 'python found but returned non-zero exit code' };
|
|
55
|
+
}
|
|
56
|
+
return { passed: true, hint: '' };
|
|
57
|
+
}
|
|
58
|
+
if (result3.status !== 0) {
|
|
59
|
+
// python3 exists but returned non-zero exit code
|
|
60
|
+
return { passed: false, hint: 'python3 found but returned non-zero exit code' };
|
|
61
|
+
}
|
|
62
|
+
return { passed: true, hint: '' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check git repo (.git directory exists)
|
|
66
|
+
function checkGitRepo() {
|
|
67
|
+
const gitDir = path.join(CURRENT_DIR, '.git');
|
|
68
|
+
const passed = fs.existsSync(gitDir);
|
|
69
|
+
const hint = passed ? '' : 'Not inside a git repository';
|
|
70
|
+
return { passed, hint };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check aesop.config.json exists and is valid JSON
|
|
74
|
+
function checkConfig() {
|
|
75
|
+
const configPath = path.join(CURRENT_DIR, 'aesop.config.json');
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(configPath)) {
|
|
78
|
+
return { passed: false, hint: 'aesop.config.json not found' };
|
|
79
|
+
}
|
|
80
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
81
|
+
JSON.parse(content);
|
|
82
|
+
return { passed: true, hint: '' };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { passed: false, hint: `Config parse error: ${e.message}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check required directories exist
|
|
89
|
+
function checkDirectories() {
|
|
90
|
+
const requiredDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui'];
|
|
91
|
+
const missing = requiredDirs.filter(dir => {
|
|
92
|
+
const dirPath = path.join(CURRENT_DIR, dir);
|
|
93
|
+
return !fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (missing.length === 0) {
|
|
97
|
+
return { passed: true, hint: '' };
|
|
98
|
+
} else {
|
|
99
|
+
return { passed: false, hint: `Missing: ${missing.join(', ')}` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check git pre-push hook installed
|
|
104
|
+
function checkPrePushHook() {
|
|
105
|
+
const hookPath = path.join(CURRENT_DIR, '.git', 'hooks', 'pre-push');
|
|
106
|
+
const passed = fs.existsSync(hookPath);
|
|
107
|
+
const hint = passed ? '' : 'Pre-push hook not installed at .git/hooks/pre-push';
|
|
108
|
+
return { passed, hint };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check if port 8770 is free (using socket connection test)
|
|
112
|
+
function checkPort8770() {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let resolved = false;
|
|
115
|
+
const sock = net.createConnection({ port: 8770, host: '127.0.0.1', timeout: 500 });
|
|
116
|
+
|
|
117
|
+
const cleanup = () => {
|
|
118
|
+
try {
|
|
119
|
+
sock.destroy();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Ignore cleanup errors
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
sock.on('connect', () => {
|
|
126
|
+
if (!resolved) {
|
|
127
|
+
resolved = true;
|
|
128
|
+
cleanup();
|
|
129
|
+
resolve({ passed: false, hint: 'Port 8770 is in use' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
sock.on('error', () => {
|
|
134
|
+
// Connection failed, port is free
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
resolved = true;
|
|
137
|
+
cleanup();
|
|
138
|
+
resolve({ passed: true, hint: '' });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
sock.on('timeout', () => {
|
|
143
|
+
if (!resolved) {
|
|
144
|
+
resolved = true;
|
|
145
|
+
cleanup();
|
|
146
|
+
resolve({ passed: true, hint: '' });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Fallback timeout to ensure we resolve within 2 seconds
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (!resolved) {
|
|
153
|
+
resolved = true;
|
|
154
|
+
cleanup();
|
|
155
|
+
resolve({ passed: true, hint: '' });
|
|
156
|
+
}
|
|
157
|
+
}, 2000);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Format a row in the readiness table
|
|
162
|
+
function formatRow(label, status, hint) {
|
|
163
|
+
const statusStr = status ? colorPass() : colorFail();
|
|
164
|
+
const hintStr = hint ? ` — ${hint}` : '';
|
|
165
|
+
// Pad label to 35 chars for alignment
|
|
166
|
+
const paddedLabel = label.padEnd(35);
|
|
167
|
+
return ` ${paddedLabel} ${statusStr}${hintStr}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Main execution
|
|
171
|
+
(async function main() {
|
|
172
|
+
try {
|
|
173
|
+
console.log(`\n${COLORS.BOLD}Aesop Readiness Check${COLORS.RESET}\n`);
|
|
174
|
+
|
|
175
|
+
const syncChecks = [
|
|
176
|
+
{ label: 'Node.js version ≥18', fn: checkNodeVersion },
|
|
177
|
+
{ label: 'Python (python3 or python)', fn: checkPython },
|
|
178
|
+
{ label: 'Git repository', fn: checkGitRepo },
|
|
179
|
+
{ label: 'aesop.config.json (valid JSON)', fn: checkConfig },
|
|
180
|
+
{ label: 'Required directories (daemons, dash, monitor, tools, ui)', fn: checkDirectories },
|
|
181
|
+
{ label: 'Git pre-push hook installed', fn: checkPrePushHook }
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const results = [];
|
|
185
|
+
|
|
186
|
+
// Run sync checks
|
|
187
|
+
for (const check of syncChecks) {
|
|
188
|
+
const result = check.fn();
|
|
189
|
+
results.push({ label: check.label, ...result });
|
|
190
|
+
console.log(formatRow(check.label, result.passed, result.hint));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Run async port check
|
|
194
|
+
try {
|
|
195
|
+
const portResult = await checkPort8770();
|
|
196
|
+
results.push({ label: 'Port 8770 available', ...portResult });
|
|
197
|
+
console.log(formatRow('Port 8770 available', portResult.passed, portResult.hint));
|
|
198
|
+
} catch (e) {
|
|
199
|
+
results.push({ label: 'Port 8770 available', passed: false, hint: 'Port check failed' });
|
|
200
|
+
console.log(formatRow('Port 8770 available', false, 'Port check failed'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const allPassed = results.every(r => r.passed);
|
|
204
|
+
const passCount = results.filter(r => r.passed).length;
|
|
205
|
+
const failCount = results.length - passCount;
|
|
206
|
+
|
|
207
|
+
console.log(`\n${COLORS.BOLD}Summary: ${passCount}/${results.length} checks passed${COLORS.RESET}`);
|
|
208
|
+
|
|
209
|
+
if (allPassed) {
|
|
210
|
+
console.log(`${COLORS.GREEN}✓ You are ready to run: bash daemons/run-watchdog.sh --once${COLORS.RESET}\n`);
|
|
211
|
+
process.exitCode = 0;
|
|
212
|
+
} else {
|
|
213
|
+
console.log(`${COLORS.RED}✗ Fix the ${failCount} failed check(s) above and try again${COLORS.RESET}\n`);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error(`Error running doctor: ${err.message}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
})();
|
package/tools/fleet_ledger.py
CHANGED
|
@@ -3,16 +3,20 @@ r"""
|
|
|
3
3
|
Fleet Outcome Ledger — append-only audit trail for dispatched agents.
|
|
4
4
|
|
|
5
5
|
Subcommands:
|
|
6
|
-
append <timestamp> <agent_type> <model> <duration_sec> <tokens_in> <tokens_out> [verdict]
|
|
6
|
+
append <timestamp> <agent_type> <model> <duration_sec> <tokens_in> <tokens_out> [verdict] [phase] [wave]
|
|
7
7
|
Manually append one ledger line (verdict = OK|FAILED|EMPTY|HUNG, default OK)
|
|
8
|
+
phase = build|verify|repair|other (optional, default null)
|
|
9
|
+
wave = wave number as integer (optional, default null)
|
|
8
10
|
harvest
|
|
9
11
|
Scan session tasks directories for agent outcomes and append missing entries.
|
|
10
12
|
(Tracks state in ledger directory for resume capability)
|
|
11
13
|
rotate
|
|
12
14
|
Archive ledger lines exceeding ~200 lines to dated archive, keep recent tail in live ledger
|
|
15
|
+
summary
|
|
16
|
+
Print total cost/tokens grouped by wave number and phase; supports --json for machine reading
|
|
13
17
|
|
|
14
18
|
Ledger format (markdown table):
|
|
15
|
-
| ISO ts | agent_type | model | duration_sec | tokens_in | tokens_out | verdict |
|
|
19
|
+
| ISO ts | agent_type | model | duration_sec | tokens_in | tokens_out | verdict | phase | wave |
|
|
16
20
|
|
|
17
21
|
Environment:
|
|
18
22
|
AESOP_STATE_ROOT: Path to state directory (default: ./state relative to cwd)
|
|
@@ -27,12 +31,10 @@ from datetime import datetime, timezone
|
|
|
27
31
|
import re
|
|
28
32
|
from collections import defaultdict
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return Path(os.environ["AESOP_STATE_ROOT"])
|
|
35
|
-
return Path.cwd() / "state"
|
|
34
|
+
try:
|
|
35
|
+
from common import get_state_dir
|
|
36
|
+
except ImportError:
|
|
37
|
+
from tools.common import get_state_dir
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
def get_ledger_paths():
|
|
@@ -49,13 +51,25 @@ def ensure_ledger_header():
|
|
|
49
51
|
ledger_file, _, ledger_dir = get_ledger_paths()
|
|
50
52
|
if not ledger_file.exists():
|
|
51
53
|
ledger_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
-
header = '| ISO ts | agent_type | model | duration_sec | tokens_in | tokens_out | verdict |\n'
|
|
53
|
-
header += '
|
|
54
|
+
header = '| ISO ts | agent_type | model | duration_sec | tokens_in | tokens_out | verdict | phase | wave |\n'
|
|
55
|
+
header += '|--------|------------|-------|--------------|-----------|------------|--------|-------|------|\n'
|
|
54
56
|
ledger_file.write_text(header, encoding='utf-8')
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
def append_ledger_line(iso_ts, agent_type, model, duration_sec, tokens_in, tokens_out, verdict='OK'):
|
|
58
|
-
"""Append one line to the ledger.
|
|
59
|
+
def append_ledger_line(iso_ts, agent_type, model, duration_sec, tokens_in, tokens_out, verdict='OK', phase=None, wave=None):
|
|
60
|
+
"""Append one line to the ledger.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
iso_ts: ISO 8601 timestamp
|
|
64
|
+
agent_type: agent type string
|
|
65
|
+
model: model identifier
|
|
66
|
+
duration_sec: duration in seconds
|
|
67
|
+
tokens_in: input tokens
|
|
68
|
+
tokens_out: output tokens
|
|
69
|
+
verdict: OK|FAILED|EMPTY|HUNG (default OK)
|
|
70
|
+
phase: build|verify|repair|other (optional, default None)
|
|
71
|
+
wave: wave number as int or None (optional, default None)
|
|
72
|
+
"""
|
|
59
73
|
ensure_ledger_header()
|
|
60
74
|
ledger_file, _, _ = get_ledger_paths()
|
|
61
75
|
|
|
@@ -64,6 +78,20 @@ def append_ledger_line(iso_ts, agent_type, model, duration_sec, tokens_in, token
|
|
|
64
78
|
model = str(model or '-').replace('|', '').strip()[:30]
|
|
65
79
|
verdict = str(verdict or 'OK').replace('|', '').strip()[:10]
|
|
66
80
|
|
|
81
|
+
# Sanitize optional fields
|
|
82
|
+
if phase is not None:
|
|
83
|
+
phase = str(phase).replace('|', '').strip()[:15]
|
|
84
|
+
else:
|
|
85
|
+
phase = ''
|
|
86
|
+
|
|
87
|
+
if wave is not None:
|
|
88
|
+
try:
|
|
89
|
+
wave = str(int(wave))
|
|
90
|
+
except (ValueError, TypeError):
|
|
91
|
+
wave = ''
|
|
92
|
+
else:
|
|
93
|
+
wave = ''
|
|
94
|
+
|
|
67
95
|
try:
|
|
68
96
|
dur = int(duration_sec) if duration_sec else 0
|
|
69
97
|
except (ValueError, TypeError):
|
|
@@ -79,7 +107,7 @@ def append_ledger_line(iso_ts, agent_type, model, duration_sec, tokens_in, token
|
|
|
79
107
|
except (ValueError, TypeError):
|
|
80
108
|
to = 0
|
|
81
109
|
|
|
82
|
-
line = f'| {iso_ts} | {agent_type} | {model} | {dur} | {ti} | {to} | {verdict} |\n'
|
|
110
|
+
line = f'| {iso_ts} | {agent_type} | {model} | {dur} | {ti} | {to} | {verdict} | {phase} | {wave} |\n'
|
|
83
111
|
with open(ledger_file, 'a', encoding='utf-8') as f:
|
|
84
112
|
f.write(line)
|
|
85
113
|
|
|
@@ -232,6 +260,144 @@ def rotate():
|
|
|
232
260
|
print(f'Live ledger now has {len(new_ledger) - len(header_lines)} data lines')
|
|
233
261
|
|
|
234
262
|
|
|
263
|
+
def parse_ledger_rows():
|
|
264
|
+
"""Parse and return all ledger rows as structured data.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
list of dicts, each with keys: iso_ts, agent_type, model, duration_sec,
|
|
268
|
+
tokens_in, tokens_out, verdict, phase, wave (wave as int or None)
|
|
269
|
+
|
|
270
|
+
Returns empty list if ledger doesn't exist or is unreadable.
|
|
271
|
+
"""
|
|
272
|
+
ensure_ledger_header()
|
|
273
|
+
ledger_file, _, _ = get_ledger_paths()
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
lines = ledger_file.read_text(encoding='utf-8').split('\n')
|
|
277
|
+
except (IOError, OSError):
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
rows = []
|
|
281
|
+
for line in lines:
|
|
282
|
+
# Skip empty, header, separator lines
|
|
283
|
+
if not line.strip() or '---|' in line or not line.startswith('|'):
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# Parse markdown table row
|
|
287
|
+
cells = [c.strip() for c in line.split('|')[1:-1]] # split by |, skip first/last empty
|
|
288
|
+
if len(cells) < 7:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
# Original columns: ISO ts, agent_type, model, duration_sec, tokens_in, tokens_out, verdict
|
|
293
|
+
# New columns: phase, wave
|
|
294
|
+
iso_ts = cells[0]
|
|
295
|
+
agent_type = cells[1]
|
|
296
|
+
model = cells[2]
|
|
297
|
+
duration_sec = int(cells[3]) if cells[3] else 0
|
|
298
|
+
tokens_in = int(cells[4]) if cells[4] else 0
|
|
299
|
+
tokens_out = int(cells[5]) if cells[5] else 0
|
|
300
|
+
verdict = cells[6] if len(cells) > 6 else 'OK'
|
|
301
|
+
phase = cells[7].strip() if len(cells) > 7 and cells[7].strip() else None
|
|
302
|
+
wave = cells[8].strip() if len(cells) > 8 and cells[8].strip() else None
|
|
303
|
+
|
|
304
|
+
# Try to parse wave as int
|
|
305
|
+
wave_num = None
|
|
306
|
+
if wave:
|
|
307
|
+
try:
|
|
308
|
+
wave_num = int(wave)
|
|
309
|
+
except ValueError:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
rows.append({
|
|
313
|
+
'iso_ts': iso_ts,
|
|
314
|
+
'agent_type': agent_type,
|
|
315
|
+
'model': model,
|
|
316
|
+
'duration_sec': duration_sec,
|
|
317
|
+
'tokens_in': tokens_in,
|
|
318
|
+
'tokens_out': tokens_out,
|
|
319
|
+
'verdict': verdict,
|
|
320
|
+
'phase': phase,
|
|
321
|
+
'wave': wave_num,
|
|
322
|
+
})
|
|
323
|
+
except (ValueError, IndexError):
|
|
324
|
+
# Skip malformed lines silently
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
return rows
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def summary(output_format='text'):
|
|
331
|
+
"""Aggregate ledger entries by wave and phase; report total tokens/cost.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
output_format: 'text' (default) or 'json'
|
|
335
|
+
"""
|
|
336
|
+
# Use shared parser to get rows
|
|
337
|
+
rows = parse_ledger_rows()
|
|
338
|
+
|
|
339
|
+
# Parse ledger: skip header and separator lines, parse data lines
|
|
340
|
+
by_wave_phase = defaultdict(lambda: {'tokens_out': 0, 'tokens_in': 0, 'entries': 0, 'duration': 0})
|
|
341
|
+
by_wave = defaultdict(lambda: {'tokens_out': 0, 'tokens_in': 0, 'entries': 0, 'duration': 0})
|
|
342
|
+
totals = {'tokens_out': 0, 'tokens_in': 0, 'entries': 0, 'duration': 0}
|
|
343
|
+
|
|
344
|
+
for row in rows:
|
|
345
|
+
tokens_in = row['tokens_in']
|
|
346
|
+
tokens_out = row['tokens_out']
|
|
347
|
+
duration_sec = row['duration_sec']
|
|
348
|
+
wave_num = row['wave']
|
|
349
|
+
phase = row['phase']
|
|
350
|
+
|
|
351
|
+
# Accumulate by wave+phase
|
|
352
|
+
key = (wave_num, phase)
|
|
353
|
+
by_wave_phase[key]['tokens_out'] += tokens_out
|
|
354
|
+
by_wave_phase[key]['tokens_in'] += tokens_in
|
|
355
|
+
by_wave_phase[key]['entries'] += 1
|
|
356
|
+
by_wave_phase[key]['duration'] += duration_sec
|
|
357
|
+
|
|
358
|
+
# Accumulate by wave
|
|
359
|
+
by_wave[wave_num]['tokens_out'] += tokens_out
|
|
360
|
+
by_wave[wave_num]['tokens_in'] += tokens_in
|
|
361
|
+
by_wave[wave_num]['entries'] += 1
|
|
362
|
+
by_wave[wave_num]['duration'] += duration_sec
|
|
363
|
+
|
|
364
|
+
# Accumulate totals
|
|
365
|
+
totals['tokens_out'] += tokens_out
|
|
366
|
+
totals['tokens_in'] += tokens_in
|
|
367
|
+
totals['entries'] += 1
|
|
368
|
+
totals['duration'] += duration_sec
|
|
369
|
+
|
|
370
|
+
if output_format == 'json':
|
|
371
|
+
import json
|
|
372
|
+
result = {
|
|
373
|
+
'by_wave_phase': {str(k): v for k, v in sorted(by_wave_phase.items())},
|
|
374
|
+
'by_wave': {str(k): v for k, v in sorted(by_wave.items())},
|
|
375
|
+
'totals': totals
|
|
376
|
+
}
|
|
377
|
+
print(json.dumps(result, indent=2))
|
|
378
|
+
else:
|
|
379
|
+
# Text format: human-readable table
|
|
380
|
+
print('\n=== Ledger Summary ===')
|
|
381
|
+
print(f'\nTotal: {totals["entries"]} entries | {totals["tokens_in"]} in | {totals["tokens_out"]} out | {totals["duration"]}s\n')
|
|
382
|
+
|
|
383
|
+
if by_wave_phase:
|
|
384
|
+
print('By Wave + Phase:')
|
|
385
|
+
print(' Wave | Phase | Entries | Tokens In | Tokens Out | Duration')
|
|
386
|
+
print(' -----|----------|---------|-----------|------------|----------')
|
|
387
|
+
for (wave_num, phase), stats in sorted(by_wave_phase.items()):
|
|
388
|
+
w_str = str(wave_num) if wave_num is not None else 'None'
|
|
389
|
+
p_str = phase if phase else '(no phase)'
|
|
390
|
+
print(f' {w_str:4} | {p_str:8} | {stats["entries"]:7} | {stats["tokens_in"]:9} | {stats["tokens_out"]:10} | {stats["duration"]:8}')
|
|
391
|
+
|
|
392
|
+
if by_wave:
|
|
393
|
+
print('\nBy Wave (Total):')
|
|
394
|
+
print(' Wave | Entries | Tokens In | Tokens Out | Duration')
|
|
395
|
+
print(' -----|---------|-----------|------------|----------')
|
|
396
|
+
for wave_num, stats in sorted(by_wave.items()):
|
|
397
|
+
w_str = str(wave_num) if wave_num is not None else 'None'
|
|
398
|
+
print(f' {w_str:4} | {stats["entries"]:7} | {stats["tokens_in"]:9} | {stats["tokens_out"]:10} | {stats["duration"]:8}')
|
|
399
|
+
|
|
400
|
+
|
|
235
401
|
def main():
|
|
236
402
|
if len(sys.argv) < 2:
|
|
237
403
|
print(__doc__)
|
|
@@ -240,9 +406,9 @@ def main():
|
|
|
240
406
|
cmd = sys.argv[1].lower()
|
|
241
407
|
|
|
242
408
|
if cmd == 'append':
|
|
243
|
-
# append <ts> <agent_type> <model> <dur> <tokens_in> <tokens_out> [verdict]
|
|
409
|
+
# append <ts> <agent_type> <model> <dur> <tokens_in> <tokens_out> [verdict] [phase] [wave]
|
|
244
410
|
if len(sys.argv) < 7:
|
|
245
|
-
print('Usage: fleet_ledger.py append <ts> <agent_type> <model> <dur_sec> <tokens_in> <tokens_out> [verdict]')
|
|
411
|
+
print('Usage: fleet_ledger.py append <ts> <agent_type> <model> <dur_sec> <tokens_in> <tokens_out> [verdict] [phase] [wave]')
|
|
246
412
|
sys.exit(1)
|
|
247
413
|
|
|
248
414
|
ts = sys.argv[2]
|
|
@@ -252,9 +418,11 @@ def main():
|
|
|
252
418
|
ti = sys.argv[6]
|
|
253
419
|
to = sys.argv[7] if len(sys.argv) > 7 else '0'
|
|
254
420
|
verdict = sys.argv[8] if len(sys.argv) > 8 else 'OK'
|
|
421
|
+
phase = sys.argv[9] if len(sys.argv) > 9 else None
|
|
422
|
+
wave = sys.argv[10] if len(sys.argv) > 10 else None
|
|
255
423
|
|
|
256
|
-
append_ledger_line(ts, agent_type, model, dur, ti, to, verdict)
|
|
257
|
-
print(f'Appended: {ts} {agent_type} {model} {dur}s {ti}->{to} [{verdict}]')
|
|
424
|
+
append_ledger_line(ts, agent_type, model, dur, ti, to, verdict, phase, wave)
|
|
425
|
+
print(f'Appended: {ts} {agent_type} {model} {dur}s {ti}->{to} [{verdict}] phase={phase} wave={wave}')
|
|
258
426
|
|
|
259
427
|
elif cmd == 'harvest':
|
|
260
428
|
harvest()
|
|
@@ -262,6 +430,10 @@ def main():
|
|
|
262
430
|
elif cmd == 'rotate':
|
|
263
431
|
rotate()
|
|
264
432
|
|
|
433
|
+
elif cmd == 'summary':
|
|
434
|
+
output_fmt = 'json' if '--json' in sys.argv else 'text'
|
|
435
|
+
summary(output_fmt)
|
|
436
|
+
|
|
265
437
|
else:
|
|
266
438
|
print(f'Unknown command: {cmd}')
|
|
267
439
|
print(__doc__)
|
package/tools/halt.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fleet kill switch — a .HALT sentinel file that daemons/run-watchdog.sh (and any
|
|
4
|
+
other cardinal-rule-abiding loop) must check every cycle and refuse to do work
|
|
5
|
+
while it exists.
|
|
6
|
+
|
|
7
|
+
Why: wave-26 critique — autonomy expanded to self-merging portfolio PRs on green
|
|
8
|
+
with no ceiling/cap/limit/abort anywhere in the harness. This is the abort.
|
|
9
|
+
|
|
10
|
+
API:
|
|
11
|
+
halt(reason, state_dir=None) -> Path
|
|
12
|
+
Write the .HALT sentinel (reason + UTC timestamp, JSON) under the resolved
|
|
13
|
+
state dir. Creates the state dir if missing. Overwrites any existing sentinel
|
|
14
|
+
(last halt() call wins — reason is always the most recent one).
|
|
15
|
+
|
|
16
|
+
is_halted(state_dir=None) -> bool
|
|
17
|
+
True iff the sentinel file exists.
|
|
18
|
+
|
|
19
|
+
get_halt_info(state_dir=None) -> dict | None
|
|
20
|
+
Parsed sentinel contents ({"reason": ..., "timestamp": ...}), or None if not halted.
|
|
21
|
+
|
|
22
|
+
clear_halt(state_dir=None) -> bool
|
|
23
|
+
Remove the sentinel if present. Returns True if a sentinel was removed, False
|
|
24
|
+
if there was nothing to clear (idempotent no-op).
|
|
25
|
+
|
|
26
|
+
resolve_state_dir(config=None) -> Path
|
|
27
|
+
Precedence: AESOP_STATE_ROOT env var > aesop.config.json "state_root" > default
|
|
28
|
+
(tools/common.get_state_dir(): ./state relative to cwd). Mirrors the precedence
|
|
29
|
+
documented in aesop.config.example.json's top-level "description" field.
|
|
30
|
+
|
|
31
|
+
CLI:
|
|
32
|
+
python tools/halt.py set "<reason>" -> writes sentinel, exit 0
|
|
33
|
+
python tools/halt.py --status -> prints halted/not-halted; exit 1 if halted, 0 if not
|
|
34
|
+
python tools/halt.py --clear -> removes sentinel if present, exit 0
|
|
35
|
+
|
|
36
|
+
Sentinel location: <state_dir>/.HALT
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import sys
|
|
42
|
+
from datetime import datetime, timezone
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from common import get_state_dir
|
|
47
|
+
except ImportError:
|
|
48
|
+
from tools.common import get_state_dir
|
|
49
|
+
|
|
50
|
+
SENTINEL_NAME = ".HALT"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_config():
|
|
54
|
+
"""Load aesop.config.json from current directory, return dict (or {} if absent/bad)."""
|
|
55
|
+
config_file = Path("aesop.config.json")
|
|
56
|
+
if not config_file.exists():
|
|
57
|
+
return {}
|
|
58
|
+
try:
|
|
59
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
60
|
+
return json.load(f)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"[halt] Failed to load config: {e}", file=sys.stderr)
|
|
63
|
+
return {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_state_dir(config=None):
|
|
67
|
+
"""Resolve the state directory: AESOP_STATE_ROOT env > config state_root > default.
|
|
68
|
+
|
|
69
|
+
A relative config state_root is resolved against AESOP_ROOT (if set) else cwd,
|
|
70
|
+
matching how daemons/run-watchdog.sh resolves $AESOP_ROOT/state.
|
|
71
|
+
"""
|
|
72
|
+
if os.environ.get("AESOP_STATE_ROOT"):
|
|
73
|
+
return Path(os.environ["AESOP_STATE_ROOT"])
|
|
74
|
+
|
|
75
|
+
if config is None:
|
|
76
|
+
config = load_config()
|
|
77
|
+
|
|
78
|
+
state_root = config.get("state_root") if isinstance(config, dict) else None
|
|
79
|
+
if state_root:
|
|
80
|
+
p = Path(state_root).expanduser()
|
|
81
|
+
if not p.is_absolute():
|
|
82
|
+
root = Path(os.environ.get("AESOP_ROOT", Path.cwd()))
|
|
83
|
+
p = root / p
|
|
84
|
+
return p
|
|
85
|
+
|
|
86
|
+
return get_state_dir()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _sentinel_path(state_dir=None):
|
|
90
|
+
if state_dir is None:
|
|
91
|
+
state_dir = resolve_state_dir()
|
|
92
|
+
return Path(state_dir) / SENTINEL_NAME
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def halt(reason, state_dir=None):
|
|
96
|
+
"""Write the .HALT sentinel. Returns the sentinel Path."""
|
|
97
|
+
if state_dir is None:
|
|
98
|
+
state_dir = resolve_state_dir()
|
|
99
|
+
state_dir = Path(state_dir)
|
|
100
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
sentinel = state_dir / SENTINEL_NAME
|
|
103
|
+
payload = {
|
|
104
|
+
"reason": str(reason),
|
|
105
|
+
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
106
|
+
}
|
|
107
|
+
sentinel.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
108
|
+
return sentinel
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_halted(state_dir=None):
|
|
112
|
+
"""True iff the .HALT sentinel exists."""
|
|
113
|
+
return _sentinel_path(state_dir).exists()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_halt_info(state_dir=None):
|
|
117
|
+
"""Return parsed sentinel dict, or None if not halted / unreadable."""
|
|
118
|
+
sentinel = _sentinel_path(state_dir)
|
|
119
|
+
if not sentinel.exists():
|
|
120
|
+
return None
|
|
121
|
+
try:
|
|
122
|
+
return json.loads(sentinel.read_text(encoding="utf-8"))
|
|
123
|
+
except (json.JSONDecodeError, OSError):
|
|
124
|
+
# Sentinel exists but is corrupt — still halted, just no parsed detail.
|
|
125
|
+
return {"reason": "(unreadable sentinel)", "timestamp": None}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def clear_halt(state_dir=None):
|
|
129
|
+
"""Remove the sentinel if present. Returns True if removed, False if absent."""
|
|
130
|
+
sentinel = _sentinel_path(state_dir)
|
|
131
|
+
if sentinel.exists():
|
|
132
|
+
sentinel.unlink()
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main(argv=None):
|
|
138
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
139
|
+
|
|
140
|
+
if not argv:
|
|
141
|
+
print('Usage: halt.py set "<reason>" | --status | --clear', file=sys.stderr)
|
|
142
|
+
return 2
|
|
143
|
+
|
|
144
|
+
if argv[0] == "set":
|
|
145
|
+
if len(argv) < 2 or not argv[1].strip():
|
|
146
|
+
print('Usage: halt.py set "<reason>"', file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
reason = " ".join(argv[1:])
|
|
149
|
+
sentinel = halt(reason)
|
|
150
|
+
print(f"HALTED: {reason}")
|
|
151
|
+
print(f"sentinel: {sentinel}")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
if argv[0] == "--status":
|
|
155
|
+
info = get_halt_info()
|
|
156
|
+
if info:
|
|
157
|
+
print(f"HALTED: {info.get('reason')} (since {info.get('timestamp')})")
|
|
158
|
+
return 1
|
|
159
|
+
print("not halted")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
if argv[0] == "--clear":
|
|
163
|
+
cleared = clear_halt()
|
|
164
|
+
print("cleared" if cleared else "not halted (nothing to clear)")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
print(f"Unknown argument: {argv[0]}", file=sys.stderr)
|
|
168
|
+
return 2
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
sys.exit(main())
|