@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.
@@ -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()