@matt82198/aesop 0.1.0-beta.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.
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/aesop.config.example.json +47 -0
- package/bin/cli.js +112 -0
- package/daemons/backup-fleet.sh +173 -0
- package/daemons/run-watchdog.sh +37 -0
- package/dash/dash-extra.mjs +184 -0
- package/dash/watchdog-gui.sh +141 -0
- package/docs/CARDINAL-RULES.md +146 -0
- package/docs/DISPATCH-MODEL.md +180 -0
- package/docs/PUBLISHING.md +150 -0
- package/docs/av-resilience.md +246 -0
- package/monitor/CHARTER.md +66 -0
- package/monitor/collect-signals.mjs +137 -0
- package/package.json +50 -0
- package/tools/launch_tui.py +245 -0
- package/tools/secret_scan.py +363 -0
- package/ui/README.md +141 -0
- package/ui/serve.py +806 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Orchestration Refinement Monitor — deterministic signal collector (no LLM).
|
|
2
|
+
// Walks known roots and emits a compact BRIEF.md + SIGNALS.json the Haiku monitor reads
|
|
3
|
+
// each cycle, so its reasoning stays cheap and focused. Node built-ins only.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const AESOP_ROOT = process.env.AESOP_ROOT || '.';
|
|
10
|
+
const MON = path.join(AESOP_ROOT, 'monitor');
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const HOUR = 3600e3;
|
|
13
|
+
const DAY = 24 * HOUR;
|
|
14
|
+
|
|
15
|
+
const sh = (cmd, cwd) => {
|
|
16
|
+
try {
|
|
17
|
+
return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const stat = (p) => {
|
|
24
|
+
try {
|
|
25
|
+
return fs.statSync(p);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const age = (ms) => {
|
|
32
|
+
if (ms < HOUR) return `${Math.round(ms / 60e3)}m`;
|
|
33
|
+
if (ms < DAY) return `${(ms / HOUR).toFixed(1)}h`;
|
|
34
|
+
return `${(ms / DAY).toFixed(1)}d`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Main signal collection
|
|
38
|
+
const signals = {
|
|
39
|
+
heartbeats: [],
|
|
40
|
+
rotations: [],
|
|
41
|
+
unreviewedPrompts: 0,
|
|
42
|
+
staleMemories: [],
|
|
43
|
+
respawnWatch: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const brief = [
|
|
47
|
+
`# Aesop Monitor Brief — ${new Date(now).toISOString()}`,
|
|
48
|
+
'',
|
|
49
|
+
'## Status',
|
|
50
|
+
'Orchestration monitor cycle complete.',
|
|
51
|
+
'',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// 1) Heartbeat check
|
|
55
|
+
const beatsDir = path.join(MON, '.heartbeats');
|
|
56
|
+
let beatFiles = [];
|
|
57
|
+
try {
|
|
58
|
+
const files = fs.readdirSync(beatsDir);
|
|
59
|
+
beatFiles = files.map((f) => path.join(beatsDir, f));
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
const legacyBeats = [
|
|
63
|
+
path.join(MON, '.monitor-heartbeat'),
|
|
64
|
+
path.join(AESOP_ROOT, 'state', '.watchdog-heartbeat'),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
beatFiles = beatFiles.concat(legacyBeats.filter((f) => fs.existsSync(f)));
|
|
68
|
+
|
|
69
|
+
for (const fp of beatFiles) {
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync(fp, 'utf8').trim();
|
|
72
|
+
const epoch = parseInt(content.split('\n')[0], 10);
|
|
73
|
+
if (!epoch) continue;
|
|
74
|
+
|
|
75
|
+
const beatAge = now - epoch * 1000;
|
|
76
|
+
const name = path.basename(fp);
|
|
77
|
+
const thresholds = { watchdog: 300e3, monitor: 3600e3, default: 1800e3 };
|
|
78
|
+
let threshold = thresholds.default;
|
|
79
|
+
if (name.includes('watchdog')) threshold = thresholds.watchdog;
|
|
80
|
+
if (name.includes('monitor')) threshold = thresholds.monitor;
|
|
81
|
+
|
|
82
|
+
signals.heartbeats.push({
|
|
83
|
+
name,
|
|
84
|
+
ageMs: beatAge,
|
|
85
|
+
threshold,
|
|
86
|
+
ok: beatAge < threshold,
|
|
87
|
+
});
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
brief.push('## Heartbeats');
|
|
92
|
+
if (signals.heartbeats.length === 0) {
|
|
93
|
+
brief.push('No heartbeats detected yet.');
|
|
94
|
+
} else {
|
|
95
|
+
for (const hb of signals.heartbeats) {
|
|
96
|
+
const status = hb.ok ? '✓' : '✗';
|
|
97
|
+
brief.push(`- ${status} ${hb.name}: ${age(hb.ageMs)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
brief.push('');
|
|
101
|
+
|
|
102
|
+
// 2) Log file status
|
|
103
|
+
brief.push('## Logs');
|
|
104
|
+
const logFiles = [
|
|
105
|
+
path.join(AESOP_ROOT, 'state', 'FLEET-BACKUP.log'),
|
|
106
|
+
path.join(AESOP_ROOT, 'state', 'SECURITY-ALERTS.log'),
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const logFile of logFiles) {
|
|
110
|
+
const st = stat(logFile);
|
|
111
|
+
if (st) {
|
|
112
|
+
brief.push(`- ${path.basename(logFile)}: ${st.size} bytes`);
|
|
113
|
+
} else {
|
|
114
|
+
brief.push(`- ${path.basename(logFile)}: (not found)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
brief.push('');
|
|
118
|
+
|
|
119
|
+
// 3) Placeholder for future signal collectors
|
|
120
|
+
brief.push('## Notes');
|
|
121
|
+
brief.push('This is a template monitor. Extend collect-signals.mjs to add:');
|
|
122
|
+
brief.push('- Junk-script sprawl detection');
|
|
123
|
+
brief.push('- Memory gap detection');
|
|
124
|
+
brief.push('- Rule friction analysis');
|
|
125
|
+
brief.push('- Orchestration health checks');
|
|
126
|
+
brief.push('');
|
|
127
|
+
|
|
128
|
+
// Write outputs
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(MON, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(MON, 'BRIEF.md'), brief.join('\n'), 'utf8');
|
|
132
|
+
fs.writeFileSync(path.join(MON, 'SIGNALS.json'), JSON.stringify(signals, null, 2), 'utf8');
|
|
133
|
+
console.log('Monitor signals collected. See BRIEF.md and SIGNALS.json.');
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error('Failed to write signals:', e.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matt82198/aesop",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure — a plain-file brain, git as the only durable layer, cheap subagent fleets, and guardrails enforced in code.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"aesop": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"daemons/",
|
|
12
|
+
"dash/",
|
|
13
|
+
"monitor/",
|
|
14
|
+
"tools/",
|
|
15
|
+
"ui/",
|
|
16
|
+
"docs/",
|
|
17
|
+
"aesop.config.example.json",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai-agents",
|
|
27
|
+
"llm",
|
|
28
|
+
"claude",
|
|
29
|
+
"orchestration",
|
|
30
|
+
"multi-agent",
|
|
31
|
+
"developer-tools",
|
|
32
|
+
"resilience",
|
|
33
|
+
"template"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Matt Culliton",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/matt82198/aesop.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/matt82198/aesop",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/matt82198/aesop/issues"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
launch_tui.py — Open a bash TUI script in its own terminal window.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python launch_tui.py --script <path-to-bash-script> [--title <window-title>] [--pidfile <path>]
|
|
7
|
+
|
|
8
|
+
Behavior:
|
|
9
|
+
- Finds a terminal (prefer Git Bash, else Windows Terminal wt.exe)
|
|
10
|
+
- Opens a NEW visible window running `bash <script>`
|
|
11
|
+
- Idempotent via pidfile: if process already running, prints "already running (pid N)"
|
|
12
|
+
- Always outputs exactly one line: spawned pid, already-running, or error
|
|
13
|
+
- Direct git-bash spawn avoids cmd /c start quoting bugs
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import subprocess
|
|
20
|
+
import shutil
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_terminal():
|
|
26
|
+
"""
|
|
27
|
+
Locate a terminal executable.
|
|
28
|
+
Prefer Git Bash (process persists → pidfile valid), fallback to Windows Terminal.
|
|
29
|
+
Returns (terminal_path, command_builder_fn, is_wt_bool).
|
|
30
|
+
command_builder_fn(script_path, title) returns command list to spawn.
|
|
31
|
+
is_wt_bool: True if using wt.exe (needs bash process tracking); False if git-bash (stable pid).
|
|
32
|
+
"""
|
|
33
|
+
git_bash = r"C:\Program Files\Git\git-bash.exe"
|
|
34
|
+
wt_exe = shutil.which("wt.exe")
|
|
35
|
+
bash_exe = r"C:\Program Files\Git\bin\bash.exe"
|
|
36
|
+
|
|
37
|
+
# Prefer Git Bash (its process persists, so pidfile/idempotency works)
|
|
38
|
+
if os.path.exists(git_bash):
|
|
39
|
+
def git_bash_cmd(script_path, title):
|
|
40
|
+
# Spawn git-bash.exe -c "bash '/path/to/script.sh'"
|
|
41
|
+
# Convert Windows path to POSIX: C:\Users\...\... -> /c/Users/...\...
|
|
42
|
+
script_abs = os.path.abspath(script_path)
|
|
43
|
+
script_posix = "/" + script_abs[0].lower() + script_abs[2:].replace("\\", "/")
|
|
44
|
+
return [git_bash, "-c", f"bash '{script_posix}'"]
|
|
45
|
+
return git_bash, git_bash_cmd, False
|
|
46
|
+
|
|
47
|
+
# Fallback to Windows Terminal (spawns and exits, so we track bash process instead)
|
|
48
|
+
if wt_exe and os.path.exists(bash_exe):
|
|
49
|
+
def wt_cmd(script_path, title):
|
|
50
|
+
cmd = [wt_exe]
|
|
51
|
+
if title:
|
|
52
|
+
cmd.extend(["-w", title])
|
|
53
|
+
# Start a new tab/window running bash with the script
|
|
54
|
+
cmd.extend(["-d", str(Path(script_path).parent), bash_exe, script_path])
|
|
55
|
+
return cmd
|
|
56
|
+
return wt_exe, wt_cmd, True
|
|
57
|
+
|
|
58
|
+
raise FileNotFoundError(
|
|
59
|
+
"Terminal not found. Tried: Git Bash at "
|
|
60
|
+
f"{git_bash}, Windows Terminal (wt.exe) with bash at {bash_exe}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def find_bash_process_by_script(script_path):
|
|
65
|
+
r"""
|
|
66
|
+
Find bash.exe process running the given script using PowerShell.
|
|
67
|
+
Searches for the script path in the bash process CommandLine.
|
|
68
|
+
Returns PID if found, None otherwise.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
script_abs = os.path.abspath(script_path)
|
|
72
|
+
# Bash processes may show the path in either Windows or POSIX format
|
|
73
|
+
# Try both: C:\Users\... and /c/Users/...
|
|
74
|
+
search_patterns = [
|
|
75
|
+
script_abs, # Windows format: C:\Users\...
|
|
76
|
+
"/" + script_abs[0].lower() + script_abs[2:].replace("\\", "/"), # POSIX: /c/Users/...
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# Use PowerShell to find bash process matching either path format
|
|
80
|
+
ps_cmd = (
|
|
81
|
+
f"$patterns = @('{script_abs}', '/{script_abs[0].lower()}{script_abs[2:].replace(chr(92), '/')}'); "
|
|
82
|
+
f"Get-CimInstance Win32_Process -Filter \"name='bash.exe'\" | "
|
|
83
|
+
f"Where-Object {{ $cmd = $_.CommandLine; $patterns | Where-Object {{ $cmd -like \"*$_*\" }} }} | "
|
|
84
|
+
f"Select-Object -First 1 -ExpandProperty ProcessId"
|
|
85
|
+
)
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["powershell", "-NoProfile", "-Command", ps_cmd],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=5,
|
|
91
|
+
)
|
|
92
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
93
|
+
pid_str = result.stdout.strip()
|
|
94
|
+
if pid_str.isdigit():
|
|
95
|
+
return int(pid_str)
|
|
96
|
+
return None
|
|
97
|
+
except Exception:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_bash_running(script_path):
|
|
102
|
+
"""
|
|
103
|
+
Check if bash is currently running the script by looking at running processes.
|
|
104
|
+
Returns True if any bash/git-bash process is active.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
result = subprocess.run(
|
|
108
|
+
["tasklist"],
|
|
109
|
+
capture_output=True,
|
|
110
|
+
text=True,
|
|
111
|
+
timeout=5,
|
|
112
|
+
)
|
|
113
|
+
# If bash or git-bash is running, assume script might be active
|
|
114
|
+
return "bash.exe" in result.stdout or "git-bash.exe" in result.stdout
|
|
115
|
+
except Exception:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def is_pidfile_valid_and_recent(pidfile_path, script_path):
|
|
120
|
+
"""
|
|
121
|
+
Check if pidfile exists, is recent (created in last 60 seconds),
|
|
122
|
+
and bash processes are running. This is an idempotency heuristic.
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
if not pidfile_path.exists():
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
mtime = os.path.getmtime(pidfile_path)
|
|
129
|
+
age = time.time() - mtime
|
|
130
|
+
# If pidfile is less than 60 seconds old and bash is running, assume still active
|
|
131
|
+
if age < 60 and check_bash_running(script_path):
|
|
132
|
+
with open(pidfile_path, "r") as f:
|
|
133
|
+
old_pid = f.read().strip()
|
|
134
|
+
return int(old_pid)
|
|
135
|
+
|
|
136
|
+
return False
|
|
137
|
+
except Exception:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main():
|
|
142
|
+
parser = argparse.ArgumentParser(
|
|
143
|
+
description="Open a bash TUI script in its own terminal window.",
|
|
144
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--script",
|
|
148
|
+
required=True,
|
|
149
|
+
help="Path to bash script to run",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--title",
|
|
153
|
+
default=None,
|
|
154
|
+
help="Window title (optional)",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"--pidfile",
|
|
158
|
+
default=None,
|
|
159
|
+
help="Path to pidfile for idempotency (optional)",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
script_path = args.script
|
|
165
|
+
title = args.title
|
|
166
|
+
pidfile = args.pidfile
|
|
167
|
+
|
|
168
|
+
# Validate script exists
|
|
169
|
+
if not os.path.exists(script_path):
|
|
170
|
+
print(f"ERROR: Script not found: {script_path}")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
# Check pidfile for idempotency
|
|
174
|
+
if pidfile:
|
|
175
|
+
pidfile_path = Path(pidfile)
|
|
176
|
+
old_pid = is_pidfile_valid_and_recent(pidfile_path, script_path)
|
|
177
|
+
if old_pid:
|
|
178
|
+
print(f"already running (pid {old_pid})")
|
|
179
|
+
sys.exit(0)
|
|
180
|
+
|
|
181
|
+
# Find terminal
|
|
182
|
+
try:
|
|
183
|
+
terminal_path, cmd_builder, is_wt = find_terminal()
|
|
184
|
+
except FileNotFoundError as e:
|
|
185
|
+
print(f"ERROR: {e}")
|
|
186
|
+
sys.exit(1)
|
|
187
|
+
|
|
188
|
+
# Build and spawn command
|
|
189
|
+
cmd = cmd_builder(script_path, title)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Windows process creation flags
|
|
193
|
+
DETACHED_PROCESS = 0x00000008
|
|
194
|
+
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
195
|
+
|
|
196
|
+
# For git-bash: spawn detached so process persists after parent exits
|
|
197
|
+
# For wt.exe: spawn normally (it exits immediately anyway)
|
|
198
|
+
if is_wt:
|
|
199
|
+
creationflags = 0
|
|
200
|
+
else:
|
|
201
|
+
creationflags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
|
202
|
+
|
|
203
|
+
# Spawn the process without waiting
|
|
204
|
+
proc = subprocess.Popen(
|
|
205
|
+
cmd,
|
|
206
|
+
stdout=subprocess.DEVNULL,
|
|
207
|
+
stderr=subprocess.DEVNULL,
|
|
208
|
+
stdin=subprocess.DEVNULL,
|
|
209
|
+
creationflags=creationflags,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# For git-bash, its pid persists and we can use it directly
|
|
213
|
+
if not is_wt:
|
|
214
|
+
new_pid = proc.pid
|
|
215
|
+
else:
|
|
216
|
+
# For wt.exe, find the actual bash process running the script
|
|
217
|
+
# Poll for bash process running the script (up to 2 seconds)
|
|
218
|
+
new_pid = None
|
|
219
|
+
for _ in range(20): # 20 attempts * 0.1s = 2s max wait
|
|
220
|
+
new_pid = find_bash_process_by_script(script_path)
|
|
221
|
+
if new_pid:
|
|
222
|
+
break
|
|
223
|
+
time.sleep(0.1)
|
|
224
|
+
|
|
225
|
+
if not new_pid:
|
|
226
|
+
print(f"ERROR: Failed to locate bash process for {script_path}")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
# Write pidfile if specified
|
|
230
|
+
if pidfile:
|
|
231
|
+
pidfile_path = Path(pidfile)
|
|
232
|
+
pidfile_path.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
with open(pidfile_path, "w") as f:
|
|
234
|
+
f.write(str(new_pid))
|
|
235
|
+
|
|
236
|
+
print(f"spawned (pid {new_pid})")
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
print(f"ERROR: Failed to spawn terminal: {e}")
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
main()
|