@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -4,40 +4,121 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { acquireLock, releaseLock } from '../tools/lock.mjs';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
8
14
|
|
|
9
15
|
// === Configuration ===
|
|
10
|
-
//
|
|
16
|
+
// Helper: load aesop.config.json if it exists
|
|
17
|
+
function loadConfigFile(aesopRoot) {
|
|
18
|
+
try {
|
|
19
|
+
const configPath = path.join(aesopRoot, 'aesop.config.json');
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Parse error or file doesn't exist; ignore
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper: expand ~ and environment variables in paths for portability
|
|
30
|
+
function expandPath(pathStr) {
|
|
31
|
+
if (!pathStr) return pathStr;
|
|
32
|
+
// Expand ~ to home directory
|
|
33
|
+
if (pathStr.startsWith('~')) {
|
|
34
|
+
return path.join(os.homedir(), pathStr.slice(1));
|
|
35
|
+
}
|
|
36
|
+
// Expand environment variables like $VAR or %VAR%
|
|
37
|
+
return pathStr.replace(/\$\{?([A-Z_]+)\}?/gi, (match, varName) => {
|
|
38
|
+
return process.env[varName] || match;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Precedence: env var > config file > built-in default
|
|
11
43
|
const AESOP_ROOT = process.env.AESOP_ROOT || '.';
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
44
|
+
const config = loadConfigFile(AESOP_ROOT);
|
|
45
|
+
|
|
46
|
+
const BRAIN_ROOT = expandPath(
|
|
47
|
+
process.env.BRAIN_ROOT ||
|
|
48
|
+
config.brain_root ||
|
|
49
|
+
path.join(AESOP_ROOT, '..', '.claude')
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const SCRIPTS_ROOT = expandPath(
|
|
53
|
+
process.env.SCRIPTS_ROOT ||
|
|
54
|
+
config.scripts_root ||
|
|
55
|
+
path.join(AESOP_ROOT, '..', 'scripts')
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const TEMP_ROOT = expandPath(
|
|
59
|
+
process.env.TEMP_ROOT ||
|
|
60
|
+
config.temp_root ||
|
|
61
|
+
path.join(os.tmpdir(), 'claude')
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const STATE_DIR = expandPath(
|
|
65
|
+
process.env.AESOP_STATE_ROOT ||
|
|
66
|
+
config.state_root ||
|
|
67
|
+
path.join(AESOP_ROOT, 'state')
|
|
68
|
+
);
|
|
69
|
+
|
|
15
70
|
const MON = path.join(AESOP_ROOT, 'monitor');
|
|
16
|
-
const STATE_DIR = path.join(AESOP_ROOT, 'state');
|
|
17
71
|
|
|
18
|
-
//
|
|
72
|
+
// Config-driven thresholds and feature flags
|
|
19
73
|
let repos = [];
|
|
20
74
|
let logThresholds = { maxLines: 500, maxKb: 40 };
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
75
|
+
let extendedSignals = false;
|
|
76
|
+
|
|
77
|
+
if (config.repos && Array.isArray(config.repos)) {
|
|
78
|
+
repos = config.repos.map(r => r.path);
|
|
35
79
|
}
|
|
80
|
+
if (config.monitor && config.monitor.log_max_lines) {
|
|
81
|
+
logThresholds.maxLines = config.monitor.log_max_lines;
|
|
82
|
+
}
|
|
83
|
+
if (config.monitor && config.monitor.log_max_kb) {
|
|
84
|
+
logThresholds.maxKb = config.monitor.log_max_kb;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Precedence: env > config > default
|
|
88
|
+
// AESOP_EXTENDED_SIGNALS env var takes precedence
|
|
89
|
+
if (process.env.AESOP_EXTENDED_SIGNALS !== undefined) {
|
|
90
|
+
extendedSignals = process.env.AESOP_EXTENDED_SIGNALS === 'true' || process.env.AESOP_EXTENDED_SIGNALS === '1';
|
|
91
|
+
} else if (config.monitor && config.monitor.extended_signals !== undefined) {
|
|
92
|
+
extendedSignals = config.monitor.extended_signals;
|
|
93
|
+
}
|
|
94
|
+
// else default is false (already set above)
|
|
36
95
|
|
|
37
96
|
const now = Date.now();
|
|
38
97
|
const HOUR = 3600e3;
|
|
39
98
|
const DAY = 24 * HOUR;
|
|
40
99
|
|
|
100
|
+
// === Single-instance guard: check own heartbeat at startup ===
|
|
101
|
+
// If heartbeat is <300s old and AESOP_MONITOR_FORCE is not explicitly set to 'true' or '1', skip this cycle
|
|
102
|
+
// (another instance is running). Match the AESOP_EXTENDED_SIGNALS truthiness pattern.
|
|
103
|
+
if (process.env.AESOP_MONITOR_FORCE !== 'true' && process.env.AESOP_MONITOR_FORCE !== '1') {
|
|
104
|
+
const heartbeatPath = path.join(MON, '.monitor-heartbeat');
|
|
105
|
+
try {
|
|
106
|
+
const content = fs.readFileSync(heartbeatPath, 'utf8').trim();
|
|
107
|
+
const epoch = parseInt(content.split('\n')[0], 10);
|
|
108
|
+
if (epoch) {
|
|
109
|
+
const beatAge = now - epoch * 1000;
|
|
110
|
+
const MONITOR_THRESHOLD = 300e3; // 300 seconds
|
|
111
|
+
if (beatAge < MONITOR_THRESHOLD) {
|
|
112
|
+
// Heartbeat is recent; another instance is running. Skip this cycle.
|
|
113
|
+
console.log(`[skip] Monitor already running (heartbeat: ${(beatAge / 1000).toFixed(0)}s ago, threshold: ${MONITOR_THRESHOLD / 1000}s)`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Heartbeat file doesn't exist or is unreadable; proceed with cycle
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
41
122
|
// === Utilities ===
|
|
42
123
|
const sh = (cmd, cwd) => {
|
|
43
124
|
try {
|
|
@@ -87,7 +168,13 @@ const walk = (dir, test, out = [], depth = 0) => {
|
|
|
87
168
|
function checkHeartbeats() {
|
|
88
169
|
const staleLoops = [];
|
|
89
170
|
const beatsDir = path.join(MON, '.heartbeats');
|
|
90
|
-
|
|
171
|
+
// Load thresholds from config or use defaults (in seconds; convert to milliseconds)
|
|
172
|
+
const hbThresholds = (config.monitor && config.monitor.heartbeat_thresholds) || {};
|
|
173
|
+
const thresholds = {
|
|
174
|
+
watchdog: (hbThresholds.watchdog !== undefined ? hbThresholds.watchdog : 300) * 1000,
|
|
175
|
+
monitor: (hbThresholds.monitor !== undefined ? hbThresholds.monitor : 3600) * 1000,
|
|
176
|
+
default: (hbThresholds.default !== undefined ? hbThresholds.default : 1800) * 1000,
|
|
177
|
+
};
|
|
91
178
|
let beatFiles = [];
|
|
92
179
|
try {
|
|
93
180
|
beatFiles = fs.readdirSync(beatsDir).map(f => path.join(beatsDir, f));
|
|
@@ -242,6 +329,8 @@ function detectJunkScripts() {
|
|
|
242
329
|
.slice(0, 8)
|
|
243
330
|
.map(x => `${age(x.ageMs)} ${x.fp.split(/[/\\]/).slice(-2).join('/')}`),
|
|
244
331
|
recentCount: tempScripts.filter(x => now - x.ageMs < HOUR).length,
|
|
332
|
+
// Store all temp scripts for AUTO quarantine action
|
|
333
|
+
_scripts: tempScripts,
|
|
245
334
|
};
|
|
246
335
|
return junk;
|
|
247
336
|
}
|
|
@@ -319,11 +408,27 @@ function detectRespawnWatch() {
|
|
|
319
408
|
function trackCostCadence() {
|
|
320
409
|
const prevStateFile = path.join(MON, '.signal-state.json');
|
|
321
410
|
let prevState = {};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
411
|
+
|
|
412
|
+
// Check file existence first; don't silently fail on parse errors
|
|
413
|
+
if (fs.existsSync(prevStateFile)) {
|
|
414
|
+
try {
|
|
415
|
+
prevState = JSON.parse(fs.readFileSync(prevStateFile, 'utf8'));
|
|
416
|
+
} catch (e) {
|
|
417
|
+
// Parse failure: log warning and preserve a .corrupt copy for evidence
|
|
418
|
+
console.error(`Warning: Failed to parse .signal-state.json: ${e.message}`);
|
|
419
|
+
try {
|
|
420
|
+
const corruptPath = prevStateFile + '.corrupt';
|
|
421
|
+
const content = fs.readFileSync(prevStateFile, 'utf8');
|
|
422
|
+
fs.writeFileSync(corruptPath, content, 'utf8');
|
|
423
|
+
console.error(`Corrupt state preserved to ${corruptPath}`);
|
|
424
|
+
} catch (copyErr) {
|
|
425
|
+
console.error(`Failed to preserve corrupt state: ${copyErr.message}`);
|
|
426
|
+
}
|
|
427
|
+
// Reset to empty state and continue (graceful recovery)
|
|
428
|
+
prevState = {};
|
|
429
|
+
}
|
|
326
430
|
}
|
|
431
|
+
|
|
327
432
|
const cycleCount = (prevState.cycleCount || 0) + 1;
|
|
328
433
|
let costTick = null;
|
|
329
434
|
if (cycleCount % 3 === 0) {
|
|
@@ -350,17 +455,311 @@ function checkUnreviewedPrompts() {
|
|
|
350
455
|
return 0;
|
|
351
456
|
}
|
|
352
457
|
|
|
458
|
+
// 11) Isolation violation detection (rec #5: git-tracked dirs don't flag)
|
|
459
|
+
// Detects worktrees created INSIDE repo root (violation) vs sanctioned ../sibling-wt-* (OK)
|
|
460
|
+
// FP fix: git-tracked source directories (new modules) do NOT flag as violations
|
|
461
|
+
function detectIsolationViolations() {
|
|
462
|
+
if (!fs.existsSync(AESOP_ROOT)) return { violations: [], count: 0 };
|
|
463
|
+
|
|
464
|
+
const violations = [];
|
|
465
|
+
const trackedFiles = new Set();
|
|
466
|
+
|
|
467
|
+
// Get list of git-tracked files to exclude legitimate source dirs
|
|
468
|
+
try {
|
|
469
|
+
const tracked = sh('git ls-files', AESOP_ROOT);
|
|
470
|
+
tracked.split('\n').forEach(f => {
|
|
471
|
+
if (f) {
|
|
472
|
+
const dir = f.split('/')[0];
|
|
473
|
+
trackedFiles.add(dir);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
} catch {
|
|
477
|
+
// If git ls-files fails, proceed without tracked set (less precise but fail-open)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Walk AESOP_ROOT (shallow) looking for dirs with nested .git
|
|
481
|
+
try {
|
|
482
|
+
const entries = fs.readdirSync(AESOP_ROOT, { withFileTypes: true });
|
|
483
|
+
for (const entry of entries) {
|
|
484
|
+
if (!entry.isDirectory()) continue;
|
|
485
|
+
if (entry.name.startsWith('.')) continue;
|
|
486
|
+
if (entry.name === 'node_modules') continue;
|
|
487
|
+
|
|
488
|
+
const dirPath = path.join(AESOP_ROOT, entry.name);
|
|
489
|
+
const gitPath = path.join(dirPath, '.git');
|
|
490
|
+
|
|
491
|
+
// Check if this directory has a .git file or .git dir (worktree indicator)
|
|
492
|
+
try {
|
|
493
|
+
const gitStat = fs.statSync(gitPath);
|
|
494
|
+
// It has .git — check if it's a worktree (contains "gitdir:" or is a symlink)
|
|
495
|
+
const isWorktree = gitStat.isFile() || gitStat.isSymbolicLink();
|
|
496
|
+
|
|
497
|
+
if (isWorktree) {
|
|
498
|
+
// This looks like a worktree. Check if it's git-tracked (FP fix)
|
|
499
|
+
const isTracked = trackedFiles.has(entry.name);
|
|
500
|
+
|
|
501
|
+
if (!isTracked) {
|
|
502
|
+
// Untracked worktree inside repo root = VIOLATION
|
|
503
|
+
violations.push({
|
|
504
|
+
path: entry.name,
|
|
505
|
+
type: 'untracked-worktree-in-root',
|
|
506
|
+
message: `Worktree '${entry.name}' found inside repo root (untracked). Should use sanctioned ../aesop-wt-* sibling instead.`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// If tracked, it's a legitimate source directory; don't flag
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// Entry doesn't have .git or is inaccessible; skip
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
// Directory read failed; fail-open with no violations
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { violations, count: violations.length };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// === AUTO Actions ===
|
|
523
|
+
// Log rotation: invoke rotate_logs.py if available and log needs rotation
|
|
524
|
+
function performAutoLogRotation(logFiles, actionsLogPath) {
|
|
525
|
+
// rotate_logs.py is in tools directory (sibling to monitor)
|
|
526
|
+
let rotateLogsPy = path.join(path.dirname(MON), 'tools', 'rotate_logs.py');
|
|
527
|
+
|
|
528
|
+
// Fallback: look in SCRIPTS_ROOT if not found in tools
|
|
529
|
+
if (!fs.existsSync(rotateLogsPy)) {
|
|
530
|
+
rotateLogsPy = path.join(SCRIPTS_ROOT, 'rotate_logs.py');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Fallback: look in the actual aesop source directory (for tests/CI environments)
|
|
534
|
+
if (!fs.existsSync(rotateLogsPy)) {
|
|
535
|
+
// Try to find the real aesop tools directory by looking for the real monitor/CHARTER.md
|
|
536
|
+
const realMonitorCharter = path.join(__dirname, 'CHARTER.md');
|
|
537
|
+
if (fs.existsSync(realMonitorCharter)) {
|
|
538
|
+
rotateLogsPy = path.join(__dirname, '..', 'tools', 'rotate_logs.py');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!fs.existsSync(rotateLogsPy)) {
|
|
543
|
+
// rotate_logs.py not available; skip (fail-open per CHARTER.md)
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const rotatedLogs = [];
|
|
548
|
+
const logsNeedingRotation = logFiles.filter(l => l.needsRotation);
|
|
549
|
+
|
|
550
|
+
for (const log of logsNeedingRotation) {
|
|
551
|
+
if (!log.exists) continue;
|
|
552
|
+
let logPath;
|
|
553
|
+
if (log.name === 'ACTIONS.log') {
|
|
554
|
+
logPath = path.join(MON, log.name);
|
|
555
|
+
} else {
|
|
556
|
+
logPath = log.name.startsWith('/') ? log.name : path.join(STATE_DIR, log.name);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
// Invoke rotate_logs.py with thresholds from config
|
|
561
|
+
// Use execFileSync instead of execSync for command injection protection (no shell)
|
|
562
|
+
execFileSync('python', [
|
|
563
|
+
rotateLogsPy,
|
|
564
|
+
logPath,
|
|
565
|
+
'--max-lines',
|
|
566
|
+
String(logThresholds.maxLines),
|
|
567
|
+
'--max-bytes',
|
|
568
|
+
String(Math.floor(logThresholds.maxKb * 1024)),
|
|
569
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
570
|
+
rotatedLogs.push(log.name);
|
|
571
|
+
|
|
572
|
+
// Log the AUTO action
|
|
573
|
+
const timestamp = new Date(now).toISOString();
|
|
574
|
+
fs.appendFileSync(actionsLogPath, `[${timestamp}] AUTO action: Log rotation invoked for ${log.name}\n`, 'utf8');
|
|
575
|
+
} catch (e) {
|
|
576
|
+
// Log rotation failed; fail-open (log the error but continue)
|
|
577
|
+
const timestamp = new Date(now).toISOString();
|
|
578
|
+
fs.appendFileSync(actionsLogPath, `[${timestamp}] AUTO action FAILED: Log rotation for ${log.name}: ${e.message}\n`, 'utf8');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return rotatedLogs;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Junk quarantine: move old temp scripts to monitor/quarantine/ with manifest
|
|
586
|
+
function performAutoJunkQuarantine(junkScripts, quarantineDir, manifestPath) {
|
|
587
|
+
if (!Array.isArray(junkScripts) || junkScripts.length === 0) {
|
|
588
|
+
return { quarantined: 0 };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let quarantinedCount = 0;
|
|
592
|
+
const manifestLines = [];
|
|
593
|
+
|
|
594
|
+
// Read existing manifest if it exists
|
|
595
|
+
let existingManifest = '';
|
|
596
|
+
if (fs.existsSync(manifestPath)) {
|
|
597
|
+
try {
|
|
598
|
+
existingManifest = fs.readFileSync(manifestPath, 'utf8');
|
|
599
|
+
} catch {
|
|
600
|
+
// Ignore read errors
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Create quarantine directory if needed
|
|
605
|
+
try {
|
|
606
|
+
fs.mkdirSync(quarantineDir, { recursive: true });
|
|
607
|
+
} catch {
|
|
608
|
+
// Directory creation failed; skip quarantine
|
|
609
|
+
return { quarantined: 0 };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Quarantine each old junk script
|
|
613
|
+
for (const junkItem of junkScripts) {
|
|
614
|
+
if (junkItem.quarantinable && fs.existsSync(junkItem.fp)) {
|
|
615
|
+
try {
|
|
616
|
+
const basename = path.basename(junkItem.fp);
|
|
617
|
+
const quarantinePath = path.join(quarantineDir, basename);
|
|
618
|
+
|
|
619
|
+
// Copy (not move) to quarantine to avoid issues with long paths or multiple instances
|
|
620
|
+
fs.copyFileSync(junkItem.fp, quarantinePath);
|
|
621
|
+
|
|
622
|
+
// Record in manifest
|
|
623
|
+
const timestamp = new Date(now).toISOString();
|
|
624
|
+
const manifestLine = `${timestamp}\t${basename}\t${junkItem.fp}\t${junkItem.size}\tbytes\n`;
|
|
625
|
+
manifestLines.push(manifestLine);
|
|
626
|
+
|
|
627
|
+
quarantinedCount++;
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// Quarantine failed for this item; continue with others
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Append new entries to manifest
|
|
635
|
+
if (manifestLines.length > 0) {
|
|
636
|
+
try {
|
|
637
|
+
if (!existingManifest) {
|
|
638
|
+
// Write header
|
|
639
|
+
fs.writeFileSync(manifestPath, 'timestamp\tfilename\tsource_path\tsize_bytes\tunit\n', 'utf8');
|
|
640
|
+
}
|
|
641
|
+
fs.appendFileSync(manifestPath, manifestLines.join(''), 'utf8');
|
|
642
|
+
} catch {
|
|
643
|
+
// Manifest write failed; continue
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return { quarantined: quarantinedCount };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// === Locking utilities (imported from tools/lock.mjs with fail-closed behavior) ===
|
|
651
|
+
// acquireLock now throws on timeout (fail-closed) instead of proceeding unlocked (P0 wave-8 fix)
|
|
652
|
+
// For emitProposal (monitor context), we wrap it to log errors and skip emission rather than crashing the cycle
|
|
653
|
+
function safeAcquireLock(proposalsFile) {
|
|
654
|
+
try {
|
|
655
|
+
return acquireLock(proposalsFile);
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// Lock acquisition failed; log error and return null
|
|
658
|
+
// Caller (emitProposal) will skip emission in this case
|
|
659
|
+
console.error(`Warning: ${e.message}; skipping proposal emission for this cycle`);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
// === Proposal Emission ===
|
|
666
|
+
// Append PROPOSE-tier signals to monitor/PROPOSALS.md (idempotent per signal key, with atomic locking)
|
|
667
|
+
function emitProposal(signalKey, problem, suggestedChange) {
|
|
668
|
+
const proposalsPath = path.join(MON, 'PROPOSALS.md');
|
|
669
|
+
const timestamp = new Date(now).toISOString();
|
|
670
|
+
|
|
671
|
+
// Acquire lock before read-check-append
|
|
672
|
+
const lockDir = safeAcquireLock(proposalsPath);
|
|
673
|
+
|
|
674
|
+
// If lock acquisition failed (fail-closed), skip emission for this cycle
|
|
675
|
+
if (!lockDir) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
// Read existing proposals to check for duplicate
|
|
681
|
+
let existingContent = '';
|
|
682
|
+
try {
|
|
683
|
+
existingContent = fs.readFileSync(proposalsPath, 'utf8');
|
|
684
|
+
} catch {
|
|
685
|
+
// File doesn't exist yet; start fresh
|
|
686
|
+
if (!fs.existsSync(MON)) {
|
|
687
|
+
fs.mkdirSync(MON, { recursive: true });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Check if this signal key already has an entry (idempotency check)
|
|
692
|
+
if (existingContent.includes(`**Signal:** ${signalKey}`)) {
|
|
693
|
+
// Entry already exists; skip to avoid duplicates
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Append new proposal entry
|
|
698
|
+
const proposal = `
|
|
699
|
+
## ${signalKey} — ${timestamp}
|
|
700
|
+
|
|
701
|
+
**Signal:** ${signalKey}
|
|
702
|
+
|
|
703
|
+
**Problem:**
|
|
704
|
+
${problem}
|
|
705
|
+
|
|
706
|
+
**Suggested change:**
|
|
707
|
+
${suggestedChange}
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
`;
|
|
711
|
+
|
|
712
|
+
try {
|
|
713
|
+
fs.appendFileSync(proposalsPath, proposal, 'utf8');
|
|
714
|
+
} catch (e) {
|
|
715
|
+
// Fail-open: log to BRIEF instead of crashing
|
|
716
|
+
console.error(`Failed to write PROPOSALS.md: ${e.message}`);
|
|
717
|
+
}
|
|
718
|
+
} finally {
|
|
719
|
+
releaseLock(lockDir);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
353
723
|
// === Main ===
|
|
354
724
|
const staleLoops = checkHeartbeats();
|
|
355
725
|
const gitState = checkGitState();
|
|
356
726
|
const memory = checkMemoryFreshness();
|
|
357
727
|
const logFiles = checkLogFiles();
|
|
358
|
-
const
|
|
359
|
-
|
|
728
|
+
const isolationViolations = detectIsolationViolations();
|
|
729
|
+
|
|
730
|
+
// Extended signal checks (5, 6, 8, 10) — skipped if extended_signals is OFF
|
|
731
|
+
const junk = extendedSignals ? detectJunkScripts() : { skipped: true };
|
|
732
|
+
const strayRepo = extendedSignals ? detectStrayRepoScripts() : { skipped: true };
|
|
733
|
+
|
|
360
734
|
const alerts = checkSecurityAlerts();
|
|
361
|
-
|
|
735
|
+
|
|
736
|
+
const respawnWatch = extendedSignals ? detectRespawnWatch() : { skipped: true };
|
|
362
737
|
const { cycleCount, costTick } = trackCostCadence();
|
|
363
|
-
const unreviewedPrompts = checkUnreviewedPrompts();
|
|
738
|
+
const unreviewedPrompts = extendedSignals ? checkUnreviewedPrompts() : { skipped: true };
|
|
739
|
+
|
|
740
|
+
// === Perform AUTO Actions ===
|
|
741
|
+
// (Executed before emitting signals, so outputs reflect actions taken)
|
|
742
|
+
const actionsLogPath = path.join(MON, 'ACTIONS.log');
|
|
743
|
+
|
|
744
|
+
// Ensure MON directory and ACTIONS.log exist
|
|
745
|
+
try {
|
|
746
|
+
fs.mkdirSync(MON, { recursive: true });
|
|
747
|
+
if (!fs.existsSync(actionsLogPath)) {
|
|
748
|
+
fs.writeFileSync(actionsLogPath, '', 'utf8');
|
|
749
|
+
}
|
|
750
|
+
} catch {
|
|
751
|
+
// Ignore directory creation errors
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// AUTO: Log rotation
|
|
755
|
+
performAutoLogRotation(logFiles, actionsLogPath);
|
|
756
|
+
|
|
757
|
+
// AUTO: Junk quarantine (only run if junk check was not skipped)
|
|
758
|
+
const quarantineDir = path.join(MON, 'quarantine');
|
|
759
|
+
const manifestPath = path.join(quarantineDir, 'MANIFEST.tsv');
|
|
760
|
+
if (!junk.skipped && junk._scripts && junk.quarantinable > 0) {
|
|
761
|
+
performAutoJunkQuarantine(junk._scripts, quarantineDir, manifestPath);
|
|
762
|
+
}
|
|
364
763
|
|
|
365
764
|
const signals = {
|
|
366
765
|
timestamp: new Date(now).toISOString(),
|
|
@@ -375,6 +774,7 @@ const signals = {
|
|
|
375
774
|
respawnWatch,
|
|
376
775
|
costTick,
|
|
377
776
|
unreviewedPrompts,
|
|
777
|
+
isolationViolations,
|
|
378
778
|
};
|
|
379
779
|
|
|
380
780
|
const brief = [];
|
|
@@ -422,39 +822,60 @@ if (needsRotation.length === 0) {
|
|
|
422
822
|
}
|
|
423
823
|
brief.push('');
|
|
424
824
|
|
|
425
|
-
brief.push('##
|
|
426
|
-
|
|
427
|
-
brief.push(
|
|
428
|
-
|
|
429
|
-
brief.push(
|
|
430
|
-
for (const
|
|
431
|
-
brief.push(`
|
|
825
|
+
brief.push('## Isolation violations');
|
|
826
|
+
if (isolationViolations.count === 0) {
|
|
827
|
+
brief.push('✓ No worktrees detected inside repo root (sanctioned siblings OK).');
|
|
828
|
+
} else {
|
|
829
|
+
brief.push(`🚨 **${isolationViolations.count} isolation violation(s)** detected (worktrees inside repo root):`);
|
|
830
|
+
for (const v of isolationViolations.violations) {
|
|
831
|
+
brief.push(` - ${v.path}: ${v.message}`);
|
|
432
832
|
}
|
|
433
833
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
834
|
+
brief.push('');
|
|
835
|
+
|
|
836
|
+
// Extended signals section (if disabled, just note they're off; if enabled, show details)
|
|
837
|
+
if (extendedSignals) {
|
|
838
|
+
brief.push('## Junk-script sprawl (temp/scratch)');
|
|
839
|
+
brief.push(`- ${junk.total} total scripts, ${(junk.bytes / 1024).toFixed(0)}kb`);
|
|
840
|
+
brief.push(` Quarantinable (>24h, not live): ${junk.quarantinable}`);
|
|
841
|
+
if (junk.oldest.length > 0) {
|
|
842
|
+
brief.push(' Oldest:');
|
|
843
|
+
for (const o of junk.oldest) {
|
|
844
|
+
brief.push(` ${o}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (strayRepo.length > 0) {
|
|
848
|
+
brief.push('');
|
|
849
|
+
brief.push('## Stray repo scripts (7d)');
|
|
850
|
+
for (const s of strayRepo) {
|
|
851
|
+
brief.push(`- ${s}`);
|
|
852
|
+
}
|
|
439
853
|
}
|
|
854
|
+
brief.push('');
|
|
855
|
+
} else {
|
|
856
|
+
brief.push('## Extended signal checks');
|
|
857
|
+
brief.push('Checks 5 (junk-script sprawl), 6 (stray-repo scripts), 8 (respawn-watch), 10 (unreviewed-prompts) are **extended (off)** — enable via `monitor.extended_signals: true` in aesop.config.json or `AESOP_EXTENDED_SIGNALS=true`.');
|
|
858
|
+
brief.push('');
|
|
440
859
|
}
|
|
441
|
-
brief.push('');
|
|
442
860
|
|
|
443
861
|
brief.push('## Security');
|
|
444
862
|
brief.push(`- Alert log: ${alerts.count} entries, ${alerts.highMedCount} HIGH/MED`);
|
|
445
863
|
brief.push('');
|
|
446
864
|
|
|
447
|
-
|
|
448
|
-
if (
|
|
449
|
-
brief.push('
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
brief.push(
|
|
865
|
+
// Respawn watch (check 8 — extended)
|
|
866
|
+
if (extendedSignals) {
|
|
867
|
+
brief.push('## Respawn watch (Rule 6 retry cap)');
|
|
868
|
+
if (respawnWatch.length === 0) {
|
|
869
|
+
brief.push('✓ No retry-cap breaches (all signatures ≤3 occurrences).');
|
|
870
|
+
} else {
|
|
871
|
+
brief.push(`⚠ **${respawnWatch.length} signature(s) exceeded 3-attempt limit:**`);
|
|
872
|
+
for (const rw of respawnWatch) {
|
|
873
|
+
brief.push(` - ${rw.warning}`);
|
|
874
|
+
}
|
|
875
|
+
brief.push(' (Note: distinguish legitimate fan-outs from identical retries; manual review recommended.)');
|
|
454
876
|
}
|
|
455
|
-
brief.push('
|
|
877
|
+
brief.push('');
|
|
456
878
|
}
|
|
457
|
-
brief.push('');
|
|
458
879
|
|
|
459
880
|
brief.push('## Cost tracking');
|
|
460
881
|
brief.push(`- Cycle: ${cycleCount}${costTick ? ' — tick recorded' : ''}`);
|
|
@@ -463,21 +884,152 @@ if (costTick) {
|
|
|
463
884
|
}
|
|
464
885
|
brief.push('');
|
|
465
886
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
brief.push('');
|
|
887
|
+
// Unreviewed prompts (check 10 — extended)
|
|
888
|
+
if (extendedSignals) {
|
|
889
|
+
brief.push('## Unreviewed prompts');
|
|
890
|
+
brief.push(`- ${unreviewedPrompts} new prompt(s) awaiting semantic review`);
|
|
891
|
+
brief.push('');
|
|
892
|
+
}
|
|
469
893
|
|
|
470
894
|
brief.push('_Refinement points → act per CHARTER.md (AUTO safe, PROPOSE rule changes). Goal is fixed._');
|
|
471
895
|
|
|
472
|
-
//
|
|
896
|
+
// === Emit PROPOSE-tier proposals ===
|
|
897
|
+
// Only emit for signals that warrant user review per CHARTER.md action tiers
|
|
898
|
+
|
|
899
|
+
// Proposals for extended checks (only if extended_signals is ON)
|
|
900
|
+
if (extendedSignals) {
|
|
901
|
+
if (respawnWatch.length > 0) {
|
|
902
|
+
emitProposal(
|
|
903
|
+
'respawn-watch-breach',
|
|
904
|
+
`Rule 6 retry cap breached: ${respawnWatch.length} agent signature(s) appeared >3 times in recent spawn history. This indicates either an intentional parallel fan-out or a hung-agent loop.`,
|
|
905
|
+
`Review FLEET-LEDGER.md to distinguish legitimate concurrent spawns from identical retries. If retries are unintentional, investigate root cause and add guardrails to prevent re-dispatch. Consider updating monitoring thresholds or retry strategy.`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (strayRepo.length > 0) {
|
|
910
|
+
emitProposal(
|
|
911
|
+
'stray-repo-scripts',
|
|
912
|
+
`${strayRepo.length} script file(s) committed to repo root in past 7 days: ${strayRepo.join(', ')}. Scripts should live in dedicated src/ or scripts/ paths, not repo root.`,
|
|
913
|
+
`Move stray scripts to proper paths per project discipline. Update CONTRIBUTING.md if repo structure is ambiguous. Add pre-commit hook or CI check to enforce.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Core proposals (always emitted)
|
|
919
|
+
if (isolationViolations.count > 0) {
|
|
920
|
+
emitProposal(
|
|
921
|
+
'isolation-violation-detected',
|
|
922
|
+
`${isolationViolations.count} worktree(s) detected inside repo root. Agents should create worktrees as sanctioned siblings (../aesop-wt-*), not inside the repo root to maintain isolation.`,
|
|
923
|
+
`Review the violation(s): ${isolationViolations.violations.map(v => v.path).join(', ')}. Move worktrees to ../aesop-wt-<name> location outside repo root. Add pre-commit hook or monitoring to prevent future in-root worktrees.`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (alerts.highMedCount > 0) {
|
|
928
|
+
emitProposal(
|
|
929
|
+
'security-alerts-high-med',
|
|
930
|
+
`${alerts.highMedCount} HIGH/MED security alert(s) in SECURITY-ALERTS.log. These may indicate real vulnerabilities, credential exposure, or false positives requiring review.`,
|
|
931
|
+
`Review each HIGH/MED entry in SECURITY-ALERTS.log. Distinguish real issues (fix immediately) from false positives (mark SUPPRESSED-FP). Update scanning rules if needed to reduce noise.`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (memory.staleCount > 0) {
|
|
936
|
+
emitProposal(
|
|
937
|
+
'stale-memory-files',
|
|
938
|
+
`${memory.staleCount} memory file(s) older than 30 days: ${memory.staleMemories.join(', ')}. Stale memory may indicate obsolete project context or abandoned projects.`,
|
|
939
|
+
`Review stale memory files in keeper. Consolidate, archive, or delete per project lifecycle. Update memory refresh schedule if projects are active but infrequently updated.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Helper: Atomic rename with EPERM/EBUSY retry and cleanup on failure
|
|
944
|
+
function atomicRename(tmpPath, targetPath) {
|
|
945
|
+
const maxRetries = 5;
|
|
946
|
+
const baseDelayMs = 50;
|
|
947
|
+
|
|
948
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
949
|
+
try {
|
|
950
|
+
fs.renameSync(tmpPath, targetPath);
|
|
951
|
+
return true; // Success
|
|
952
|
+
} catch (e) {
|
|
953
|
+
if ((e.code === 'EPERM' || e.code === 'EBUSY') && i < maxRetries - 1) {
|
|
954
|
+
// Retry on Windows EPERM or EBUSY (file held by reader)
|
|
955
|
+
const delayMs = baseDelayMs * (i + 1); // Exponential backoff: 50ms, 100ms, 150ms, 200ms
|
|
956
|
+
const start = Date.now();
|
|
957
|
+
while (Date.now() - start < delayMs) {
|
|
958
|
+
// Busy-wait to avoid scheduling overhead
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
// Final failure or non-retryable error; clean up .tmp file and return false
|
|
962
|
+
try {
|
|
963
|
+
fs.unlinkSync(tmpPath);
|
|
964
|
+
} catch {
|
|
965
|
+
// Cleanup failed; best effort
|
|
966
|
+
}
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Final failure after all retries; clean up and return false
|
|
973
|
+
try {
|
|
974
|
+
fs.unlinkSync(tmpPath);
|
|
975
|
+
} catch {
|
|
976
|
+
// Cleanup failed; best effort
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Write outputs atomically with per-file retry on EPERM
|
|
473
982
|
try {
|
|
474
983
|
fs.mkdirSync(MON, { recursive: true });
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
984
|
+
|
|
985
|
+
// Atomic write for BRIEF.md: write to .tmp, then rename with retry
|
|
986
|
+
const briefPath = path.join(MON, 'BRIEF.md');
|
|
987
|
+
const briefTmpPath = briefPath + '.tmp';
|
|
988
|
+
let briefSuccess = false;
|
|
989
|
+
try {
|
|
990
|
+
fs.writeFileSync(briefTmpPath, brief.join('\n'), 'utf8');
|
|
991
|
+
briefSuccess = atomicRename(briefTmpPath, briefPath);
|
|
992
|
+
if (!briefSuccess) {
|
|
993
|
+
console.error(`Warning: Failed to write BRIEF.md after retries; keeping prior file`);
|
|
994
|
+
}
|
|
995
|
+
} catch (e) {
|
|
996
|
+
console.error(`Warning: Failed to write BRIEF.md: ${e.message}`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Atomic write for SIGNALS.json: write to .tmp, then rename with retry
|
|
1000
|
+
const signalsPath = path.join(MON, 'SIGNALS.json');
|
|
1001
|
+
const signalsTmpPath = signalsPath + '.tmp';
|
|
1002
|
+
let signalsSuccess = false;
|
|
1003
|
+
try {
|
|
1004
|
+
fs.writeFileSync(signalsTmpPath, JSON.stringify(signals, null, 2), 'utf8');
|
|
1005
|
+
signalsSuccess = atomicRename(signalsTmpPath, signalsPath);
|
|
1006
|
+
if (!signalsSuccess) {
|
|
1007
|
+
console.error(`Warning: Failed to write SIGNALS.json after retries; keeping prior file`);
|
|
1008
|
+
}
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
console.error(`Warning: Failed to write SIGNALS.json: ${e.message}`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Always write heartbeat and signal state (these use direct write, no rename)
|
|
1014
|
+
try {
|
|
1015
|
+
fs.writeFileSync(path.join(MON, '.monitor-heartbeat'), String(Math.floor(now / 1000)), 'utf8');
|
|
1016
|
+
} catch (e) {
|
|
1017
|
+
console.error(`Warning: Failed to write heartbeat: ${e.message}`);
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
fs.writeFileSync(path.join(MON, '.signal-state.json'), JSON.stringify({ cycleCount }, null, 2), 'utf8');
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
console.error(`Warning: Failed to write signal state: ${e.message}`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Normalize skipped signals to 0 for summary display
|
|
1026
|
+
const junkQuarantinable = junk.skipped ? 0 : (junk.quarantinable || 0);
|
|
1027
|
+
const strayRepoCount = strayRepo.skipped ? 0 : (Array.isArray(strayRepo) ? strayRepo.length : 0);
|
|
1028
|
+
const respawnWatchCount = respawnWatch.skipped ? 0 : (Array.isArray(respawnWatch) ? respawnWatch.length : 0);
|
|
1029
|
+
|
|
1030
|
+
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: ${junkQuarantinable}, stray-repo-scripts: ${strayRepoCount}, alerts-high-med: ${alerts.highMedCount}, respawn-watch: ${respawnWatchCount}, cycle: ${cycleCount}`;
|
|
479
1031
|
console.log(summaryLine);
|
|
480
1032
|
} catch (e) {
|
|
481
|
-
console.error('
|
|
1033
|
+
console.error('Unexpected error during output write:', e.message);
|
|
482
1034
|
process.exit(1);
|
|
483
1035
|
}
|