@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,173 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Backup fleet repos: stash uncommitted work, push unpushed commits to backup branches.
4
+ # Runs every 150s from run-watchdog.sh. Abort on secret-scan failures.
5
+ # Improvements: dot-directory discovery, path dedup, tracked-files-only secret scanning.
6
+
7
+ AESOP_ROOT="${AESOP_ROOT:-.}"
8
+ HEARTBEAT="$AESOP_ROOT/state/.watchdog-heartbeat"
9
+ LOG="$AESOP_ROOT/state/FLEET-BACKUP.log"
10
+ REPOS_STATUS="$AESOP_ROOT/state/.watchdog-repos.json"
11
+
12
+ date +%s > "$HEARTBEAT" 2>/dev/null
13
+ ts() { date '+%Y-%m-%d %H:%M:%S'; }
14
+ log() { echo "[$(ts)] $*" | tee -a "$LOG"; }
15
+
16
+ is_touched() {
17
+ local repo="$1"
18
+ [ ! -d "$repo/.git" ] && return 1
19
+ (
20
+ cd "$repo" || return 1
21
+ [ -n "$(git status --porcelain 2>/dev/null)" ] && return 0
22
+ [ -n "$(git log @{u}.. --oneline 2>/dev/null)" ] && return 0
23
+ return 1
24
+ )
25
+ }
26
+
27
+ get_tracked_modifications() {
28
+ local repo="$1"
29
+ (
30
+ cd "$repo" || return 1
31
+ git diff --name-only HEAD 2>/dev/null
32
+ git ls-files -m 2>/dev/null
33
+ ) | sort -u
34
+ }
35
+
36
+ scan_tracked_files() {
37
+ local repo="$1"
38
+ local tracked_files
39
+ local file_paths=""
40
+ tracked_files=$(get_tracked_modifications "$repo")
41
+
42
+ if [ -z "$tracked_files" ]; then
43
+ return 0
44
+ fi
45
+
46
+ while IFS= read -r file; do
47
+ if [ -n "$file" ] && [ -f "$repo/$file" ]; then
48
+ file_paths="$file_paths $repo/$file"
49
+ fi
50
+ done <<EOF
51
+ $tracked_files
52
+ EOF
53
+
54
+ if [ -z "$file_paths" ]; then
55
+ return 0
56
+ fi
57
+
58
+ if [ -f "$AESOP_ROOT/tools/secret_scan.py" ]; then
59
+ python "$AESOP_ROOT/tools/secret_scan.py" $file_paths >/dev/null 2>&1
60
+ fi
61
+ }
62
+
63
+ get_default_branch() {
64
+ local repo="$1"
65
+ (cd "$repo" && git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's/refs\/remotes\/origin\///' || echo "master")
66
+ }
67
+
68
+ process_repo() {
69
+ local repo="$1"
70
+ local name=$(basename "$repo")
71
+ local default=$(get_default_branch "$repo")
72
+
73
+ (
74
+ cd "$repo" || exit 0
75
+ git fetch -q origin 2>/dev/null
76
+ local uncommitted=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
77
+
78
+ if [ "$uncommitted" -gt 0 ]; then
79
+ TMPIDX=$(mktemp)
80
+ GIT_INDEX_FILE="$TMPIDX" git read-tree HEAD 2>/dev/null
81
+ GIT_INDEX_FILE="$TMPIDX" git add -A 2>/dev/null
82
+ TREE=$(GIT_INDEX_FILE="$TMPIDX" git write-tree 2>/dev/null)
83
+ rm -f "$TMPIDX"
84
+ LOCAL=$(git rev-parse HEAD 2>/dev/null)
85
+ HEADTREE=$(git rev-parse 'HEAD^{tree}' 2>/dev/null)
86
+ if [ -n "$TREE" ] && [ "$TREE" != "$HEADTREE" ]; then
87
+ COMMIT=$(git commit-tree "$TREE" -p "$LOCAL" -m "wip $(ts) — $uncommitted files" 2>/dev/null)
88
+ WIPREF="backup/wip-$(date +%Y%m%d)"
89
+ if scan_tracked_files "$repo"; then
90
+ if git push -qf origin "$COMMIT:refs/heads/$WIPREF" 2>/dev/null; then
91
+ printf 'SNAPSHOTTED|%s\n' "$name"
92
+ exit 0
93
+ fi
94
+ else
95
+ printf 'BLOCKED|%s\n' "$name"
96
+ exit 0
97
+ fi
98
+ fi
99
+ fi
100
+
101
+ local unpushed=$(git log @{u}.. --oneline 2>/dev/null | wc -l | tr -d ' ')
102
+ if [ "$unpushed" -gt 0 ]; then
103
+ local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
104
+ [ "$branch" = "HEAD" ] && branch="$default"
105
+
106
+ if [ "$branch" = "$default" ]; then
107
+ WIPREF="backup/master-wip-$(date +%Y%m%d)"
108
+ if scan_tracked_files "$repo"; then
109
+ if git push -qf origin "HEAD:refs/heads/$WIPREF" 2>/dev/null; then
110
+ printf 'SNAPSHOTTED|%s\n' "$name"
111
+ exit 0
112
+ fi
113
+ else
114
+ printf 'BLOCKED|%s\n' "$name"
115
+ exit 0
116
+ fi
117
+ else
118
+ if scan_tracked_files "$repo"; then
119
+ if git push -q origin "$branch" 2>/dev/null; then
120
+ printf 'PUSHED|%s\n' "$name"
121
+ exit 0
122
+ fi
123
+ else
124
+ printf 'BLOCKED|%s\n' "$name"
125
+ exit 0
126
+ fi
127
+ fi
128
+ fi
129
+
130
+ printf 'CLEAN|%s\n' "$name"
131
+ )
132
+ }
133
+
134
+ log "=== cycle start ==="
135
+ temp_json=$(mktemp)
136
+ echo "[" > "$temp_json"
137
+ first=1
138
+ processed_paths=""
139
+
140
+ # Discover all git repos: scan home dot-directories, root directories, dev/ subdirectory
141
+ # This pattern includes ~/.* (dot-dirs like .claude), ~/* (home root), ~/dev/* (dev subtree)
142
+ for dir in ~/.* ~/* ~/dev/*; do
143
+ base=$(basename "$dir")
144
+ [ "$base" = "." ] || [ "$base" = ".." ] && continue
145
+ [ ! -d "$dir/.git" ] && continue
146
+
147
+ # Normalize path to detect duplicates (e.g., .claude vs .claude/)
148
+ real_dir=$(cd "$dir" && pwd 2>/dev/null)
149
+ if [ -z "$real_dir" ]; then continue; fi
150
+ if echo "$processed_paths" | grep -Fq "$real_dir"; then
151
+ continue
152
+ fi
153
+ processed_paths="$processed_paths
154
+ $real_dir"
155
+
156
+ if is_touched "$dir"; then
157
+ result=$(process_repo "$dir")
158
+ IFS="|" read -r state name <<< "$result"
159
+ [ -z "$state" ] && continue
160
+ if [ "$first" = 1 ]; then
161
+ first=0
162
+ else
163
+ echo "," >> "$temp_json"
164
+ fi
165
+ printf '{"repo":"%s","state":"%s","age":"%s"}' "$name" "$state" "$(date -Iseconds)" >> "$temp_json"
166
+ log "$state: $name"
167
+ fi
168
+ done
169
+
170
+ echo "" >> "$temp_json"
171
+ echo "]" >> "$temp_json"
172
+ mv "$temp_json" "$REPOS_STATUS"
173
+ log "=== cycle end ==="
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # Durable fleet watchdog daemon (runs in a shell window). Ctrl-C to stop.
3
+ # Backs up committed + uncommitted fleet work and scans for security issues every 150s.
4
+ # Usage: run-watchdog.sh [--once]
5
+ #
6
+ # Configuration: export AESOP_ROOT=/path/to/aesop before running, or edit default below.
7
+
8
+ AESOP_ROOT="${AESOP_ROOT:-.}"
9
+ MODE="${1:-daemon}"
10
+
11
+ # Heartbeat guard: skip if already running (within 200s)
12
+ __hb=$(cat "$AESOP_ROOT/state/.watchdog-heartbeat" 2>/dev/null)
13
+ __now=$(date +%s)
14
+ if [ "$MODE" != "--once" ] && [ -n "$__hb" ] && [ $((__now - __hb)) -lt 200 ] 2>/dev/null; then
15
+ echo "watchdog already running (heartbeat $((__now - __hb))s ago) — not starting a duplicate."
16
+ exit 0
17
+ fi
18
+
19
+ echo "==================================================================="
20
+ echo " FLEET WATCHDOG DAEMON · backup + ensure-push + scan / 150s"
21
+ echo " logs: $AESOP_ROOT/state/FLEET-BACKUP.log · Ctrl-C to stop"
22
+ echo "==================================================================="
23
+ echo "[$(date '+%F %T')] === watchdog daemon (shell) STARTED ===" >> "$AESOP_ROOT/state/FLEET-BACKUP.log"
24
+ trap 'echo "[$(date "+%F %T")] === watchdog daemon (shell) STOPPED ===" >> "$AESOP_ROOT/state/FLEET-BACKUP.log"; echo "stopped."; exit 0' INT TERM
25
+
26
+ if [ "$MODE" = "--once" ]; then
27
+ bash "$AESOP_ROOT/daemons/backup-fleet.sh" 2>&1
28
+ exit 0
29
+ fi
30
+
31
+ n=0
32
+ while true; do
33
+ n=$((n+1))
34
+ out=$(bash "$AESOP_ROOT/daemons/backup-fleet.sh" 2>&1 | tail -2)
35
+ printf '%s cycle #%d\n%s\n' "$(date '+%H:%M:%S')" "$n" "$out"
36
+ sleep 150
37
+ done
@@ -0,0 +1,184 @@
1
+ // Detect and render running Claude agents from transcripts.
2
+ // Usage: node dash-extra.mjs [--json]
3
+ // --json: output JSON array of agents (for web dashboard /data endpoint)
4
+ // (default): render TUI text output (for terminal dashboard)
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ // Configuration from environment
10
+ const AESOP_ROOT = process.env.AESOP_ROOT || path.join(process.env.HOME || '.', 'aesop');
11
+ const TRANSCRIPTS_ROOT = path.resolve(
12
+ process.env.AESOP_TRANSCRIPTS_ROOT || path.join(process.env.HOME || '.', '.claude', 'projects')
13
+ );
14
+ const SCAN_DIR = path.join(AESOP_ROOT, 'scan');
15
+ const ALERTS_LOG = path.join(SCAN_DIR, 'SECURITY-ALERTS.log');
16
+
17
+ // ANSI colors for TUI output
18
+ const c = {
19
+ R: '\x1b[31m',
20
+ G: '\x1b[32m',
21
+ Y: '\x1b[33m',
22
+ M: '\x1b[35m',
23
+ C: '\x1b[36m',
24
+ B: '\x1b[1m',
25
+ D: '\x1b[2m',
26
+ X: '\x1b[0m'
27
+ };
28
+
29
+ const now = Date.now();
30
+ const out = [];
31
+
32
+ // Read alerts log if present
33
+ let slog = [];
34
+ try {
35
+ if (fs.existsSync(ALERTS_LOG)) {
36
+ slog = fs.readFileSync(ALERTS_LOG, 'utf8').split('\n');
37
+ }
38
+ } catch {}
39
+
40
+ // Recursively walk directory tree to find agent-*.jsonl files
41
+ function walk(dir, accumulator) {
42
+ try {
43
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ const fullPath = path.join(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ walk(fullPath, accumulator);
48
+ } else if (/^agent-.*\.jsonl$/.test(entry.name)) {
49
+ accumulator.push(fullPath);
50
+ }
51
+ }
52
+ } catch {}
53
+ }
54
+
55
+ // Find all agent transcript files
56
+ let files = [];
57
+ if (fs.existsSync(TRANSCRIPTS_ROOT)) {
58
+ walk(TRANSCRIPTS_ROOT, files);
59
+ }
60
+
61
+ // Filter: active agents in last 12 minutes, sorted by recency
62
+ files = files
63
+ .map(f => {
64
+ let mtime = 0;
65
+ try {
66
+ mtime = fs.statSync(f).mtimeMs;
67
+ } catch {}
68
+ return { f, mtime };
69
+ })
70
+ .filter(x => now - x.mtime < 12 * 60 * 1000)
71
+ .sort((a, b) => b.mtime - a.mtime)
72
+ .slice(0, 8);
73
+
74
+ // Extract description/hint from first ~60KB of agent transcript
75
+ function label(filePath) {
76
+ try {
77
+ const fd = fs.openSync(filePath, 'r');
78
+ const buf = Buffer.alloc(60000);
79
+ const n = fs.readSync(fd, buf, 0, 60000, 0);
80
+ fs.closeSync(fd);
81
+ const content = buf.toString('utf8', 0, n);
82
+
83
+ // Try to find description or subagent_type in JSON
84
+ let match = content.match(/"description":"([^"]{2,60})"/);
85
+ if (!match) {
86
+ match = content.match(/"subagent_type":"([^"]{2,40})"/);
87
+ }
88
+ return match ? match[1] : '';
89
+ } catch {}
90
+ return '';
91
+ }
92
+
93
+ // TUI output: render heading
94
+ out.push(`${c.B} FLEET AGENTS${c.X} ${c.D}(green=running · severity-colored if flagged)${c.X}`);
95
+
96
+ if (files.length === 0) {
97
+ out.push(` ${c.D}(no active fleet agents in last 12 min)${c.X}`);
98
+ }
99
+
100
+ let runningCount = 0;
101
+
102
+ // Render each agent
103
+ for (const { f, mtime } of files) {
104
+ const basename = path.basename(f);
105
+ const ageSeconds = Math.round((now - mtime) / 1000);
106
+
107
+ // Check if agent is referenced in alerts log
108
+ const alertsForAgent = slog.filter(line => line.includes(basename));
109
+
110
+ let statusColor = ageSeconds < 120 ? c.G : c.D;
111
+ let statusText = ageSeconds < 120 ? 'running' : 'idle';
112
+
113
+ if (ageSeconds < 120) runningCount++;
114
+
115
+ // Recolor based on alert severity
116
+ if (alertsForAgent.some(l => l.includes('SUSPICIOUS'))) {
117
+ statusColor = c.R;
118
+ statusText = 'SUSPICIOUS';
119
+ } else if (alertsForAgent.some(l => / HIGH /.test(l))) {
120
+ statusColor = c.R;
121
+ statusText = 'HIGH';
122
+ } else if (alertsForAgent.some(l => / DRIFT /.test(l))) {
123
+ statusColor = c.M;
124
+ statusText = 'DRIFT';
125
+ } else if (alertsForAgent.some(l => / MED /.test(l))) {
126
+ statusColor = c.Y;
127
+ statusText = 'MED';
128
+ }
129
+
130
+ // Extract agent ID from filename (agent-<id>.jsonl)
131
+ const agentId = basename
132
+ .replace(/^agent-/, '')
133
+ .replace(/\.jsonl$/, '')
134
+ .slice(0, 13);
135
+
136
+ const hint = label(f).slice(0, 38);
137
+
138
+ out.push(
139
+ ` ${statusColor}●${c.X} ${agentId.padEnd(14)} ${c.D}${String(ageSeconds).padStart(4)}s${c.X} ${statusColor}${statusText.padEnd(11)}${c.X}${c.D}${hint}${c.X}`
140
+ );
141
+ }
142
+
143
+ if (files.length > 0) {
144
+ out.push(` ${c.D}${runningCount} running, ${files.length - runningCount} idle (last 12 min)${c.X}`);
145
+ }
146
+
147
+ // Output
148
+ if (process.argv.includes('--json')) {
149
+ // JSON mode: emit agents array for web dashboard
150
+ const agents = [];
151
+ for (const { f, mtime } of files) {
152
+ const basename = path.basename(f);
153
+ const ageSeconds = Math.round((now - mtime) / 1000);
154
+ const alertsForAgent = slog.filter(line => line.includes(basename));
155
+
156
+ let status = ageSeconds < 120 ? 'running' : 'idle';
157
+
158
+ if (alertsForAgent.some(l => l.includes('SUSPICIOUS'))) {
159
+ status = 'SUSPICIOUS';
160
+ } else if (alertsForAgent.some(l => / HIGH /.test(l))) {
161
+ status = 'HIGH';
162
+ } else if (alertsForAgent.some(l => / DRIFT /.test(l))) {
163
+ status = 'DRIFT';
164
+ } else if (alertsForAgent.some(l => / MED /.test(l))) {
165
+ status = 'MED';
166
+ }
167
+
168
+ const agentId = basename
169
+ .replace(/^agent-/, '')
170
+ .replace(/\.jsonl$/, '')
171
+ .slice(0, 13);
172
+
173
+ agents.push({
174
+ id: agentId,
175
+ age_s: ageSeconds,
176
+ status: status,
177
+ hint: label(f).slice(0, 60)
178
+ });
179
+ }
180
+ process.stdout.write(JSON.stringify(agents) + '\n');
181
+ } else {
182
+ // TUI mode: emit colored text
183
+ process.stdout.write(out.join('\n') + '\n');
184
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # Fleet watchdog TUI dashboard. Double-buffered no-flicker render. 4s refresh. CRLF-safe (no line continuations).
3
+ # Launch in its own window: bash aesop/dash/watchdog-gui.sh
4
+ # Set AESOP_ROOT=/path/to/aesop and TRACKED_REPOS before running.
5
+
6
+ AESOP_ROOT="${AESOP_ROOT:-.}"
7
+
8
+ # Color codes (ANSI)
9
+ R=$'\e[31m'
10
+ G=$'\e[32m'
11
+ Y=$'\e[33m'
12
+ M=$'\e[35m'
13
+ C=$'\e[36m'
14
+ B=$'\e[1m'
15
+ D=$'\e[2m'
16
+ X=$'\e[0m'
17
+
18
+ # State files
19
+ BLOG="$AESOP_ROOT/state/FLEET-BACKUP.log"
20
+ SLOG="$AESOP_ROOT/state/SECURITY-ALERTS.log"
21
+ HB="$AESOP_ROOT/state/.watchdog-heartbeat"
22
+ REPOS_FILE="$AESOP_ROOT/state/.watchdog-repos.json"
23
+ HB_DIR="$AESOP_ROOT/state/.heartbeats"
24
+
25
+ SPINNER=0
26
+ FIRST_FRAME=1
27
+
28
+ # Trap Ctrl-C to restore cursor and exit cleanly
29
+ trap 'printf "\033[?25h"; exit 0' INT TERM
30
+ printf '\033[?25l'
31
+
32
+ get_hb_threshold() {
33
+ local name="$1"
34
+ case "$name" in
35
+ *monitor*) echo 3600;;
36
+ *watchdog*) echo 300;;
37
+ *) echo 300;;
38
+ esac
39
+ }
40
+
41
+ render_frame() {
42
+ local FRAME=""
43
+ FRAME="${FRAME}${B}${C}== FLEET WATCHDOG${X}\n"
44
+ FRAME="${FRAME} ${SPIN_CHAR} $(date '+%a %H:%M:%S') Daemon: $WD ${R}HIGH:$HI${X} ${Y}MED:$ME${X}${X}\n"
45
+ FRAME="${FRAME}\n"
46
+ FRAME="${FRAME}${B} REPOS BACKED UP${X}\n"
47
+ if [ -f "$REPOS_FILE" ]; then
48
+ repos_data=$(cat "$REPOS_FILE" 2>/dev/null)
49
+ if [ "$repos_data" != "[]" ] 2>/dev/null; then
50
+ REPOS_LINES=$(echo "$repos_data" | jq -r '.[] | " \(.repo) \(.state) \(.age)"' 2>/dev/null)
51
+ if [ -n "$REPOS_LINES" ]; then
52
+ FRAME="${FRAME}${REPOS_LINES}\n"
53
+ else
54
+ FRAME="${FRAME} ${D}(repos unavailable)${X}\n"
55
+ fi
56
+ else
57
+ FRAME="${FRAME} ${D}(no touched repos yet)${X}\n"
58
+ fi
59
+ else
60
+ FRAME="${FRAME} ${D}(no status)${X}\n"
61
+ fi
62
+ FRAME="${FRAME}\n"
63
+ FRAME="${FRAME}${B} HEARTBEATS${X}\n"
64
+ HB_FOUND=0
65
+ if [ -d "$HB_DIR" ] && [ -n "$(ls -A "$HB_DIR" 2>/dev/null)" ]; then
66
+ for hb_file in "$HB_DIR"/*; do
67
+ if [ -f "$hb_file" ]; then
68
+ name=$(basename "$hb_file")
69
+ epoch=$(head -1 "$hb_file" 2>/dev/null | grep -o '^[0-9]*')
70
+ if [ -n "$epoch" ] && [ "$epoch" -gt 0 ]; then
71
+ HB_FOUND=1
72
+ age=$(( now - epoch ))
73
+ threshold=$(get_hb_threshold "$name")
74
+ if [ "$age" -lt "$threshold" ]; then
75
+ status="${G}ALIVE${X} ${D}age:${age}s${X}"
76
+ else
77
+ status="${R}STALE${X} ${D}age:${age}s${X}"
78
+ fi
79
+ FRAME="${FRAME} $name $status\n"
80
+ fi
81
+ fi
82
+ done
83
+ fi
84
+ if [ "$HB_FOUND" -eq 0 ]; then
85
+ FRAME="${FRAME} ${D}(none)${X}\n"
86
+ fi
87
+ FRAME="${FRAME}\n"
88
+ FRAME="${FRAME}${B} RECENT EVENTS${X}\n"
89
+ if [ -s "$BLOG" ]; then
90
+ BACKUP_LINES=$(tail -3 "$BLOG" 2>/dev/null | sed 's/^/ /')
91
+ FRAME="${FRAME}${BACKUP_LINES}\n"
92
+ else
93
+ FRAME="${FRAME} ${D}(none)${X}\n"
94
+ fi
95
+ FRAME="${FRAME}\n"
96
+ FRAME="${FRAME}${D} Ctrl-C to exit · 4s refresh${X}\n"
97
+ echo -ne "$FRAME"
98
+ }
99
+
100
+ while true; do
101
+ SPINNER_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
102
+ SPINNER_IDX=$(( SPINNER % 10 ))
103
+ SPIN_CHAR="${SPINNER_CHARS:$SPINNER_IDX:1}"
104
+ ((SPINNER++))
105
+
106
+ now=$(date +%s)
107
+ hb=$(cat "$HB" 2>/dev/null)
108
+ case "$hb" in
109
+ ''|*[!0-9]*) hb=0 ;;
110
+ esac
111
+
112
+ if [ "$hb" -gt 0 ]; then
113
+ AGE=$(( now - hb ))
114
+ else
115
+ AGE=99999
116
+ fi
117
+
118
+ WD_THRESH=$(get_hb_threshold "watchdog")
119
+ if [ "$AGE" -lt "$WD_THRESH" ]; then
120
+ WD="${G}ALIVE${X} ${D}(${AGE}s)${X}"
121
+ else
122
+ WD="${R}STALE${X}"
123
+ fi
124
+
125
+ HI=$(grep -v '^RESOLVED-FP' "$SLOG" 2>/dev/null | grep -c ' HIGH '); HI=${HI:-0}
126
+ ME=$(grep -v '^RESOLVED-FP' "$SLOG" 2>/dev/null | grep -c ' MED '); ME=${ME:-0}
127
+
128
+ if [ "$FIRST_FRAME" -eq 1 ]; then
129
+ printf '\033[2J'
130
+ FIRST_FRAME=0
131
+ else
132
+ printf '\033[H'
133
+ fi
134
+
135
+ render_frame | while IFS= read -r line; do
136
+ printf '%b\033[K\n' "$line"
137
+ done
138
+ printf '\033[J'
139
+
140
+ sleep 4
141
+ done
@@ -0,0 +1,146 @@
1
+ # Cardinal Rules for Aesop Orchestration
2
+
3
+ These are the foundational principles that guide all work in an Aesop-driven fleet. Violating these risks cost explosion, data loss, or orchestration breakdown.
4
+
5
+ ## 1. Dispatch model & cost
6
+
7
+ **Rule**: Subagents are ALWAYS Haiku (1/3 Sonnet cost). Orchestrator (Opus) runs on main thread only.
8
+
9
+ **Why**: Haiku at scale (6–8 agents in parallel) costs ~25% of all-Opus fleet while maintaining quality on tiny scoped tasks. Opus reserved for final validation, handoff, and orchestration.
10
+
11
+ **Implementation**:
12
+ - When spawning a new agent, default to Haiku.
13
+ - If you need Opus-tier reasoning, consider decomposing into smaller Haiku tasks first.
14
+ - Track token spend per subagent; alert if spend deviates >20% from baseline.
15
+
16
+ ## 2. TDD-first & parallel domains
17
+
18
+ **Rule**: Test-driven development: failing tests before implementation. Decompose work into tiny scoped domains; one Haiku subagent per domain in parallel.
19
+
20
+ **Why**: Small domains enable parallelism (cheaper, faster). TDD catches bugs early. Tests are living documentation.
21
+
22
+ **Implementation**:
23
+ - Write acceptance criteria first (in your story/ticket).
24
+ - Red: verify tests fail.
25
+ - Green: implement the minimum to pass tests.
26
+ - Refactor: simplify, improve, extend reusable libraries.
27
+ - When a task grows beyond one domain, split it and fan out to parallel Haiku agents.
28
+
29
+ ## 3. Reliability core: inputs always produce outputs
30
+
31
+ **Rule**: Every input (request, event, cycle) must produce an output (brief/log/heartbeat/FAILED). Never wait silently; dispatch next work and offer ideas while waiting.
32
+
33
+ **Why**: Hangs hide cost waste, data loss, and orchestration confusion. Observable failure is better than silent lag.
34
+
35
+ **Implementation**:
36
+ - Daemons emit heartbeats every cycle (even on error).
37
+ - Logs are append-only; every action logged with timestamp.
38
+ - If a subagent stalls >200s, watchdog respawns it.
39
+ - Orchestrator briefs the user with findings while delegating to subagents (never idle).
40
+
41
+ ## 4. Orchestrator isolation: lean context
42
+
43
+ **Rule**: Orchestrator reads ONLY: cardinal rules, STATE.md, BUILDLOG.md, MEMORY.md, and short git one-liners. Dispatch Haiku for research.
44
+
45
+ **Why**: Large orchestrator context = token waste, slower decisions. Brief facts + git log tell the story.
46
+
47
+ **Implementation**:
48
+ - Orchestrator prompt stays <2000 tokens.
49
+ - Long-form analysis → delegated to Haiku researcher agents.
50
+ - Durable checkpoints (STATE.md, BUILDLOG.md) replace ephemeral context.
51
+ - Prompt caching on cardinal rules + memory improves throughput.
52
+
53
+ ## 5. Durable handoff: STATE.md + BUILDLOG.md
54
+
55
+ **Rule**: STATE.md tracks intent/decisions/phase/NEXT STEPS. BUILDLOG.md is append-only snapshots of agent work. On resume, re-sync from disk before acting.
56
+
57
+ **Why**: Sessions end abruptly (wipes, crashes, restarts). Git-committed state survives. Re-syncing prevents duplicate work and data loss.
58
+
59
+ **Implementation**:
60
+ - Before acting, orchestrator reads STATE.md (5 min old max) and BUILDLOG.md (latest 10 entries).
61
+ - After each major step, orchestrator updates STATE.md with new phase + next steps.
62
+ - Subagents append work summaries to BUILDLOG.md (one line per completion).
63
+ - Heartbeats mark liveness; stale heartbeats trigger re-sync.
64
+
65
+ ## 6. Branch discipline & continuous push
66
+
67
+ **Rule**: Feature branches only (never main/master). Continuously push green work to origin. Never amend; create new commits.
68
+
69
+ **Why**: Main branch stays deployable. Continuous push distributes backup risk. Amending hides history.
70
+
71
+ **Implementation**:
72
+ - Create feature/your-task at start.
73
+ - Commit often (every 15–30 min of solid work).
74
+ - Push after every commit (github mirrors your work).
75
+ - Open PR when feature is ready for review.
76
+ - Never force-push (unless explicitly approved for specific commit).
77
+
78
+ ## 7. Control files & single-writer discipline
79
+
80
+ **Rule**: MEMORY.md (keeper writes), STATE.md (orchestrator writes), BUILDLOG.md (append-only, orchestrator appends).
81
+
82
+ **Why**: Contention on shared state causes data loss and confusion. Single-writer enforcement prevents races.
83
+
84
+ **Implementation**:
85
+ - Designate one role per file.
86
+ - Use heartbeats to detect live writers; skip if <200s.
87
+ - On resume, read from disk; never trust in-memory state.
88
+ - Append-only logs never overwrite; oldest entries rotate to archives.
89
+
90
+ ## 8. Secret-scan & version control
91
+
92
+ **Rule**: `secret_scan.py` blocks every push (exit 1 = blocked). No credentials in repos. Sensitive data → private remote (e.g., claude-vault).
93
+
94
+ **Why**: Credentials leak → account compromise. Scanning + gating prevents accidents.
95
+
96
+ **Implementation**:
97
+ - Install secret_scan.py in your aesop/tools/.
98
+ - Run before every push (watchdog daemon does this).
99
+ - If blocked, fix the issue and re-push (never --no-verify).
100
+ - Log blocked attempts to SECURITY-ALERTS.log; triage later.
101
+
102
+ ## 9. Local execution only
103
+
104
+ **Rule**: Python runs locally only (no cloud runners). Reusable scripts live in ~/scripts (indexed in CLAUDE.md); extend existing or add genuinely reusable.
105
+
106
+ **Why**: Cloud execution = latency, cost, complexity, partial failure modes. Local = deterministic, fast, auditable.
107
+
108
+ **Implementation**:
109
+ - When you write a script, ask: "Will this be reused?" If yes, add to ~/scripts and index in CLAUDE.md. If no, keep in scratchpad.
110
+ - Never schedule Python on a cloud agent or workflow.
111
+ - Daemon scripts (watchdog, monitor, tooling) are canonical and live in aesop/tools/.
112
+
113
+ ## 10. Observability & audit trails
114
+
115
+ **Rule**: Every agent run is logged. Every cost tracked. Every security event triaged.
116
+
117
+ **Why**: Observability reveals cost leaks, drift, and breaches. Unreviewed alerts rot into ignored noise.
118
+
119
+ **Implementation**:
120
+ - FLEET-LEDGER.md: one row per agent outcome (timestamp, domain, token spend, result).
121
+ - SECURITY-ALERTS.log: one row per security event (classified REAL/FP, triaged, archived).
122
+ - COST-LOG.md: periodic summaries (spend rate, drivers, regressions, optimization levers).
123
+ - Dashboard shows live status; triaged alerts move to RESOLVED-FP archives.
124
+
125
+ ---
126
+
127
+ ## Enforcement
128
+
129
+ These rules are **guardrails-in-code**:
130
+ - Watchdog daemon enforces secret-scan gate and branch discipline.
131
+ - Monitor auto-detects violations and stages PROPOSALS.md or escalates.
132
+ - Orchestrator reads STATE.md to verify cardinal rule compliance.
133
+ - Violations caught late are logged and triaged; never silently ignored.
134
+
135
+ ## What to do if a rule seems wrong
136
+
137
+ Rules are durable, but not dogmatic. If a rule creates friction:
138
+
139
+ 1. Propose a change (write it to PROPOSALS.md).
140
+ 2. Explain why it's necessary.
141
+ 3. Ask the user for approval.
142
+ 4. Never work around a rule; instead, propose improving it.
143
+
144
+ ---
145
+
146
+ **Why these rules matter**: They ensure that your orchestration fleet operates **reliably** (inputs → outputs), **cheaply** (Haiku scale), **safely** (secrets gated), and **durably** (state survives wipes). Together, they enable you to scale from 1 Opus orchestrator to dozens of parallel Haiku subagents without losing control, visibility, or cost discipline.