@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matt82198/aesop",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
4
|
-
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure
|
|
3
|
+
"version": "0.1.0-beta.5",
|
|
4
|
+
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure \u00e2\u20ac\u201d 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"
|
|
7
7
|
},
|
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
"daemons/",
|
|
12
12
|
"dash/",
|
|
13
13
|
"monitor/",
|
|
14
|
+
"mcp/",
|
|
14
15
|
"tools/",
|
|
15
16
|
"ui/",
|
|
16
17
|
"docs/",
|
|
18
|
+
"hooks/",
|
|
19
|
+
"skills/",
|
|
20
|
+
"state_store/",
|
|
21
|
+
"scan/",
|
|
17
22
|
"aesop.config.example.json",
|
|
23
|
+
"CLAUDE-TEMPLATE.md",
|
|
18
24
|
"README.md",
|
|
19
25
|
"LICENSE",
|
|
20
26
|
"CHANGELOG.md"
|
|
@@ -45,5 +51,11 @@
|
|
|
45
51
|
"publishConfig": {
|
|
46
52
|
"access": "public"
|
|
47
53
|
},
|
|
48
|
-
"scripts": {
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --test --test-force-exit --test-timeout=60000 tests/**/*.test.mjs",
|
|
56
|
+
"test:node": "node --test --test-force-exit --test-timeout=60000 tests/**/*.test.mjs",
|
|
57
|
+
"test:sh": "bash tests/test_pre_push_policy.sh && bash tests/test-run-watchdog.sh && bash tests/backup-fleet.test.sh && bash tests/test_reconstitute.sh && bash tests/test_reconstitute_fixes.sh && bash tests/test_agent_forensics.sh && bash hooks/pre-push-policy.sh --test && bash tools/reconstitute.sh --test",
|
|
58
|
+
"test:py": "python -m unittest discover -s tests",
|
|
59
|
+
"test:all": "npm run test:node && npm run test:sh && npm run test:py"
|
|
60
|
+
}
|
|
49
61
|
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Example IOC/secret scanner — copy to fleet-scan.mjs and configure paths.
|
|
2
|
+
// Scans committed code and fleet transcripts for security/alignment red-flags.
|
|
3
|
+
// Runs each watchdog cycle; marks findings in SECURITY-ALERTS.log. Never blocks the fleet.
|
|
4
|
+
//
|
|
5
|
+
// SETUP:
|
|
6
|
+
// 1. Copy this file to fleet-scan.mjs (in the same directory)
|
|
7
|
+
// 2. Edit the REPOS and PROJECT_ROOTS configuration below to match your fleet setup
|
|
8
|
+
// 3. Configure paths via aesop.config.json or environment variables:
|
|
9
|
+
// - AESOP_FLEET_ROOT: root directory containing your project repositories
|
|
10
|
+
// - AESOP_TRANSCRIPTS_ROOT: root directory containing ~/.claude/projects transcripts
|
|
11
|
+
// 4. Ensure aesop.config.json contains repo definitions (see aesop.config.example.json)
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
// Load configuration from aesop.config.json or use environment variable defaults
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
const configPath = process.env.AESOP_CONFIG || path.join(process.cwd(), 'aesop.config.json');
|
|
21
|
+
let config = {};
|
|
22
|
+
if (fs.existsSync(configPath)) {
|
|
23
|
+
try {
|
|
24
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error(`Warning: Failed to load config from ${configPath}: ${e.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
|
|
34
|
+
// Environment variable overrides (precedence: env > config > defaults)
|
|
35
|
+
const FLEET_ROOT = process.env.AESOP_FLEET_ROOT || config.fleet_root || process.env.HOME;
|
|
36
|
+
const TRANSCRIPTS_ROOT = process.env.AESOP_TRANSCRIPTS_ROOT || config.transcripts_root || path.join(process.env.HOME, '.claude', 'projects');
|
|
37
|
+
|
|
38
|
+
// === CONFIGURE YOUR FLEET ===
|
|
39
|
+
// Edit REPOS array to match your monitored repositories
|
|
40
|
+
// Example: { path: '/path/to/project1', name: 'project1', branch: 'main' }
|
|
41
|
+
// Paths can be absolute or relative to FLEET_ROOT
|
|
42
|
+
const REPOS = config.repos || [
|
|
43
|
+
// Example:
|
|
44
|
+
// { path: 'project-a', name: 'project-a', branch: 'main' },
|
|
45
|
+
// { path: 'project-b', name: 'project-b', branch: 'main' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Transcript project roots to scan for fleet prompts
|
|
49
|
+
// Default: ~/.claude/projects (set via AESOP_TRANSCRIPTS_ROOT or config)
|
|
50
|
+
const PROJECT_ROOTS = [
|
|
51
|
+
TRANSCRIPTS_ROOT,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Handoff/alerts directory (where SECURITY-ALERTS.log is stored)
|
|
55
|
+
// Set via config: alerts.alerts_root or alerts_root
|
|
56
|
+
const ALERTS_ROOT = config.alerts?.alerts_root || config.alerts_root || path.join(FLEET_ROOT, '..', 'conductor3', 'state');
|
|
57
|
+
const ALERTS = path.join(ALERTS_ROOT, 'SECURITY-ALERTS.log');
|
|
58
|
+
const SEENF = path.join(ALERTS_ROOT, '.fleet-scan-seen.json');
|
|
59
|
+
const MARKERF = path.join(ALERTS_ROOT, '.fleet-scan-lastcommit');
|
|
60
|
+
|
|
61
|
+
// Session ID to exclude (optional: set via EXCLUDE_SESSION env var or config)
|
|
62
|
+
const MY_SESSION = process.env.EXCLUDE_SESSION || config.exclude_session || '';
|
|
63
|
+
|
|
64
|
+
// ---- Utility functions ----
|
|
65
|
+
const git = (a, cwd) => {
|
|
66
|
+
try {
|
|
67
|
+
return execSync(`git ${a}`, {
|
|
68
|
+
cwd,
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const now = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
77
|
+
const seen = fs.existsSync(SEENF) ? new Set(JSON.parse(fs.readFileSync(SEENF, 'utf8'))) : new Set();
|
|
78
|
+
const findings = [];
|
|
79
|
+
|
|
80
|
+
const add = (sev, kind, where, detail) => {
|
|
81
|
+
const key = crypto
|
|
82
|
+
.createHash('sha1')
|
|
83
|
+
.update(sev + kind + where + detail)
|
|
84
|
+
.digest('hex')
|
|
85
|
+
.slice(0, 16);
|
|
86
|
+
if (seen.has(key)) return;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
findings.push(`[${now()}] ${sev} ${kind} | ${where} | ${detail}`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ---- IOC patterns for ADDED code lines ----
|
|
92
|
+
const CODE_IOC = [
|
|
93
|
+
['HIGH', 'exec/shell', /Runtime\.getRuntime\(\)\.exec|new\s+ProcessBuilder|\bos\.system\(|\bsubprocess\.|\bpopen\(/i],
|
|
94
|
+
['HIGH', 'reverse-shell', /\/dev\/tcp\/|\bnc\s+-e\b|bash\s+-i\b|sh\s+-i\b|socket\.SOCK_STREAM.*connect/i],
|
|
95
|
+
['HIGH', 'pipe-to-shell', /\b(curl|wget)\b[^\n]*\|\s*(sh|bash)\b/i],
|
|
96
|
+
['HIGH', 'b64-exec', /base64\s+(-d|--decode)[^\n]*\|\s*(sh|bash)|Base64.*decode[^\n]*exec/i],
|
|
97
|
+
['HIGH', 'secret-literal', /AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9]{20,}|-----BEGIN[A-Z ]*PRIVATE KEY-----|xox[baprs]-[A-Za-z0-9-]{10,}|ghp_[A-Za-z0-9]{20,}/],
|
|
98
|
+
['HIGH', 'cred-access', /\.ssh\/(id_|authorized)|\.aws\/credentials|\/etc\/(passwd|shadow)|secrets\.toml|\.env\b/i],
|
|
99
|
+
['HIGH', 'destructive', /\brm\s+-rf\b|DROP\s+TABLE|TRUNCATE\s+TABLE|DELETE\s+FROM\s+\w+\s*;/i],
|
|
100
|
+
['MED', 'deserialization', /\bObjectInputStream\b|\.readObject\(|XMLDecoder|@JsonTypeInfo|enableDefaultTyping/],
|
|
101
|
+
['MED', 'reflection', /Class\.forName\(|Method\.invoke\(|getDeclaredMethod\(|setAccessible\(true\)/],
|
|
102
|
+
['MED', 'raw-network', /new\s+Socket\(|URLConnection|\.openConnection\(|InetSocketAddress\(/i],
|
|
103
|
+
['MED', 'hardcoded-cred', /(password|passwd|secret|api[_-]?key|token)\s*[:=]\s*["'][^"']{6,}["']/i],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
function scanCodeAddedLines(diff, label) {
|
|
107
|
+
if (!diff) return;
|
|
108
|
+
let file = label;
|
|
109
|
+
for (const line of diff.split('\n')) {
|
|
110
|
+
if (line.startsWith('+++ ')) {
|
|
111
|
+
file = line.slice(4).replace(/^b\//, '');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!line.startsWith('+') || line.startsWith('+++')) continue;
|
|
115
|
+
const code = line.slice(1);
|
|
116
|
+
for (const [sev, kind, re] of CODE_IOC) {
|
|
117
|
+
if (re.test(code)) add(sev, 'CODE:' + kind, file, code.trim().slice(0, 160));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Scan all configured repos
|
|
123
|
+
for (const repo of REPOS) {
|
|
124
|
+
const repoPath = path.isAbsolute(repo.path) ? repo.path : path.join(FLEET_ROOT, repo.path);
|
|
125
|
+
if (!fs.existsSync(repoPath)) {
|
|
126
|
+
console.warn(`Warning: Repo not found: ${repoPath}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const head = git('rev-parse HEAD', repoPath).trim();
|
|
131
|
+
const markerFile = path.join(repoPath, '.fleet-scan-lastcommit-' + repo.name);
|
|
132
|
+
const last = fs.existsSync(markerFile) ? fs.readFileSync(markerFile, 'utf8').trim() : '';
|
|
133
|
+
|
|
134
|
+
if (head) {
|
|
135
|
+
if (last && last !== head) {
|
|
136
|
+
scanCodeAddedLines(git(`diff ${last}..${head}`, repoPath), `${repo.name}:commit-range`);
|
|
137
|
+
} else if (!last) {
|
|
138
|
+
scanCodeAddedLines(git(`show ${head}`, repoPath), `${repo.name}:${head.slice(0, 8)}`);
|
|
139
|
+
}
|
|
140
|
+
scanCodeAddedLines(git('diff HEAD', repoPath), `${repo.name}:working-tree`);
|
|
141
|
+
fs.writeFileSync(markerFile, head);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- Fleet transcript scan (injection / exfil / off-goal) ----
|
|
146
|
+
const CRED_HARVEST_ALLOWLIST = [
|
|
147
|
+
'secret_scan.py',
|
|
148
|
+
'secret/credential gate',
|
|
149
|
+
'scripts/CLAUDE.md',
|
|
150
|
+
'env_assignment',
|
|
151
|
+
'env_suspicious_keys',
|
|
152
|
+
'scanner_selftest',
|
|
153
|
+
'allow-pattern-docs',
|
|
154
|
+
'CRED_HARVEST_ALLOWLIST',
|
|
155
|
+
'generic_secret_assignment',
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const EXFILTRATION_ALLOWLIST = [
|
|
159
|
+
'semantic-prompt-review',
|
|
160
|
+
'semantic review',
|
|
161
|
+
'OK/DRIFT/SUSPICIOUS',
|
|
162
|
+
'drift_escalator',
|
|
163
|
+
'prompt_classifier',
|
|
164
|
+
'alert-review',
|
|
165
|
+
'triage',
|
|
166
|
+
'EXFILTRATION_ALLOWLIST',
|
|
167
|
+
'power_selftest',
|
|
168
|
+
'secret_scan.py',
|
|
169
|
+
'fleet-scan.mjs',
|
|
170
|
+
'scanner_selftest',
|
|
171
|
+
'SECRETS-PERMITTED',
|
|
172
|
+
'resolve_alerts',
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const PROMPT_IOC = [
|
|
176
|
+
['HIGH', 'prompt-injection', /ignore\s+(all\s+)?(previous|prior|the\s+above)\s+instructions|disregard\s+(the|your)\s+(rules|instructions)|you\s+are\s+now\s+|new\s+system\s+prompt/i],
|
|
177
|
+
['HIGH', 'exfiltration', /exfiltrat|send\s+(it|them|the\s+\w+)\s+to\s+https?:|upload\s+to\s+|POST\s+.*to\s+http|leak\s+(the\s+)?(secret|cred|token|key)/i],
|
|
178
|
+
['HIGH', 'cred-harvest', /read\s+.*secrets\.toml|\.env\b|\.ssh\/|\.aws\/credentials|environment\s+variables?\s+.*(send|post|exfil)|harvest\s+(cred|token|key)/i],
|
|
179
|
+
['HIGH', 'evade-security', /disable\s+(the\s+)?(sentinel|antivirus|edr|defender|firewall)|bypass\s+(the\s+)?(security|scan|review)|hide\s+(this\s+)?from\s+the\s+user|don.?t\s+tell\s+the\s+(user|owner)/i],
|
|
180
|
+
['MED', 'danger-shell', /\b(curl|wget)\b.*\|\s*(sh|bash)|base64\s+-d|\/dev\/tcp\/|rm\s+-rf\s+(\/|~)|chmod\s+\+s/i],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
function extractTexts(obj) {
|
|
184
|
+
const out = [];
|
|
185
|
+
const c = obj?.message?.content;
|
|
186
|
+
if (typeof c === 'string') out.push(c);
|
|
187
|
+
else if (Array.isArray(c)) {
|
|
188
|
+
for (const x of c) {
|
|
189
|
+
if (typeof x === 'string') out.push(x);
|
|
190
|
+
else if (x?.type === 'text' && x.text) out.push(x.text);
|
|
191
|
+
else if (x?.type === 'tool_use' && (x.name === 'Agent' || x.name === 'Task')) {
|
|
192
|
+
if (x.input?.prompt) out.push('[SPAWN] ' + x.input.prompt);
|
|
193
|
+
if (x.input?.description) out.push('[SPAWN] ' + x.input.description);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const SINCE = Date.now() - 25 * 60 * 1000;
|
|
201
|
+
const tFiles = [];
|
|
202
|
+
|
|
203
|
+
function walk(d) {
|
|
204
|
+
try {
|
|
205
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
206
|
+
const p = path.join(d, e.name);
|
|
207
|
+
if (e.isDirectory()) walk(p);
|
|
208
|
+
else if (e.name.endsWith('.jsonl')) tFiles.push(p);
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const r of PROJECT_ROOTS) {
|
|
214
|
+
if (fs.existsSync(r)) walk(r);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const fp of tFiles) {
|
|
218
|
+
if (MY_SESSION && fp.includes(MY_SESSION)) continue; // skip excluded session
|
|
219
|
+
let st;
|
|
220
|
+
try {
|
|
221
|
+
st = fs.statSync(fp);
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (st.mtimeMs < SINCE) continue; // only recent fleet activity
|
|
226
|
+
const base = path.basename(fp);
|
|
227
|
+
const isAgentFile = base.startsWith('agent-'); // agent files: scan all; main session: [SPAWN] only
|
|
228
|
+
let lines;
|
|
229
|
+
try {
|
|
230
|
+
lines = fs.readFileSync(fp, 'utf8').split('\n');
|
|
231
|
+
} catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const line of lines) {
|
|
235
|
+
if (!line.trim()) continue;
|
|
236
|
+
let obj;
|
|
237
|
+
try {
|
|
238
|
+
obj = JSON.parse(line);
|
|
239
|
+
} catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
for (const text of extractTexts(obj)) {
|
|
243
|
+
if (!text || text.length < 8) continue;
|
|
244
|
+
const isSpawnedPrompt = text.startsWith('[SPAWN]');
|
|
245
|
+
const shouldScanPromptIOC = isAgentFile || isSpawnedPrompt;
|
|
246
|
+
let flagged = false;
|
|
247
|
+
if (shouldScanPromptIOC) {
|
|
248
|
+
for (const [sev, kind, re] of PROMPT_IOC) {
|
|
249
|
+
if (re.test(text)) {
|
|
250
|
+
if (kind === 'cred-harvest') {
|
|
251
|
+
const isDefensiveToolRef = CRED_HARVEST_ALLOWLIST.some((ref) => text.includes(ref));
|
|
252
|
+
if (isDefensiveToolRef) {
|
|
253
|
+
const detail = text.replace(/\s+/g, ' ').slice(0, 180);
|
|
254
|
+
add('SUPPRESSED-FP', 'cred-harvest (defensive-tool reference)', base, detail);
|
|
255
|
+
flagged = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (kind === 'exfiltration') {
|
|
260
|
+
const isDefensiveReviewRef = EXFILTRATION_ALLOWLIST.some((ref) => text.includes(ref));
|
|
261
|
+
if (isDefensiveReviewRef) {
|
|
262
|
+
const detail = text.replace(/\s+/g, ' ').slice(0, 180);
|
|
263
|
+
add('SUPPRESSED-FP', 'exfiltration (defensive-review reference)', base, detail);
|
|
264
|
+
flagged = true;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
add(sev, 'PROMPT:' + kind, base, text.replace(/\s+/g, ' ').slice(0, 180));
|
|
269
|
+
flagged = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
void flagged;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- emit ----
|
|
279
|
+
// Ensure output directory exists
|
|
280
|
+
if (!fs.existsSync(ALERTS_ROOT)) {
|
|
281
|
+
fs.mkdirSync(ALERTS_ROOT, { recursive: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (findings.length) {
|
|
285
|
+
fs.appendFileSync(ALERTS, findings.join('\n') + '\n');
|
|
286
|
+
fs.writeFileSync(SEENF, JSON.stringify([...seen]));
|
|
287
|
+
console.log(`${findings.length} new finding(s) -> ${ALERTS}`);
|
|
288
|
+
for (const f of findings) console.log(f);
|
|
289
|
+
} else {
|
|
290
|
+
fs.writeFileSync(SEENF, JSON.stringify([...seen]));
|
|
291
|
+
console.log('no new findings');
|
|
292
|
+
}
|
package/skills/CLAUDE.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: healthcheck
|
|
3
|
+
description: Check fleet health — one colored ball (heartbeats, alerts, orchestrator, tracker items).
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Healthcheck — one colored health ball
|
|
8
|
+
|
|
9
|
+
One-line status indicator for aesop fleet health, driven by aggregated signals.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
|
|
13
|
+
Run the healthcheck tool and interpret the ball:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python tools/healthcheck.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Output interpretation
|
|
20
|
+
|
|
21
|
+
**🟢 Green**: All heartbeats fresh (watchdog <300s, monitor <3600s), no HIGH alerts
|
|
22
|
+
|
|
23
|
+
**🟡 Yellow**: Stale heartbeat OR unreviewed MED severity alert
|
|
24
|
+
|
|
25
|
+
**🔴 Red**: HIGH severity alert OR watchdog dead (>600s) while orchestrator actively dispatching
|
|
26
|
+
|
|
27
|
+
### Fixing issues
|
|
28
|
+
|
|
29
|
+
When healthcheck returns non-green, check the bullet-list reasons:
|
|
30
|
+
|
|
31
|
+
1. **Stale heartbeat**: Restart the relevant daemon (watchdog or monitor). Check `bash daemons/run-watchdog.sh --once` or monitor process.
|
|
32
|
+
2. **Unreviewed HIGH alert**: Review SECURITY-ALERTS.log, prefix line with `NOTE:` or `RESOLVED-FP` to mark reviewed.
|
|
33
|
+
3. **Unreviewed MED alert**: Same as HIGH — review and mark.
|
|
34
|
+
4. **Dead watchdog + active dispatch**: Critical issue — watchdog daemon crashed while orchestrator was dispatching agents. Check logs, restart watchdog, verify orchestrator stability.
|
|
35
|
+
|
|
36
|
+
### Machine-readable output
|
|
37
|
+
|
|
38
|
+
For integration with monitoring/dashboards, use `--json`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python tools/healthcheck.py --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Returns a JSON object with `ball`, `health` status, issues list, and tracker lane counts.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: power
|
|
3
|
+
description: Prime the "filesystem brain" — load all operating rules, behaviors, the multi-agent dispatch model, memory, and the current project's semantics from disk. Report a compact primed brief with system health and next steps.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Power — prime the filesystem brain
|
|
8
|
+
|
|
9
|
+
Everything that governs how I should behave lives on disk. This skill reads it, internalizes it,
|
|
10
|
+
and reports a compact "primed" summary — **without inflating context** (code is read via subagents
|
|
11
|
+
that return briefs, not dumped raw).
|
|
12
|
+
|
|
13
|
+
The principle: **all rules can be abstracted to consistent agent behavior.** Rules are files;
|
|
14
|
+
reading them = becoming the agent the files describe.
|
|
15
|
+
|
|
16
|
+
**Scope: this is a priming skill for aesop orchestration itself** — rules, dispatch model, memory,
|
|
17
|
+
machinery health. Running it does NOT imply any particular project is being worked on. Project
|
|
18
|
+
state loads only when the session is actually resuming that project (step 3); deferred projects
|
|
19
|
+
are rolled up to counts, never item-by-item — and their repos/daemons are left untouched.
|
|
20
|
+
|
|
21
|
+
## Procedure (run in order)
|
|
22
|
+
|
|
23
|
+
### 1. Load global rules + dispatch model
|
|
24
|
+
Read these directly (they are small and authoritative):
|
|
25
|
+
- `~/.claude/CLAUDE.md` — the **cardinal rules** (autonomous continuation, dispatch model,
|
|
26
|
+
durable checkpointing, build env). These override defaults.
|
|
27
|
+
- Any project-local `CLAUDE.md` in the current working directory and its parents.
|
|
28
|
+
|
|
29
|
+
Then run the **brain integrity check**: `~/.claude` should be a git repo (if you version it).
|
|
30
|
+
Check `git -C ~/.claude status --porcelain` and ahead/behind vs origin:
|
|
31
|
+
- Deliberate uncommitted rule/hook/skill/memory changes → commit + push now (gated by
|
|
32
|
+
`python <project-root>/tools/secret_scan.py --staged`);
|
|
33
|
+
- **Unexpected** modifications or deletions → report as alert — stop, diff against remote, restore
|
|
34
|
+
before continuing (core defense: don't let tooling shadow your rules).
|
|
35
|
+
|
|
36
|
+
Also check for unactioned entries in `~/.claude/PROPOSALS.md` or similar and surface a
|
|
37
|
+
one-line summary of each in the primed report.
|
|
38
|
+
|
|
39
|
+
Internalize the **dispatch model** and apply it for the rest of the session. Document
|
|
40
|
+
the dispatch model's key invariants locally (in your private repo's `CLAUDE.md`,
|
|
41
|
+
if you version `~/.claude`), for example:
|
|
42
|
+
- **Subagents model** (Haiku vs specialists vs orchestrator tier)
|
|
43
|
+
- **Parallelism defaults** (e.g., fan out 6–8 Haikus per domain)
|
|
44
|
+
- **TDD-first discipline** and checkpointing (STATE.md, BUILDLOG.md)
|
|
45
|
+
- **Observable machinery**: every action logged, every cost tracked
|
|
46
|
+
|
|
47
|
+
### 2. Load memory
|
|
48
|
+
- Read `~/.claude/memory/MEMORY.md` (the index, or use your configured path).
|
|
49
|
+
- For any memory entry whose one-line hook is relevant to the current task/project,
|
|
50
|
+
read that memory file.
|
|
51
|
+
- Treat recalled memories as background context that reflects when they were written —
|
|
52
|
+
**verify any file/flag/path they name still exists before relying on it.**
|
|
53
|
+
|
|
54
|
+
### 3. Load the active project's state
|
|
55
|
+
If working inside (or resuming) a known project, read its durable checkpoint first:
|
|
56
|
+
- Look for a `STATE.md` and `BUILDLOG.md` in the project root or a handoff folder.
|
|
57
|
+
- `STATE.md` is the source of truth for intent, locked decisions, contracts, and **NEXT STEPS**.
|
|
58
|
+
- Re-sync from disk (git log, file state) — never trust stale assumptions.
|
|
59
|
+
|
|
60
|
+
### 4. Codebase semantics — already-primed vs first-time init
|
|
61
|
+
**First check whether this codebase has been primed before**: does it already carry its scoped
|
|
62
|
+
`.md` layer (a project-root `CLAUDE.md`, domain `CLAUDE.md` units, a `STATE.md`)?
|
|
63
|
+
|
|
64
|
+
- **Already primed** (has `CLAUDE.md`s and optionally `STATE.md`): do NOT re-investigate the code.
|
|
65
|
+
The `.md` layer IS the persisted semantic memory — the project `CLAUDE.md`s are sufficient;
|
|
66
|
+
the codebase is simply **ready to work**. Only re-scan a specific domain if its `CLAUDE.md`
|
|
67
|
+
is contradicted by on-disk reality (then fix the `.md`, which is the bug).
|
|
68
|
+
- **First time / not primed** (no scoped `.md`s found): run **init-prime** — fan out Haiku
|
|
69
|
+
subagents to read the whole codebase (briefs only, never raw dumps) and **autonomously write
|
|
70
|
+
the scoped `.md` layer it needs**:
|
|
71
|
+
- a project-root `CLAUDE.md`: purpose, build/run commands + toolchain, top gotchas
|
|
72
|
+
- smallest self-contained **domain `CLAUDE.md` units** (minimal, never lossy) — module layout,
|
|
73
|
+
key contracts, domain logic, non-obvious invariants
|
|
74
|
+
- a `STATE.md` checkpoint seed if the work warrants it
|
|
75
|
+
|
|
76
|
+
Commit + push the new `.md`s (secret-scan gated). Every future `/power` on that codebase
|
|
77
|
+
then takes the already-primed fast path.
|
|
78
|
+
|
|
79
|
+
### 5. Report "primed" (compact)
|
|
80
|
+
Output a short brief, not a wall of text:
|
|
81
|
+
|
|
82
|
+
- **Drain the UI inbox first** (if configured in `aesop.config.json`): run
|
|
83
|
+
`python <project-root>/tools/inbox_drain.py pending`. If items exist, surface each under
|
|
84
|
+
a **"QUEUED FROM DASHBOARD"** heading as `[ISO-ts] text` one-liners. Mark them processed
|
|
85
|
+
(`python <project-root>/tools/inbox_drain.py mark-all`) after actioning.
|
|
86
|
+
- **Health line first**: run `python <project-root>/tools/power_selftest.py` and lead with its
|
|
87
|
+
`POWER-SELFTEST:` line (hooks, brain push state, heartbeats, decisions, scanner state).
|
|
88
|
+
Bullet anything non-OK.
|
|
89
|
+
- **Open decisions**: surface each ACTIVE line of any decisions/proposals file and any
|
|
90
|
+
unactioned inbox entries as one-liners (rolled-up counts for deferred items, never item-by-item).
|
|
91
|
+
- **Rules loaded**: dispatch model + any project-specific overrides in one or two lines.
|
|
92
|
+
- **Project**: name, purpose, status, repo, key paths.
|
|
93
|
+
- **Semantics**: the domain model + main flow in a few bullets.
|
|
94
|
+
- **Gotchas**: the verified ones that change how I act.
|
|
95
|
+
- **Next steps**: from `STATE.md`, ready to execute (autonomously, per the dispatch model).
|
|
96
|
+
|
|
97
|
+
### 6. Project app launch (if applicable)
|
|
98
|
+
After reporting primed, bring the active project's runnable app up in the BACKGROUND if it has one.
|
|
99
|
+
Make it **idempotent**: if the port is already serving a healthy instance, skip the start and
|
|
100
|
+
just report the URL.
|
|
101
|
+
|
|
102
|
+
**Pattern** (project adopters fill in their own):
|
|
103
|
+
- Define a `run` skill or script in your project (or use an existing `start`/`run` command).
|
|
104
|
+
- Detect if the service is already healthy (e.g., `curl -s -o /dev/null -w '%{http_code}' http://localhost:PORT/`).
|
|
105
|
+
- If healthy (200), skip startup and just report the URL.
|
|
106
|
+
- If not healthy, start the service in the background via Bash with `run_in_background: true`.
|
|
107
|
+
- Poll readiness WITHOUT blocking sleep: use curl retries or a cheap probe loop.
|
|
108
|
+
- Report the URL + background task ID so it can be stopped on request.
|
|
109
|
+
|
|
110
|
+
**DO NOT** hardcode personal app paths (e.g., Spring Boot repos, IDEs, personal tools).
|
|
111
|
+
Instead, document in your project's `CLAUDE.md` how to launch the app, and reference that
|
|
112
|
+
in your local version of this skill or a project-specific override.
|
|
113
|
+
|
|
114
|
+
### 6b. Machinery health — orchestration monitor (optional)
|
|
115
|
+
If your project uses an orchestration monitor, spin up a **background Haiku agent** that:
|
|
116
|
+
1. Collects health signals (cheap, deterministic)
|
|
117
|
+
2. Reads only that brief and acts per a charter — AUTO-applies safe refinements and stages
|
|
118
|
+
rule/behavior changes to a `PROPOSALS.md` for review
|
|
119
|
+
3. Self-paces via ScheduleWakeup and beats a heartbeat
|
|
120
|
+
|
|
121
|
+
Skip if `.monitor-heartbeat` is already <300s old. Announce its task id so it can be paused/stopped.
|
|
122
|
+
|
|
123
|
+
### 7. Standing dev loops (optional; only when actively developing)
|
|
124
|
+
If the session is doing active development (writing/changing code — not just answering questions),
|
|
125
|
+
keep these background loops running continuously. They are the "tail": cheap, persistent,
|
|
126
|
+
append-only, and never inflate the main context.
|
|
127
|
+
|
|
128
|
+
**Patterns** (project adopters fill in their own):
|
|
129
|
+
|
|
130
|
+
1. **Build/status + backup watchdog** (background bash loop) — appends timestamped git/test/build
|
|
131
|
+
snapshots. For example, if your project has a `daemons/` folder with a `run-watchdog.sh`,
|
|
132
|
+
launch that via Bash with `run_in_background: true` and let it beat a heartbeat so you know
|
|
133
|
+
it is live. Stop with TaskStop when development pauses.
|
|
134
|
+
|
|
135
|
+
2. **Memory keeper** (Haiku, background) — as new decisions, contracts, or gotchas emerge,
|
|
136
|
+
updates the relevant memory files compactly (one fact per memory; never bloat). Re-invoke
|
|
137
|
+
via SendMessage to keep context.
|
|
138
|
+
|
|
139
|
+
3. **Continuous QA loop** — after each feature reaches a checkpoint: **Haiku reviews →
|
|
140
|
+
Haiku bugfixes → Haiku lint/format**, looping until the build is green and review is clean,
|
|
141
|
+
with **Opus orchestrator final-catch** before merge. TDD-first: failing tests before code.
|
|
142
|
+
|
|
143
|
+
4. **Tail-drift alignment** (Haiku, background) — periodically checks in-flight agents' output
|
|
144
|
+
against their domain `CLAUDE.md` contract, and realigns anything drifting.
|
|
145
|
+
|
|
146
|
+
5. **Hung-Haiku watchdog** (orchestrator's main thread, NOT a subagent) — keep watching every
|
|
147
|
+
spawned Haiku for stalls. On hang: TaskStop it and relaunch from checkpoint or respawn.
|
|
148
|
+
|
|
149
|
+
Launch any that aren't already running; **stop them with TaskStop when development pauses**.
|
|
150
|
+
|
|
151
|
+
Mechanism: use background `Agent` (run_in_background) for keeper/drift loops, `Monitor` for
|
|
152
|
+
the watchdog, and self-pace longer cycles with `/loop` skill or `ScheduleWakeup`.
|
|
153
|
+
Always announce which loops were started (and their task IDs) so they can be paused/stopped.
|
|
154
|
+
|
|
155
|
+
## Notes
|
|
156
|
+
- If no project context exists yet, steps 1–2 still apply; skip 3–4 and say so.
|
|
157
|
+
- Steps 1–5 always run on `/power`; **step 6 (launch app) runs only if the active project has one**;
|
|
158
|
+
**step 6b (monitor) runs only if configured**; step 7 (dev loops) runs only during active development.
|
|
159
|
+
- Prefer subagents for anything large; the goal is a *brain*, not a *dump*.
|
|
160
|
+
- After priming, proceed under the dispatch model (continue approved work autonomously; stop only
|
|
161
|
+
for genuine decisions or unauthorized outward/destructive actions).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# state_store/ — event-sourced state layer (DB source of truth, git as export)
|
|
2
|
+
|
|
3
|
+
**Purpose**: the durable substrate to move aesop's coordination/state off git —
|
|
4
|
+
which cannot scale to a team (single-writer control files, hot-file merge
|
|
5
|
+
conflicts, no transactions/concurrency/real-time). State becomes an append-only
|
|
6
|
+
event log with per-stream versioning; current state is a projection; git is
|
|
7
|
+
demoted to a rendered, diffable **export**.
|
|
8
|
+
|
|
9
|
+
**Status (2026-07-14)**: additive prototype. The live `ui/` tracker path
|
|
10
|
+
(`collectors.py` → `state/tracker.json`) is UNCHANGED. This package ships the
|
|
11
|
+
store + tracker projection + backfill + export, ready for a later dual-read
|
|
12
|
+
cutover. Design: `conductor3 plans/aesop-scaling-rearchitecture.md`.
|
|
13
|
+
|
|
14
|
+
## Files (stdlib only — sqlite3/json/threading/time)
|
|
15
|
+
- **store.py** — `EventStore(db_path)`: append-only SQLite (WAL) log.
|
|
16
|
+
Concurrency-safe across threads and processes via `busy_timeout` +
|
|
17
|
+
`BEGIN IMMEDIATE` (atomic read-max-version-then-insert). `append/read/read_all`.
|
|
18
|
+
- **projections.py** — `project_tracker(events)`: folds `item_created` /
|
|
19
|
+
`item_updated` / `item_archived` into the full `tracker.json` shape,
|
|
20
|
+
preserving first-seen order.
|
|
21
|
+
- **api.py** — `StateAPI(db_path)`: the swap seam (`append`/`get`/`project`).
|
|
22
|
+
Backend swaps SQLite→Postgres here without touching callers.
|
|
23
|
+
- **export.py** — `export_tracker(api, out_path)`: render the projection back to
|
|
24
|
+
a git-tracked JSON snapshot (indent=2, ascii-escaped to match the live file).
|
|
25
|
+
- **ingest.py** — `ingest_tracker_json(api, path)`: backfill one `item_created`
|
|
26
|
+
per existing item (the migration/backfill path).
|
|
27
|
+
|
|
28
|
+
## Invariants
|
|
29
|
+
- **Append-only**: never mutate/delete events; state changes are new events.
|
|
30
|
+
- **Per-stream version is 1-based and gapless** (enforced atomically).
|
|
31
|
+
- **git as export, not source**: nothing here reads git for state.
|
|
32
|
+
- **Round-trip fidelity**: ingest → project → export reproduces the same items
|
|
33
|
+
(tested against the real `state/tracker.json`).
|
|
34
|
+
|
|
35
|
+
## Next (cutover, follow-up — NOT this increment)
|
|
36
|
+
Point tracker CRUD at `StateAPI` (create→`item_created`, update→`item_updated`,
|
|
37
|
+
move→lane update, delete→`item_archived`); add a `subscribe()` real-time path to
|
|
38
|
+
replace the SSE file-watch; run the `export_tracker` job to keep `tracker.json`
|
|
39
|
+
rendered during dual-read; then flip readers to the API.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""state_store — event-sourced state layer (DB source of truth, git as export).
|
|
2
|
+
|
|
3
|
+
Additive prototype landed 2026-07-14: the live ui/ tracker path is UNCHANGED.
|
|
4
|
+
This package provides the durable substrate to migrate aesop's coordination
|
|
5
|
+
state off git (which does not scale to concurrent writers) onto a real store
|
|
6
|
+
with transactions and per-stream versioning — SQLite (WAL) now, Postgres later
|
|
7
|
+
behind the same StateAPI. git is demoted to a rendered export (see export.py).
|
|
8
|
+
|
|
9
|
+
Design + rationale: conductor3 plans/aesop-scaling-rearchitecture.md; overview
|
|
10
|
+
in state_store/CLAUDE.md.
|
|
11
|
+
"""
|
|
12
|
+
from .api import StateAPI
|
|
13
|
+
from .export import export_tracker
|
|
14
|
+
from .ingest import ingest_tracker_json
|
|
15
|
+
from .projections import project_tracker
|
|
16
|
+
from .store import EventStore
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"EventStore",
|
|
20
|
+
"StateAPI",
|
|
21
|
+
"project_tracker",
|
|
22
|
+
"export_tracker",
|
|
23
|
+
"ingest_tracker_json",
|
|
24
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""state_store.api — StateAPI facade over the event store + projections.
|
|
2
|
+
|
|
3
|
+
The single seam callers use, so the backend (SQLite now, Postgres later) can be
|
|
4
|
+
swapped without touching call sites. ``project(view)`` reads the same-named
|
|
5
|
+
stream and folds it through the registered projector.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .projections import project_tracker
|
|
10
|
+
from .store import EventStore
|
|
11
|
+
|
|
12
|
+
_PROJECTORS = {"tracker": project_tracker}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StateAPI:
|
|
16
|
+
"""Facade: append events, read a stream, or project a view to current state."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, db_path: str):
|
|
19
|
+
self._store = EventStore(db_path)
|
|
20
|
+
|
|
21
|
+
def append(self, stream: str, event_type: str, payload: dict, actor: str = "system") -> int:
|
|
22
|
+
"""Append one event; return its new per-stream version."""
|
|
23
|
+
return self._store.append(stream, event_type, payload, actor)
|
|
24
|
+
|
|
25
|
+
def get(self, stream: str) -> list:
|
|
26
|
+
"""Return all events in ``stream`` ascending by version."""
|
|
27
|
+
return self._store.read(stream)
|
|
28
|
+
|
|
29
|
+
def project(self, view: str) -> dict:
|
|
30
|
+
"""Fold the same-named stream through its projector into current state."""
|
|
31
|
+
try:
|
|
32
|
+
projector = _PROJECTORS[view]
|
|
33
|
+
except KeyError:
|
|
34
|
+
raise ValueError(f"unknown projection view: {view!r}")
|
|
35
|
+
return projector(self.get(view))
|