@matt82198/aesop 0.1.0-beta.1 → 0.1.0-beta.2

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/README.md CHANGED
@@ -1,4 +1,15 @@
1
- # Aesop — Fable-Fleet Orchestration Harness
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/matt82198/aesop/main/assets/logo.png" alt="Aesop" width="420">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <em>Fable-Fleet Orchestration Harness</em>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@matt82198/aesop"><img src="https://img.shields.io/npm/v/@matt82198/aesop/beta" alt="npm"></a>
11
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@matt82198/aesop" alt="license"></a>
12
+ </p>
2
13
 
3
14
  **Aesop** is an open-source orchestration harness for Claude Code, implementing a cost-optimized, self-healing multi-agent dispatch system. One orchestrator model (Opus/Sonnet) directs a fleet of cheap subagents (Haiku) across durable, observable machinery.
4
15
 
@@ -23,9 +34,12 @@ A filesystem-first orchestration system that:
23
34
 
24
35
  ### Option 1: npm (Recommended for quick scaffolding)
25
36
 
37
+ **Note:** Aesop is currently in beta. Install the prerelease version:
38
+
26
39
  ```bash
27
40
  # Create a new aesop fleet directory
28
- npx @matt82198/aesop my-fleet
41
+ # Using @beta tag to install the latest prerelease version (0.1.0-beta.1)
42
+ npx @matt82198/aesop@beta my-fleet
29
43
  cd my-fleet
30
44
 
31
45
  # Configure
@@ -41,6 +55,12 @@ python ui/serve.py
41
55
 
42
56
  Open `http://localhost:8770` to monitor your fleet.
43
57
 
58
+ Alternatively, install globally:
59
+ ```bash
60
+ npm install -g @matt82198/aesop@beta
61
+ aesop my-fleet
62
+ ```
63
+
44
64
  ### Option 2: git clone (For development or full customization)
45
65
 
46
66
  ```bash
@@ -2,16 +2,21 @@
2
2
 
3
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
4
 
5
- ## 1. Dispatch model & cost
5
+ ## 1. Dispatch model & cost — Main Rule: subagents are ALWAYS cheap tier
6
6
 
7
- **Rule**: Subagents are ALWAYS Haiku (1/3 Sonnet cost). Orchestrator (Opus) runs on main thread only.
7
+ **Rule**: Subagents are ALWAYS Haiku (1/3 Sonnet cost) — **the single most important cost lever**. Orchestrator (Fable/Opus) runs on main thread only, NEVER spawns as a subagent, performs final-catch review itself, and NEVER hand-writes files in normal flow.
8
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.
9
+ **Why**: Haiku at scale (6–8 agents in parallel) costs ~25% of all-Opus fleet while maintaining quality on tiny scoped tasks. Main thread orchestrator preserves context stability and keeps prompt cache warm across turns.
10
+
11
+ **Watchdog & stalls**: The orchestrator's core job is detecting hung Haikus. Signs: agent transcript hasn't advanced for >200s, workflow phase isn't progressing, background task should have exited but hasn't. On detection, TaskStop + relaunch the hung agent. Workflows resume from cache via `resumeFromRunId`; standalone agents respawn fresh.
12
+
13
+ **Retry cap**: 1st–3rd hang = TaskStop + relaunch automatically. On 4th hang, mark BLOCKED in BUILDLOG.md and surface to the user instead of respawning. Prevents infinite loops and ensures visibility.
10
14
 
11
15
  **Implementation**:
12
16
  - When spawning a new agent, default to Haiku.
13
17
  - If you need Opus-tier reasoning, consider decomposing into smaller Haiku tasks first.
14
18
  - Track token spend per subagent; alert if spend deviates >20% from baseline.
19
+ - Watchdog monitors for stalls; never let a stuck Haiku silently block a pipeline.
15
20
 
16
21
  ## 2. TDD-first & parallel domains
17
22
 
@@ -28,15 +33,18 @@ These are the foundational principles that guide all work in an Aesop-driven fle
28
33
 
29
34
  ## 3. Reliability core: inputs always produce outputs
30
35
 
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.
36
+ **Rule**: Every input (request, event, cycle) must produce an output (brief/log/heartbeat/FAILED). **NEVER WAIT**: the orchestrator never goes idle while background work is in flight — dispatch the next unblocked work unit, extend the roadmap, stage ideas, queue facts, and always offer the user an idea for what to do next. "Waiting on X" must come with "meanwhile, doing/proposing Y."
37
+
38
+ **Why**: Hangs hide cost waste, data loss, and orchestration confusion. Observable failure is better than silent lag. Idle time is token waste.
32
39
 
33
- **Why**: Hangs hide cost waste, data loss, and orchestration confusion. Observable failure is better than silent lag.
40
+ **The pride bar**: Never ship known-broken. "An agent returned a brief" and "tests pass" are checkpoints, not completion. Done means: verified end-to-end, briefs cross-checked against reality, test artifacts cleaned, loose ends closed or explicitly logged, nothing known-broken shipped silently. If you'd hesitate to hand it over under your own name, keep going.
34
41
 
35
42
  **Implementation**:
36
43
  - Daemons emit heartbeats every cycle (even on error).
37
44
  - Logs are append-only; every action logged with timestamp.
38
45
  - If a subagent stalls >200s, watchdog respawns it.
39
46
  - Orchestrator briefs the user with findings while delegating to subagents (never idle).
47
+ - Before marking work done, verify end-to-end and cross-check briefs against reality.
40
48
 
41
49
  ## 4. Orchestrator isolation: lean context
42
50
 
@@ -64,28 +72,30 @@ These are the foundational principles that guide all work in an Aesop-driven fle
64
72
 
65
73
  ## 6. Branch discipline & continuous push
66
74
 
67
- **Rule**: Feature branches only (never main/master). Continuously push green work to origin. Never amend; create new commits.
75
+ **Rule**: Feature branches only (never main/master). Continuously push green work to origin. **Never amend; create new commits.** **Never force-push** (unless explicitly approved for a specific commit).
68
76
 
69
- **Why**: Main branch stays deployable. Continuous push distributes backup risk. Amending hides history.
77
+ **Why**: Main branch stays deployable. Continuous push distributes backup risk. Amending hides history; force-pushing erases commits. New commits preserve a clear audit trail.
70
78
 
71
79
  **Implementation**:
72
80
  - Create feature/your-task at start.
73
81
  - Commit often (every 15–30 min of solid work).
74
82
  - Push after every commit (github mirrors your work).
75
83
  - Open PR when feature is ready for review.
76
- - Never force-push (unless explicitly approved for specific commit).
84
+ - If you need to fix a commit, create a new commit with the fix (never amend).
85
+ - Never force-push; if you must rebase, do it on a draft branch before opening a PR.
77
86
 
78
87
  ## 7. Control files & single-writer discipline
79
88
 
80
- **Rule**: MEMORY.md (keeper writes), STATE.md (orchestrator writes), BUILDLOG.md (append-only, orchestrator appends).
89
+ **Rule**: Single-writer control files: MEMORY.md (keeper writes only), STATE.md (orchestrator writes only), BUILDLOG.md (append-only for everyone, never overwrite). Single-instance loops check heartbeat before starting; skip if a live one exists.
81
90
 
82
- **Why**: Contention on shared state causes data loss and confusion. Single-writer enforcement prevents races.
91
+ **Why**: Contention on shared state causes data loss and confusion. Single-writer enforcement prevents races. Append-only-for-everyone prevents accidental overwrites. Single-instance heartbeats prevent duplicate work (e.g., two memory keepers or monitors running simultaneously).
83
92
 
84
93
  **Implementation**:
85
- - Designate one role per file.
86
- - Use heartbeats to detect live writers; skip if <200s.
94
+ - Designate one writer per control file (keeper for MEMORY.md, orchestrator for STATE.md).
95
+ - BUILDLOG.md is append-only; anyone can append, no one edits earlier entries.
96
+ - Before starting a loop, check `~/.claude/loops/<loop-name>.heartbeat`; if exists and recent (<5 min old), skip and exit.
87
97
  - On resume, read from disk; never trust in-memory state.
88
- - Append-only logs never overwrite; oldest entries rotate to archives.
98
+ - Append-only logs never overwrite; oldest entries rotate to archives when >200 lines/20KB.
89
99
 
90
100
  ## 8. Secret-scan & version control
91
101
 
@@ -33,6 +33,19 @@ Total = 1,080 tokens
33
33
  = **90% savings** vs 10 Opus tasks
34
34
  ```
