@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.
Files changed (234) hide show
  1. package/CHANGELOG.md +79 -95
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +126 -245
  4. package/aesop.config.example.json +34 -26
  5. package/bin/CLAUDE.md +67 -0
  6. package/bin/cli.js +593 -36
  7. package/daemons/CLAUDE.md +36 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +95 -12
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELEASING.md +159 -0
  23. package/docs/RELIABILITY.md +61 -0
  24. package/docs/RESTORE.md +397 -0
  25. package/docs/SCRIPTS-POLICY.md +97 -0
  26. package/docs/archive/README.md +3 -0
  27. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
  28. package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
  29. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
  30. package/docs/archive/spikes/tiered-cognition/README.md +27 -0
  31. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  32. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  33. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  34. package/docs/case-study-portfolio.md +61 -0
  35. package/docs/self-stats-data.json +11 -0
  36. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  37. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  38. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  39. package/hooks/CLAUDE.md +61 -0
  40. package/hooks/claude/force-model-policy.mjs +159 -0
  41. package/hooks/pre-push-policy.sh +753 -0
  42. package/mcp/CLAUDE.md +213 -0
  43. package/mcp/package.json +26 -0
  44. package/mcp/server.mjs +543 -0
  45. package/monitor/.signal-state.json +3 -0
  46. package/monitor/ACTIONS.log +1 -0
  47. package/monitor/BRIEF.md +24 -0
  48. package/monitor/CHARTER.md +75 -88
  49. package/monitor/CLAUDE.md +40 -0
  50. package/monitor/SIGNALS.json +54 -0
  51. package/monitor/collect-signals.mjs +613 -61
  52. package/package.json +15 -3
  53. package/scan/fleet-scan.example.mjs +292 -0
  54. package/skills/CLAUDE.md +3 -0
  55. package/skills/healthcheck/SKILL.md +44 -0
  56. package/skills/power/SKILL.md +161 -0
  57. package/state_store/CLAUDE.md +39 -0
  58. package/state_store/__init__.py +24 -0
  59. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  61. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  62. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  63. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  64. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  65. package/state_store/api.py +35 -0
  66. package/state_store/export.py +19 -0
  67. package/state_store/ingest.py +24 -0
  68. package/state_store/projections.py +52 -0
  69. package/state_store/store.py +102 -0
  70. package/tools/CLAUDE.md +72 -0
  71. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  72. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  73. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  74. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  75. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  76. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  77. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  78. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  96. package/tools/agent-forensics.sh +186 -0
  97. package/tools/alert_bridge.py +449 -0
  98. package/tools/buildlog.py +182 -0
  99. package/tools/ci_merge_wait.py +317 -0
  100. package/tools/ensure_state.py +94 -0
  101. package/tools/eod_sweep.py +239 -0
  102. package/tools/fleet_ledger.py +272 -0
  103. package/tools/fleet_prompt_extractor.py +134 -0
  104. package/tools/healthcheck.py +296 -0
  105. package/tools/heartbeat.py +131 -0
  106. package/tools/inbox_drain.py +249 -0
  107. package/tools/launch_tui.py +51 -3
  108. package/tools/lock.mjs +211 -0
  109. package/tools/metrics_gate.py +205 -0
  110. package/tools/orchestrator_status.py +99 -0
  111. package/tools/power_selftest.py +386 -0
  112. package/tools/prepublish_scan.py +84 -0
  113. package/tools/proposals.mjs +248 -0
  114. package/tools/reconstitute.sh +467 -0
  115. package/tools/rotate_logs.py +228 -0
  116. package/tools/scanner_selftest.py +303 -0
  117. package/tools/secret_scan.py +138 -31
  118. package/tools/self_stats.py +509 -0
  119. package/tools/session_usage_summary.py +198 -0
  120. package/tools/stall_check.py +172 -0
  121. package/tools/svg_to_png.mjs +50 -0
  122. package/tools/transcript_replay.py +236 -0
  123. package/tools/transcript_timeline.py +184 -0
  124. package/tools/verify_dash.py +513 -0
  125. package/tools/verify_submit_encoding.py +202 -0
  126. package/ui/CLAUDE.md +125 -0
  127. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  128. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  129. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  130. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  131. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  132. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  133. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  134. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  135. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  136. package/ui/agents.py +183 -0
  137. package/ui/api/__init__.py +84 -0
  138. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  140. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  141. package/ui/api/submit.py +97 -0
  142. package/ui/api/tracker.py +116 -0
  143. package/ui/collectors.py +735 -0
  144. package/ui/config.py +123 -0
  145. package/ui/cost.py +260 -0
  146. package/ui/csrf.py +144 -0
  147. package/ui/handler.py +676 -0
  148. package/ui/render.py +49 -0
  149. package/ui/serve.py +68 -796
  150. package/ui/sse.py +183 -0
  151. package/ui/web/.gitattributes +13 -0
  152. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  153. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  154. package/ui/web/dist/index.html +14 -0
  155. package/ui/web/index.html +13 -0
  156. package/ui/web/package-lock.json +2225 -0
  157. package/ui/web/package.json +26 -0
  158. package/ui/web/src/App.test.tsx +74 -0
  159. package/ui/web/src/App.tsx +142 -0
  160. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  161. package/ui/web/src/components/AgentRow.css +187 -0
  162. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  163. package/ui/web/src/components/AgentRow.tsx +207 -0
  164. package/ui/web/src/components/AgentsPanel.css +108 -0
  165. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  166. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  167. package/ui/web/src/components/AlertsPanel.css +88 -0
  168. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  169. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  170. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  171. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  172. package/ui/web/src/components/CostChart.css +110 -0
  173. package/ui/web/src/components/CostChart.test.tsx +144 -0
  174. package/ui/web/src/components/CostChart.tsx +152 -0
  175. package/ui/web/src/components/CostTable.css +93 -0
  176. package/ui/web/src/components/CostTable.test.tsx +165 -0
  177. package/ui/web/src/components/CostTable.tsx +94 -0
  178. package/ui/web/src/components/EventsFeed.css +68 -0
  179. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  180. package/ui/web/src/components/EventsFeed.tsx +31 -0
  181. package/ui/web/src/components/HealthHeader.css +137 -0
  182. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  183. package/ui/web/src/components/HealthHeader.tsx +281 -0
  184. package/ui/web/src/components/InboxForm.css +135 -0
  185. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  186. package/ui/web/src/components/InboxForm.tsx +116 -0
  187. package/ui/web/src/components/MessagesTail.module.css +144 -0
  188. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  189. package/ui/web/src/components/MessagesTail.tsx +94 -0
  190. package/ui/web/src/components/ReposPanel.css +90 -0
  191. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  192. package/ui/web/src/components/ReposPanel.tsx +67 -0
  193. package/ui/web/src/components/Scorecard.css +106 -0
  194. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  195. package/ui/web/src/components/Scorecard.tsx +85 -0
  196. package/ui/web/src/components/Timeline.module.css +151 -0
  197. package/ui/web/src/components/Timeline.test.tsx +215 -0
  198. package/ui/web/src/components/Timeline.tsx +99 -0
  199. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  200. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  201. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  202. package/ui/web/src/components/TrackerCard.tsx +160 -0
  203. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  204. package/ui/web/src/components/TrackerForm.tsx +144 -0
  205. package/ui/web/src/lib/api.ts +218 -0
  206. package/ui/web/src/lib/format.test.ts +89 -0
  207. package/ui/web/src/lib/format.ts +103 -0
  208. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  209. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  210. package/ui/web/src/lib/types.ts +230 -0
  211. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  212. package/ui/web/src/lib/useHashRoute.ts +23 -0
  213. package/ui/web/src/lib/useSSE.ts +175 -0
  214. package/ui/web/src/main.tsx +10 -0
  215. package/ui/web/src/styles/global.css +179 -0
  216. package/ui/web/src/styles/theme.css +184 -0
  217. package/ui/web/src/styles/work.css +572 -0
  218. package/ui/web/src/test/fixtures.ts +385 -0
  219. package/ui/web/src/test/setup.ts +49 -0
  220. package/ui/web/src/views/Activity.module.css +43 -0
  221. package/ui/web/src/views/Activity.test.tsx +89 -0
  222. package/ui/web/src/views/Activity.tsx +31 -0
  223. package/ui/web/src/views/Cost.css +87 -0
  224. package/ui/web/src/views/Cost.test.tsx +142 -0
  225. package/ui/web/src/views/Cost.tsx +54 -0
  226. package/ui/web/src/views/Overview.css +51 -0
  227. package/ui/web/src/views/Overview.test.tsx +76 -0
  228. package/ui/web/src/views/Overview.tsx +46 -0
  229. package/ui/web/src/views/Work.test.tsx +82 -0
  230. package/ui/web/src/views/Work.tsx +79 -0
  231. package/ui/web/src/vite-env.d.ts +10 -0
  232. package/ui/web/tsconfig.json +22 -0
  233. package/ui/web/vite.config.ts +25 -0
  234. 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.3",
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.",
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
+ }
@@ -0,0 +1,3 @@
1
+ # skills/ — Orchestration and priming skills
2
+
3
+ - **power/** — /power skill: prime the filesystem brain (load rules, dispatch model, memory, state) and report system health — see power/SKILL.md
@@ -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
+ ]
@@ -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))