35
35
 
36
+ ## MAIN RULE: Subagents are ALWAYS the cheap tier
37
+
38
+ This is **the single most important cost lever** in the entire system. Every subagent spawned must default to Haiku, never Sonnet or Opus. This rule, more than any other, multiplies your savings at scale.
39
+
40
+ **Why**: Haiku is 1/3 the cost of Sonnet with sufficient capability for scoped domain work. Scaling from 1 subagent to 6–8 in parallel multiplies the per-domain savings — what would cost 10× Opus now costs <1×. Violating this rule (spawning Sonnet/Opus subagents) erases the economic advantage of the entire dispatch model.
41
+
42
+ **The exception**: Only use Sonnet/Opus for a subagent if:
43
+ 1. The task genuinely exceeds Haiku capability (rare for scoped domains), **AND**
44
+ 2. You've decomposed it as far as possible and still can't fit it into Haiku, **AND**
45
+ 3. You have explicit approval to do so.
46
+
47
+ Even then, use Sonnet as a supervisor only (splits work into Haiku subdomains), never Opus as a subagent.
48
+
36
49
  ## Dispatch patterns
37
50
 
38
51
  ### Pattern 1: Fan-out (wide parallelism)
@@ -146,6 +159,20 @@ Is this task scoped to <5 min reasoning?
146
159
  2. **Respawn loop** (same domain 3+ times): subagent repeatedly failing; escalate to Sonnet or orchestrator review.
147
160
  3. **Serial bottleneck** (orchestrator >500 tokens): parallelism breaking down; split smaller or delegate.
148
161
 
162
+ ## Retry cap: Automatic recovery + escalation
163
+
164
+ The orchestrator's watchdog automatically detects hung agents and relaunches them with the same scoped prompt:
165
+
166
+ - **1st–3rd hang**: TaskStop + relaunch automatically. Workflows resume from cache via `resumeFromRunId`; standalone agents respawn fresh.
167
+ - **4th hang**: Mark BLOCKED in BUILDLOG.md and surface to the user instead of respawning.
168
+
169
+ **Why the cap?** Infinite retry loops waste tokens and hide systemic problems. After 3 attempts, a persistent hang indicates either:
170
+ - Task is fundamentally unscopable (too large, too complex)
171
+ - Subagent needs a larger model (escalate to Sonnet or Opus)
172
+ - External dependency is broken (requires human intervention)
173
+
174
+ Surfacing to the user after 3 retries ensures visibility and prevents silent cost bleed.
175
+
149
176
  ### Optimization levers
150
177
 
151
178
  - **Fan-out more tasks**: go from 2 Haiku to 5.
@@ -0,0 +1,162 @@
1
+ # Governance & Control
2
+
3
+ This guide covers the operational patterns that keep orchestration systems coherent, scalable, and auditable at production scale: single-instance loops, single-writer files, asynchronous inbox coordination, log rotation, branching discipline, and security gates.
4
+
5
+ ## Single-instance loops
6
+
7
+ Standing loops (watchdog, monitor, memory keeper, cost tracker, QA verification) must not run in duplicate — that causes contention, conflicting writes, and wasted work.
8
+
9
+ **Pattern**: Every loop checks its own heartbeat file before starting and skips the start if a live one exists.
10
+
11
+ ### Heartbeat protocol
12
+
13
+ 1. Before loop starts, check `~/.claude/loops/<loop-name>.heartbeat`
14
+ 2. If exists and recent (<5 min old), skip and exit
15
+ 3. If missing or stale, create/touch heartbeat file, run loop, delete heartbeat on exit
16
+
17
+ **Example** (pseudo-code):
18
+ ```bash
19
+ HEARTBEAT=~/.claude/loops/memory-keeper.heartbeat
20
+ if [ -f "$HEARTBEAT" ] && [ $(($(date +%s) - $(stat -c %Y "$HEARTBEAT"))) -lt 300 ]; then
21
+ exit 0 # Live keeper running, skip
22
+ fi
23
+ touch "$HEARTBEAT"
24
+ # ... run loop ...
25
+ rm "$HEARTBEAT"
26
+ ```
27
+
28
+ **Why**: Prevents duplicate work, keeps system coherent, and ensures idempotent restarts.
29
+
30
+ ## Single-writer control files
31
+
32
+ Some files must have exactly one writer to prevent race conditions and data loss.
33
+
34
+ **Writers by role**:
35
+ - **MEMORY.md**: memory keeper only
36
+ - **STATE.md**: active orchestrator only
37
+ - **BUILDLOG.md**: append-only (anyone can append, no one edits earlier entries)
38
+ - **Other loops**: append requests to an inbox file, never edit control files directly
39
+
40
+ **Why**: Contention on shared state causes corruption. Single-writer enforcement makes all edits safe.
41
+
42
+ ## Inbox pattern for coordination
43
+
44
+ Loops that need to request memory updates or signal decisions use an append-only inbox. The orchestrator reviews inbox entries on every `/power` cycle.
45
+
46
+ ### Inbox format
47
+
48
+ 1. Create `~/.claude/INBOX.md` (append-only, anyone can append)
49
+ 2. Format: `[TIMESTAMP] <loop-name>: <request>`
50
+ 3. On every `/power`, orchestrator reviews entries, acts on them, moves handled ones to an ACCEPTED/REJECTED log
51
+ 4. Clear INBOX.md when processed
52
+
53
+ **Example**:
54
+ ```
55
+ [2026-07-11 13:45] cost-loop: log monthly spend update to MEMORY.md
56
+ [2026-07-11 14:00] qa-loop: mark feature X as verified, move to shipped log
57
+ ```
58
+
59
+ **Why**: Decouples loop work from orchestrator; loops queue requests without blocking; orchestrator batches decisions on a predictable cadence.
60
+
61
+ ## Log rotation threshold
62
+
63
+ Append-only control files grow over time. When a file exceeds **~200 lines or ~20 KB**, rotate the oldest entries into a dated archive.
64
+
65
+ ### Rotation procedure
66
+
67
+ 1. Copy oldest entries (typically oldest 50%) to `<FILE>-YYYY-MM.md`
68
+ 2. Keep only recent entries in the live `<FILE>.md`
69
+ 3. Commit both old and new files
70
+ 4. Update any references to point to the live file
71
+
72
+ **Examples**:
73
+ - `BUILDLOG.md` → `BUILDLOG-2026-06.md` (archive) + new `BUILDLOG.md` (live)
74
+ - `COST-LOG.md` → `COST-LOG-2026-06.md` (archive) + new `COST-LOG.md` (live)
75
+
76
+ **Why**: Keeps orchestrator context bounded and responsive. Orchestrator reads only recent ~20 entries; old data lives in archives.
77
+
78
+ ## Branch workflow: never straight to main
79
+
80
+ **Production-grade branching** ensures code review, testing, and safety gates on all production merges.
81
+
82
+ ### Feature & fix branches
83
+
84
+ - **Features**: `feat/<topic>` branches off main
85
+ - **Fixes**: `fix/<issue-number>` branches off main
86
+ - **All work**: Branches first, PR after implementation, merge after final-catch review
87
+
88
+ **Never** push directly to main/master. All changes go through:
89
+ 1. Feature/fix branch
90
+ 2. Open PR
91
+ 3. Orchestrator final-catch review
92
+ 4. Merge into main
93
+
94
+ ### Session branches for infrastructure repos
95
+
96
+ - Daily work on infrastructure repos (e.g., `~/.claude`): use `work/session-<date>` branches (e.g., `work/session-2026-07-11`)
97
+ - Auto-commit hooks commit to the current branch, so housekeeping changes land on the session branch automatically
98
+ - On checkpoint, open a PR from session branch → main
99
+ - Orchestrator performs final-catch review and merges when ready
100
+
101
+ ### Backup branches
102
+
103
+ Emergency backup snapshots use `backup/<snapshot-label>` branches. Never merge backups to main; recovery is via cherry-pick from backup → feature → PR → merge.
104
+
105
+ **Why**: Main branch stays deployable. All work visible in PRs. History auditable. Accidental commits prevented.
106
+
107
+ ## Secret-scan gate on every push
108
+
109
+ **Critical rule**: Every push (to any repo, any branch) is preceded by:
110
+
111
+ ```bash
112
+ python ~/scripts/secret_scan.py --staged --repo <repo_path>
113
+ ```
114
+
115
+ ### Gate logic
116
+
117
+ - **Exit code 0**: safe, proceed with push
118
+ - **Exit code 1**: secrets detected, block push until resolved
119
+
120
+ ### What it detects
121
+
122
+ - AWS/API keys, OAuth tokens, credentials
123
+ - Private keys and SSH keys
124
+ - Hardcoded passwords or session tokens
125
+ - Sensitive file patterns
126
+
127
+ ### On blocked push
128
+
129
+ 1. Fix the staged files (remove secrets)
130
+ 2. Re-run secret_scan to verify clean
131
+ 3. Re-push (no `--no-verify` or workarounds)
132
+
133
+ **Why**: Credentials leak → account/system compromise. Scanning + gating prevents accidents and keeps repos clean.
134
+
135
+ ## Version control for the brain
136
+
137
+ Infrastructure repos (`~/.claude` for rules/hooks, `~/scripts` for shared scripts, handoff folders) are version-controlled and pushed to private remotes.
138
+
139
+ **Discipline**:
140
+ 1. Every rule/hook/skill/memory change is committed and pushed as it lands
141
+ 2. On every `/power` session:
142
+ - Verify repos are clean (no uncommitted changes)
143
+ - Verify pushed to remote (no commits ahead)
144
+ - If unexpected changes found, restore from remote before continuing
145
+ 3. Never commit credentials, tokens, session data, or large binaries
146
+
147
+ **Why**: Infrastructure is durable, recoverable, and auditable. Unexpected changes to rules are a Tier-1 incident.
148
+
149
+ ## Violations and escalation
150
+
151
+ Governance violations are detected and escalated:
152
+
153
+ 1. **Watchdog daemon** enforces secret-scan gate and branch discipline
154
+ 2. **Monitor agent** auto-detects control-file violations and stages PROPOSALS.md or escalates
155
+ 3. **Orchestrator** reads STATE.md to verify governance compliance
156
+ 4. **Violations caught late** are logged and triaged; never silently ignored
157
+
158
+ If a rule creates friction, propose a change via PROPOSALS.md (never work around it).
159
+
160
+ ---
161
+
162
+ **Why these patterns matter**: They ensure your orchestration fleet operates **reliably** (inputs → outputs), **safely** (no secret leaks), **coherently** (no data loss from races), and **durably** (recoverable from failures). Together, they enable scaling from 1 orchestrator to dozens of parallel subagents without losing control or visibility.
@@ -9,26 +9,59 @@ A standing background monitor (Haiku subagent) that watches how the multi-agent
9
9
  > or invent new project goals. If it ever thinks the goal should change, it writes a note
10
10
  > in `PROPOSALS.md` and stops there.
11
11
 
12
- ## What it watches
12
+ ## Standing checks (deterministic signal collection)
13
13
 
14
- 1. **Heartbeat health** are loop daemons alive? (watchdog ~300s, monitor ~3600s)
15
- 2. **Security alerts** — any high/medium severity issues in the fleet?
16
- 3. **Log rotation** — do append-only logs need archiving?
17
- 4. **Memory staleness** are project notes up to date?
18
- 5. **Orchestration drift** are agents following the cardinal rules?
14
+ Each cycle, `collect-signals.mjs` runs these checks and emits `BRIEF.md` + `SIGNALS.json`:
15
+
16
+ 1. **Heartbeat liveness check** — read `.heartbeats/*` (epoch on line 1) + legacy beat files
17
+ (`.monitor-heartbeat`, `.watchdog-heartbeat`); flag any older than threshold (watchdog 300s,
18
+ monitor 3600s, others 1800s default) as stale loops; append warning to BRIEF.md.
19
+
20
+ 2. **Git state across repos** — for each repo in config: branch, last commit, uncommitted files,
21
+ commits ahead of remote. Identifies unpushed work and dirty state.
22
+
23
+ 3. **Memory freshness** — scan project memory files (excluding index/inbox); flag any >30 days old;
24
+ append summary to BRIEF.md ("N memories stale — keeper should re-verify").
25
+
26
+ 4. **Log rotation** — check append-only logs against max-lines/max-kb thresholds (configurable,
27
+ default 500 lines / 40 KB). If a log exceeds threshold, emit rotation signal (AUTO tier can
28
+ invoke rotate_logs.py if available); append rotation summary to BRIEF.md.
29
+
30
+ 5. **Junk-script sprawl detection** — scan temp/scratch roots for throwaway `.py`/.mjs`/`.js` files
31
+ older than 24 hours, not in a live session directory (avoid false-positives during active work).
32
+ Count total, estimate quarantinable, list oldest. AUTO tier quarantines confirmed junk into
33
+ `monitor/quarantine/` with manifest.
34
+
35
+ 6. **Stray scripts in repo roots** — scan recent commits (7d) to detect one-off `.py`/`.mjs`/`.sql`
36
+ scripts committed directly to repo root (not under proper src/scripts paths). Flag for cleanup.
37
+
38
+ 7. **Security alert review loop** — tail SECURITY-ALERTS.log for new HIGH/MED entries (skip suppressed);
39
+ each new alert noted in BRIEF.md. Monitor agent does semantic review (REAL vs FP) each cycle.
40
+
41
+ 8. **Respawn watch (Rule 6 retry cap)** — parse agent spawn records; normalize descriptions to
42
+ prompt-signatures (first ~40 chars, lowercased); count occurrences in recent window (last 50 rows).
43
+ Flag any signature appearing >3 times as probable hung-agent loop → rule 6 cap breached.
44
+ Limitation: heuristic may have false positives for intentional similar-task fan-outs.
45
+
46
+ 9. **Cost cadence tracking** — every 3rd cycle, harvest agent spawn ledger and append cost tick
47
+ to COST-LOG.md (cycle count, timestamp, model distribution). Flags non-haiku specializations.
48
+
49
+ 10. **Unreviewed agent prompts** — run fleet-prompt-extractor.py (if available) to collect NEW
50
+ spawns since last review; emit count in SIGNALS.json + note in BRIEF.md.
19
51
 
20
52
  ## Action tiers
21
53
 
22
54
  - **AUTO (apply immediately, then log to `ACTIONS.log`):**
23
55
  - Heartbeat freshness checks (read-only).
24
- - Log rotation (invoke `rotate_logs.py` if available).
25
- - Persist project memory updates (if applicable).
26
- - Quarantine defunct scripts (if a quarantine directory exists).
56
+ - Log rotation (invoke rotate_logs.py if available; fail-open if not).
57
+ - Persist heartbeat write (`.monitor-heartbeat` update after cycle).
58
+ - Quarantine confirmed-junk scripts: move old temp `.py`/`.mjs` (not live session) into
59
+ `monitor/quarantine/` with manifest — never delete outright.
27
60
 
28
61
  - **PROPOSE (write to `PROPOSALS.md`, do NOT apply — needs user approval):**
29
62
  - Changes to cardinal rules or agent configuration.
30
- - Alterations to agent behavior or model choice.
31
- - Deletions of anything outside the monitor's own quarantine.
63
+ - Alterations to agent behavior, model choice, or push targets.
64
+ - Deletions of anything outside monitor's own quarantine.
32
65
  - Any change to orchestration policy.
33
66
 
34
67
  ## Hard guardrails (inviolable)
@@ -36,31 +69,48 @@ A standing background monitor (Haiku subagent) that watches how the multi-agent
36
69
  - Preserve the FIXED GOAL. Improve rules; never rewrite intent.
37
70
  - **Subagents are ALWAYS Haiku** (cost optimization). Orchestrator on main thread only.
38
71
  - NEVER push destructively or to restricted remotes as an AUTO action.
72
+ - NEVER edit cardinal-rules files (CLAUDE.md, skills/, agent configs) — only PROPOSE changes.
39
73
  - Idempotent + additive: safe to run repeatedly; prefer append over overwrite.
40
74
  - Stay cheap: read the signal brief, not raw logs. One tight cycle, then sleep.
75
+ - Robust to missing files: treat missing dirs/logs as empty, never crash.
41
76
 
42
77
  ## Outputs
43
78
 
44
- - `BRIEF.md` — human-readable status snapshot.
45
- - `SIGNALS.json` — machine-readable metrics and findings.
79
+ - `BRIEF.md` — human-readable status snapshot (overwrite each cycle).
80
+ - `SIGNALS.json` — machine-readable metrics and findings (overwrite each cycle).
46
81
  - `ACTIONS.log` — append-only record of AUTO actions taken (timestamped).
47
82
  - `PROPOSALS.md` — staged changes awaiting user approval.
48
- - `quarantine/` (optional) — parked suspect scripts + manifest.
83
+ - `quarantine/` — parked junk scripts + `MANIFEST.tsv` (append-only).
84
+ - `.monitor-heartbeat` — epoch timestamp (line 1) for liveness.
85
+ - `.signal-state.json` — sidecar state (cycle count, seen prompts, etc.).
49
86
 
50
87
  ## Operating it
51
88
 
52
- 1. **Deploy the monitor** under `/power` or as a continuous background task.
53
- 2. **Arm the charter**: customize this file with your project names, repos, and thresholds.
89
+ 1. **Deploy the monitor** as a continuous background task (or invoke manually each cycle).
90
+ 2. **Customize via config**: populate `aesop.config.json` with your repo paths, thresholds.
54
91
  3. **Review PROPOSALS.md** periodically; approve changes or reject them.
55
92
  4. **Check ACTIONS.log** to see what was automated.
56
- 5. **Archive BRIEF.md snapshots** if you want a historical record.
93
+ 5. **Monitor BRIEF.md** for drift signals; respond to findings.
57
94
 
58
95
  ## Customization
59
96
 
60
97
  Edit `collect-signals.mjs` to:
61
- - Add your own repo paths and project names.
62
- - Define custom signal collectors (e.g., junk-script detection).
98
+ - Add/remove checks based on your project needs.
99
+ - Configure paths via environment variables (AESOP_ROOT, BRAIN_ROOT, SCRIPTS_ROOT, TEMP_ROOT)
100
+ or load from aesop.config.json.
63
101
  - Set heartbeat thresholds appropriate to your workflow.
64
- - Integrate security scanners or compliance checks.
102
+ - Integrate custom signal collectors (e.g., compliance checks, custom linters).
103
+
104
+ See comments in `collect-signals.mjs` for extension points. **Keep it CRLF-safe** (no line
105
+ continuations; maintain Windows+POSIX compatibility).
106
+
107
+ ## Single-instance guard
108
+
109
+ Before running, check `.monitor-heartbeat` — if <300s old, skip cycle (another monitor is running).
110
+ After cycle completes, update `.monitor-heartbeat` with current epoch.
111
+
112
+ ## Single-writer discipline
65
113
 
66
- See comments in `collect-signals.mjs` for extension points.
114
+ - Only the monitor edits `BRIEF.md`, `SIGNALS.json`, `ACTIONS.log`, `.monitor-heartbeat`, `.signal-state.json`.
115
+ - External processes (human, other agents) write to `PROPOSALS.md` (append or structured inbox).
116
+ - Quarantine manifest is append-only; never edit entries, only add new ones.
@@ -4,14 +4,41 @@
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { execSync } from 'node:child_process';
7
+ import { execSync, spawnSync } from 'node:child_process';
8
8
 
9
+ // === Configuration ===
10
+ // Load from environment or aesop.config.json; fall back to safe defaults.
9
11
  const AESOP_ROOT = process.env.AESOP_ROOT || '.';
12
+ const BRAIN_ROOT = process.env.BRAIN_ROOT || path.join(AESOP_ROOT, '..', '.claude');
13
+ const SCRIPTS_ROOT = process.env.SCRIPTS_ROOT || path.join(AESOP_ROOT, '..', 'scripts');
14
+ const TEMP_ROOT = process.env.TEMP_ROOT || path.join(AESOP_ROOT, '..', 'AppData', 'Local', 'Temp', 'claude');
10
15
  const MON = path.join(AESOP_ROOT, 'monitor');
16
+ const STATE_DIR = path.join(AESOP_ROOT, 'state');
17
+
18
+ // Optional: load aesop.config.json for repo list
19
+ let repos = [];
20
+ let logThresholds = { maxLines: 500, maxKb: 40 };
21
+ try {
22
+ const configPath = path.join(AESOP_ROOT, 'aesop.config.json');
23
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
24
+ if (config.repos && Array.isArray(config.repos)) {
25
+ repos = config.repos.map(r => r.path);
26
+ }
27
+ if (config.monitor && config.monitor.log_max_lines) {
28
+ logThresholds.maxLines = config.monitor.log_max_lines;
29
+ }
30
+ if (config.monitor && config.monitor.log_max_kb) {
31
+ logThresholds.maxKb = config.monitor.log_max_kb;
32
+ }
33
+ } catch {
34
+ // No config file or parse error; use defaults
35
+ }
36
+
11
37
  const now = Date.now();
12
38
  const HOUR = 3600e3;
13
39
  const DAY = 24 * HOUR;
14
40
 
41
+ // === Utilities ===
15
42
  const sh = (cmd, cwd) => {
16
43
  try {
17
44
  return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -34,103 +61,422 @@ const age = (ms) => {
34
61
  return `${(ms / DAY).toFixed(1)}d`;
35
62
  };
36
63
 
37
- // Main signal collection
38
- const signals = {
39
- heartbeats: [],
40
- rotations: [],
41
- unreviewedPrompts: 0,
42
- staleMemories: [],
43
- respawnWatch: [],
64
+ const walk = (dir, test, out = [], depth = 0) => {
65
+ if (depth > 6) return out;
66
+ let ents;
67
+ try {
68
+ ents = fs.readdirSync(dir, { withFileTypes: true });
69
+ } catch {
70
+ return out;
71
+ }
72
+ for (const e of ents) {
73
+ const fp = path.join(dir, e.name);
74
+ if (e.isDirectory()) {
75
+ if (/node_modules|\.git|target|\.venv|__pycache__/.test(e.name)) continue;
76
+ walk(fp, test, out, depth + 1);
77
+ } else if (test(e.name, fp)) {
78
+ out.push(fp);
79
+ }
80
+ }
81
+ return out;
44
82
  };
45
83
 
46
- const brief = [
47
- `# Aesop Monitor Brief — ${new Date(now).toISOString()}`,
48
- '',
49
- '## Status',
50
- 'Orchestration monitor cycle complete.',
51
- '',
52
- ];
84
+ // === Signal Collectors ===
53
85
 
54
86
  // 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
- ];
87
+ function checkHeartbeats() {
88
+ const staleLoops = [];
89
+ const beatsDir = path.join(MON, '.heartbeats');
90
+ const thresholds = { watchdog: 300e3, monitor: 3600e3, default: 1800e3 };
91
+ let beatFiles = [];
92
+ try {
93
+ beatFiles = fs.readdirSync(beatsDir).map(f => path.join(beatsDir, f));
94
+ } catch {
95
+ // no .heartbeats dir
96
+ }
97
+ const legacyBeats = [
98
+ path.join(MON, '.monitor-heartbeat'),
99
+ path.join(STATE_DIR, '.watchdog-heartbeat'),
100
+ ];
101
+ beatFiles = beatFiles.concat(legacyBeats.filter(f => fs.existsSync(f)));
102
+ for (const fp of beatFiles) {
103
+ try {
104
+ const content = fs.readFileSync(fp, 'utf8').trim();
105
+ const epoch = parseInt(content.split('\n')[0], 10);
106
+ if (!epoch) continue;
107
+ const beatAge = now - epoch * 1000;
108
+ const name = path.basename(fp);
109
+ let threshold = thresholds.default;
110
+ if (name.includes('watchdog')) threshold = thresholds.watchdog;
111
+ if (name.includes('monitor')) threshold = thresholds.monitor;
112
+ if (beatAge > threshold) {
113
+ staleLoops.push({ name, ageMs: beatAge, threshold });
114
+ }
115
+ } catch {
116
+ // skip invalid heartbeat file
117
+ }
118
+ }
119
+ return staleLoops;
120
+ }
66
121
 
67
- beatFiles = beatFiles.concat(legacyBeats.filter((f) => fs.existsSync(f)));
122
+ // 2) Git state check
123
+ function checkGitState() {
124
+ const gitState = [];
125
+ for (const repoPath of repos) {
126
+ if (!fs.existsSync(repoPath)) continue;
127
+ const branch = sh('git rev-parse --abbrev-ref HEAD', repoPath);
128
+ const lastCommit = sh('git log -1 --pretty=%h·%cr·%s', repoPath);
129
+ const dirty = sh('git status --porcelain', repoPath)
130
+ .split('\n')
131
+ .filter(l => l.trim()).length;
132
+ const ahead = sh('git rev-list --count @{u}..HEAD', repoPath) || '?';
133
+ gitState.push({
134
+ repo: path.basename(repoPath),
135
+ branch,
136
+ lastCommit,
137
+ dirty,
138
+ ahead,
139
+ });
140
+ }
141
+ return gitState;
142
+ }
68
143
 
69
- for (const fp of beatFiles) {
144
+ // 3) Memory freshness check
145
+ function checkMemoryFreshness() {
146
+ const memoryDir = path.join(BRAIN_ROOT, 'projects', '*', 'memory');
147
+ const staleMemories = [];
148
+ let memoryCount = 0;
70
149
  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,
150
+ const baseDir = path.join(BRAIN_ROOT, 'projects');
151
+ if (!fs.existsSync(baseDir)) return { count: 0, staleMemories: [], staleCount: 0 };
152
+ const projDirs = fs.readdirSync(baseDir, { withFileTypes: true })
153
+ .filter(d => d.isDirectory())
154
+ .map(d => path.join(baseDir, d.name, 'memory'));
155
+ for (const memDir of projDirs) {
156
+ if (!fs.existsSync(memDir)) continue;
157
+ const memFiles = fs.readdirSync(memDir)
158
+ .filter(f => f.endsWith('.md') && f !== 'MEMORY.md' && f !== 'INBOX.md')
159
+ .map(f => path.join(memDir, f));
160
+ memoryCount += memFiles.length;
161
+ for (const fp of memFiles) {
162
+ const st = stat(fp);
163
+ if (st && now - st.mtimeMs > 30 * DAY) {
164
+ staleMemories.push(path.basename(fp));
165
+ }
166
+ }
167
+ }
168
+ } catch {
169
+ // no memory dir
170
+ }
171
+ return { count: memoryCount, staleMemories, staleCount: staleMemories.length };
172
+ }
173
+
174
+ // 4) Log file status check
175
+ function checkLogFiles() {
176
+ const logFiles = [
177
+ path.join(STATE_DIR, 'FLEET-BACKUP.log'),
178
+ path.join(STATE_DIR, 'SECURITY-ALERTS.log'),
179
+ path.join(MON, 'ACTIONS.log'),
180
+ ];
181
+ const logs = [];
182
+ for (const logPath of logFiles) {
183
+ const st = stat(logPath);
184
+ let sizeKb = 0;
185
+ let lineCount = 0;
186
+ if (st) {
187
+ sizeKb = (st.size / 1024).toFixed(1);
188
+ try {
189
+ const content = fs.readFileSync(logPath, 'utf8');
190
+ lineCount = content.split('\n').filter(l => l.trim()).length;
191
+ } catch {
192
+ // skip
193
+ }
194
+ }
195
+ logs.push({
196
+ name: path.basename(logPath),
197
+ exists: !!st,
198
+ sizeKb,
199
+ lineCount,
200
+ needsRotation: st && (lineCount > logThresholds.maxLines || st.size / 1024 > logThresholds.maxKb),
87
201
  });
88
- } catch {}
202
+ }
203
+ return logs;
204
+ }
205
+
206
+ // 5) Junk-script sprawl detection
207
+ function detectJunkScripts() {
208
+ if (!fs.existsSync(TEMP_ROOT)) return { total: 0, quarantinable: 0, bytes: 0, oldest: [], recentCount: 0 };
209
+ const sessionDirs = (() => {
210
+ try {
211
+ return fs.readdirSync(TEMP_ROOT, { withFileTypes: true })
212
+ .filter(d => d.isDirectory())
213
+ .map(d => path.join(TEMP_ROOT, d.name));
214
+ } catch {
215
+ return [];
216
+ }
217
+ })();
218
+ const liveDirs = new Set();
219
+ for (const root of sessionDirs) {
220
+ const files = walk(root, () => true);
221
+ const newest = Math.max(0, ...files.map(fp => (stat(fp) || { mtimeMs: 0 }).mtimeMs));
222
+ if (now - newest < 2 * HOUR) {
223
+ liveDirs.add(root.replace(/\\/g, '/'));
224
+ }
225
+ }
226
+ const inLiveDir = (fp) => [...liveDirs].some(d => fp.replace(/\\/g, '/').startsWith(d));
227
+ const tempScripts = walk(TEMP_ROOT, n => /\.(py|mjs|js)$/.test(n))
228
+ .map(fp => ({ fp, st: stat(fp) }))
229
+ .filter(x => x.st)
230
+ .map(x => ({
231
+ fp: x.fp,
232
+ ageMs: now - x.st.mtimeMs,
233
+ size: x.st.size,
234
+ quarantinable: now - x.st.mtimeMs > DAY && !inLiveDir(x.fp),
235
+ }));
236
+ const junk = {
237
+ total: tempScripts.length,
238
+ quarantinable: tempScripts.filter(x => x.quarantinable).length,
239
+ bytes: tempScripts.reduce((a, x) => a + x.size, 0),
240
+ oldest: tempScripts
241
+ .sort((a, b) => b.ageMs - a.ageMs)
242
+ .slice(0, 8)
243
+ .map(x => `${age(x.ageMs)} ${x.fp.split(/[/\\]/).slice(-2).join('/')}`),
244
+ recentCount: tempScripts.filter(x => now - x.ageMs < HOUR).length,
245
+ };
246
+ return junk;
247
+ }
248
+
249
+ // 6) Stray scripts in repo roots
250
+ function detectStrayRepoScripts() {
251
+ const strayRepo = [];
252
+ for (const repoPath of repos) {
253
+ if (!fs.existsSync(repoPath)) continue;
254
+ const recent = sh('git log --since="7 days ago" --name-only --pretty=format: --diff-filter=A', repoPath)
255
+ .split('\n')
256
+ .map(s => s.trim())
257
+ .filter(Boolean);
258
+ for (const f of new Set(recent)) {
259
+ if (/^[^/\\]+\.(py|mjs|js|sql)$/.test(f)) {
260
+ strayRepo.push(`${path.basename(repoPath)}: ${f}`);
261
+ }
262
+ }
263
+ }
264
+ return strayRepo;
265
+ }
266
+
267
+ // 7) Security alert review
268
+ function checkSecurityAlerts() {
269
+ const alertLog = path.join(STATE_DIR, 'SECURITY-ALERTS.log');
270
+ const st = stat(alertLog);
271
+ if (!st) return { count: 0, highMedCount: 0 };
272
+ try {
273
+ const content = fs.readFileSync(alertLog, 'utf8');
274
+ const lines = content.split('\n');
275
+ const highMedCount = lines.filter(l => /HIGH|MED/.test(l) && !l.includes('SUPPRESSED-FP')).length;
276
+ return { count: lines.filter(l => l.trim()).length, highMedCount };
277
+ } catch {
278
+ return { count: 0, highMedCount: 0 };
279
+ }
280
+ }
281
+
282
+ // 8) Respawn watch (Rule 6 retry cap)
283
+ function detectRespawnWatch() {
284
+ const respawnWatch = [];
285
+ const ledgerPath = path.join(BRAIN_ROOT, 'FLEET-LEDGER.md');
286
+ if (!fs.existsSync(ledgerPath)) return respawnWatch;
287
+ try {
288
+ const content = fs.readFileSync(ledgerPath, 'utf8');
289
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('|'));
290
+ const windowSize = 50;
291
+ const recentStart = Math.max(0, lines.length - windowSize);
292
+ const signatures = {};
293
+ const normalize = (desc) => (desc || '').substring(0, 40).toLowerCase().trim();
294
+ for (let i = recentStart; i < lines.length; i++) {
295
+ const line = lines[i];
296
+ const parts = line.split('|').map(s => s.trim());
297
+ if (parts.length >= 4) {
298
+ const description = parts[3];
299
+ const sig = normalize(description);
300
+ if (sig) signatures[sig] = (signatures[sig] || 0) + 1;
301
+ }
302
+ }
303
+ for (const [sig, count] of Object.entries(signatures)) {
304
+ if (count > 3) {
305
+ respawnWatch.push({
306
+ signature: sig,
307
+ count,
308
+ warning: `RESPAWN CAP: '${sig}' dispatched ${count} times — investigate hang or mark BLOCKED`,
309
+ });
310
+ }
311
+ }
312
+ } catch {
313
+ // fail-open on error
314
+ }
315
+ return respawnWatch;
316
+ }
317
+
318
+ // 9) Cost cadence tracking
319
+ function trackCostCadence() {
320
+ const prevStateFile = path.join(MON, '.signal-state.json');
321
+ let prevState = {};
322
+ try {
323
+ prevState = JSON.parse(fs.readFileSync(prevStateFile, 'utf8'));
324
+ } catch {
325
+ // no prev state
326
+ }
327
+ const cycleCount = (prevState.cycleCount || 0) + 1;
328
+ let costTick = null;
329
+ if (cycleCount % 3 === 0) {
330
+ costTick = {
331
+ cycle: cycleCount,
332
+ ts: new Date(now).toISOString(),
333
+ summary: 'Cost cycle tick recorded',
334
+ };
335
+ }
336
+ return { cycleCount, costTick };
337
+ }
338
+
339
+ // 10) Unreviewed prompts count
340
+ function checkUnreviewedPrompts() {
341
+ const seenFile = path.join(MON, '.fleet-prompts-seen.json');
342
+ let prevSeen = {};
343
+ try {
344
+ prevSeen = JSON.parse(fs.readFileSync(seenFile, 'utf8'));
345
+ } catch {
346
+ // no prev state
347
+ }
348
+ // Without fleet_prompt_extractor.py available, emit 0
349
+ // In production, invoke spawnSync('python', [...fleet_prompt_extractor.py...])
350
+ return 0;
351
+ }
352
+
353
+ // === Main ===
354
+ const staleLoops = checkHeartbeats();
355
+ const gitState = checkGitState();
356
+ const memory = checkMemoryFreshness();
357
+ const logFiles = checkLogFiles();
358
+ const junk = detectJunkScripts();
359
+ const strayRepo = detectStrayRepoScripts();
360
+ const alerts = checkSecurityAlerts();
361
+ const respawnWatch = detectRespawnWatch();
362
+ const { cycleCount, costTick } = trackCostCadence();
363
+ const unreviewedPrompts = checkUnreviewedPrompts();
364
+
365
+ const signals = {
366
+ timestamp: new Date(now).toISOString(),
367
+ cycleCount,
368
+ heartbeats: { staleCount: staleLoops.length, details: staleLoops },
369
+ git: gitState,
370
+ memory,
371
+ logs: logFiles,
372
+ junk,
373
+ strayRepo,
374
+ alerts,
375
+ respawnWatch,
376
+ costTick,
377
+ unreviewedPrompts,
378
+ };
379
+
380
+ const brief = [];
381
+ brief.push(`# Aesop Monitor Brief — ${signals.timestamp}`);
382
+ brief.push('');
383
+ brief.push('## Heartbeat check');
384
+ if (staleLoops.length === 0) {
385
+ brief.push('✓ All heartbeats fresh (watchdog 300s, monitor 3600s, others 1800s).');
386
+ } else {
387
+ brief.push(`✗ **${staleLoops.length} stale loop(s)** detected:`);
388
+ for (const sl of staleLoops) {
389
+ brief.push(` - ${sl.name}: ${age(sl.ageMs)} (threshold: ${age(sl.threshold)})`);
390
+ }
89
391
  }
392
+ brief.push('');
90
393
 
91
- brief.push('## Heartbeats');
92
- if (signals.heartbeats.length === 0) {
93
- brief.push('No heartbeats detected yet.');
394
+ brief.push('## Git state');
395
+ if (gitState.length === 0) {
396
+ brief.push('No repos configured.');
94
397
  } else {
95
- for (const hb of signals.heartbeats) {
96
- const status = hb.ok ? '✓' : '';
97
- brief.push(`- ${status} ${hb.name}: ${age(hb.ageMs)}`);
398
+ for (const g of gitState) {
399
+ const status = g.dirty === 0 && g.ahead === '0' ? '✓' : '';
400
+ brief.push(`- ${status} **${g.repo}** [${g.branch}] — dirty: ${g.dirty}, ahead: ${g.ahead}`);
401
+ if (g.lastCommit) brief.push(` ${g.lastCommit}`);
98
402
  }
99
403
  }
100
404
  brief.push('');
101
405
 
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
- ];
406
+ brief.push('## Memory');
407
+ brief.push(`- ${memory.count} memory file(s)`);
408
+ if (memory.staleCount > 0) {
409
+ brief.push(` ⚠ **${memory.staleCount} stale** (>30d): ${memory.staleMemories.join(', ')}`);
410
+ }
411
+ brief.push('');
108
412
 
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)`);
413
+ brief.push('## Log files');
414
+ const needsRotation = logFiles.filter(l => l.needsRotation);
415
+ if (needsRotation.length === 0) {
416
+ brief.push('✓ All logs within thresholds.');
417
+ } else {
418
+ brief.push(`⚠ **${needsRotation.length} log(s) need rotation:**`);
419
+ for (const l of needsRotation) {
420
+ brief.push(` - ${l.name}: ${l.lineCount} lines, ${l.sizeKb}kb`);
115
421
  }
116
422
  }
117
423
  brief.push('');
118
424
 
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');
425
+ brief.push('## Junk-script sprawl (temp/scratch)');
426
+ brief.push(`- ${junk.total} total scripts, ${(junk.bytes / 1024).toFixed(0)}kb`);
427
+ brief.push(` Quarantinable (>24h, not live): ${junk.quarantinable}`);
428
+ if (junk.oldest.length > 0) {
429
+ brief.push(' Oldest:');
430
+ for (const o of junk.oldest) {
431
+ brief.push(` ${o}`);
432
+ }
433
+ }
434
+ if (strayRepo.length > 0) {
435
+ brief.push('');
436
+ brief.push('## Stray repo scripts (7d)');
437
+ for (const s of strayRepo) {
438
+ brief.push(`- ${s}`);
439
+ }
440
+ }
126
441
  brief.push('');
127
442
 
443
+ brief.push('## Security');
444
+ brief.push(`- Alert log: ${alerts.count} entries, ${alerts.highMedCount} HIGH/MED`);
445
+ brief.push('');
446
+
447
+ brief.push('## Respawn watch (Rule 6 retry cap)');
448
+ if (respawnWatch.length === 0) {
449
+ brief.push('✓ No retry-cap breaches (all signatures ≤3 occurrences).');
450
+ } else {
451
+ brief.push(`⚠ **${respawnWatch.length} signature(s) exceeded 3-attempt limit:**`);
452
+ for (const rw of respawnWatch) {
453
+ brief.push(` - ${rw.warning}`);
454
+ }
455
+ brief.push(' (Note: distinguish legitimate fan-outs from identical retries; manual review recommended.)');
456
+ }
457
+ brief.push('');
458
+
459
+ brief.push('## Cost tracking');
460
+ brief.push(`- Cycle: ${cycleCount}${costTick ? ' — tick recorded' : ''}`);
461
+ if (costTick) {
462
+ brief.push(` ${costTick.ts}`);
463
+ }
464
+ brief.push('');
465
+
466
+ brief.push('## Unreviewed prompts');
467
+ brief.push(`- ${unreviewedPrompts} new prompt(s) awaiting semantic review`);
468
+ brief.push('');
469
+
470
+ brief.push('_Refinement points → act per CHARTER.md (AUTO safe, PROPOSE rule changes). Goal is fixed._');
471
+
128
472
  // Write outputs
129
473
  try {
130
474
  fs.mkdirSync(MON, { recursive: true });
131
475
  fs.writeFileSync(path.join(MON, 'BRIEF.md'), brief.join('\n'), 'utf8');
132
476
  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.');
477
+ fs.writeFileSync(path.join(MON, '.monitor-heartbeat'), String(Math.floor(now / 1000)), 'utf8');
478
+ const summaryLine = `stale-loops: ${staleLoops.length}, repos-dirty: ${gitState.filter(g => g.dirty > 0).length}, stale-mem: ${memory.staleCount}, logs-need-rotation: ${needsRotation.length}, junk-quarantinable: ${junk.quarantinable}, stray-repo-scripts: ${strayRepo.length}, alerts-high-med: ${alerts.highMedCount}, respawn-watch: ${respawnWatch.length}, cycle: ${cycleCount}`;
479
+ console.log(summaryLine);
134
480
  } catch (e) {
135
481
  console.error('Failed to write signals:', e.message);
136
482
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matt82198/aesop",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
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
5
  "bin": {
6
6
  "aesop": "bin/cli.js"
@@ -45,6 +45,5 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
- "scripts": {
49
- }
48
+ "scripts": {}
50
49
  }