@matt82198/aesop 0.3.1 → 0.4.0
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 +34 -1
- package/README.md +26 -11
- package/aesop.config.example.json +16 -0
- package/bin/cli.js +5 -2
- package/daemons/install-tasks.ps1 +193 -0
- package/daemons/run-hidden.vbs +39 -0
- package/docs/INSTALL.md +148 -14
- package/docs/MICROKERNEL.md +452 -0
- package/docs/README.md +4 -0
- package/docs/THE-AESOP-HYPOTHESIS.md +140 -0
- package/driver/CLAUDE.md +123 -123
- package/driver/README.md +40 -2
- package/driver/adjudication_gate.py +367 -0
- package/driver/aesop.config.example.json +20 -4
- package/driver/backend_config.py +603 -12
- package/driver/claude_code_driver.py +9 -17
- package/driver/codex_driver.py +80 -25
- package/driver/context_pack.py +454 -0
- package/driver/openai_compatible_driver.py +53 -11
- package/driver/openai_transport.py +31 -10
- package/driver/orchestrator_backend.py +332 -0
- package/driver/orchestrator_driver.py +589 -0
- package/driver/proc_util.py +134 -0
- package/driver/wave_loop.py +801 -59
- package/driver/wave_scheduler.py +361 -37
- package/monitor/collect-signals.mjs +83 -0
- package/package.json +3 -1
- package/state_store/coordination.py +65 -5
- package/tools/ci_merge_wait.py +88 -42
- package/tools/ci_shard_runner.py +128 -0
- package/tools/seated_shadow_adjudication.py +920 -0
- package/tools/shadow_adjudication.py +1024 -0
- package/tools/verify_ui_trio_redaction_proof.py +292 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to Aesop are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
**Version scheme**: Stable releases are `0.x.y`; `0.x.0-beta.N` / `-rc.N` are pre-releases; `0.1.0-wave.N` were internal wave-milestone previews.
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Two-seat unified config** (HS-1): New `seats` configuration block allows swapping both worker AND orchestrator models from a single config block — eliminates split configuration and enables model experimentation without code changes. No-op default: existing installations remain unchanged (install-time safety).
|
|
14
|
+
- **Live orchestrator-seat swap** (HS-2): Added `final_catch` gate in the wave loop with block/quarantine + crash-only degradation — orchestrator model can be swapped mid-flight without restarting the entire fleet. Failure path quarantines the wave and logs the incident.
|
|
15
|
+
- **Microkernel architecture guide** (HS-3): New `docs/MICROKERNEL.md` explains the proof-of-concept model-swap seam, multi-model verification bounds, and a 60-second "swap a seat's model" quickstart for adopters.
|
|
16
|
+
- **Scaffold completeness** (`npx aesop init`): driver/ directory now scaffolds on first run (was omitted, breaking adopter first-run experience). Added --force idempotent re-scaffold and child process timeout protection.
|
|
17
|
+
|
|
18
|
+
### Security / Hardening
|
|
19
|
+
- **IPv6 SSRF + DNS-resolution validation**: Requests to orchestrator's `base_url` now validate against IPv6 loopback and route-local semantics; DNS resolution blocked for external hostnames; `is_local` pinned to loopback only.
|
|
20
|
+
- **API key environment variable allowlist** (primary enforcement): `api_key_env` rejects obvious secret patterns (AWS_SECRET, AZURE_KEY, etc.) and logs a loud NOTICE for non-standard names; fail-closed on read errors.
|
|
21
|
+
- **Worker model field validation**: `worker.model` config field no longer silently ignored if orchestrator uses a non-Claude driver; now validated at load time.
|
|
22
|
+
- **Wave execution timeouts**: Added `run_command` timeouts to prevent indefinite wave hangs on subprocess failures; child processes now respect explicit timeout boundaries (no more wave-level hard stops).
|
|
23
|
+
- **CI merge-wait fail-closed**: `ci_merge_wait` gate now exits with explicit failure codes (not silent pass) on timeout or missing CI status, preventing accidental merges of untested code.
|
|
24
|
+
- **Windows CI shard concurrency cap**: Reduced `max-parallel` for Windows runner to cut container contention flakes; Windows shard now stable on standard GitHub Actions.
|
|
25
|
+
- **Path redaction completeness** (bench tooling): Bench results and logs redact full paths before reporting; `is_local` validation prevents accidental leakage of internal network topology.
|
|
26
|
+
- **JSON-boundary fail-open holes patched**: Orchestrator/driver seam now validates JSON boundaries at worker<->orchestrator message crossings; malformed messages fail-closed (not skipped).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **`npx aesop init` driver scaffolding**: Adopters running init on a fresh repo now get a complete driver/ directory tree; was silently omitting driver/, causing "module not found" on first wave.
|
|
30
|
+
- **Scaffold idempotency + child timeouts**: Re-running init --force no longer corrupts existing templates; child scaffold processes timeout cleanly instead of hanging.
|
|
31
|
+
- **Gate incumbent-validation**: Pre-gate validation of model incumbent (before swap attempt) catches incompatibilities early and logs them for forensics.
|
|
32
|
+
|
|
33
|
+
## [0.3.2] - 2026-07-23
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Windows daemons no longer flash a console window**: ad-hoc Scheduled Tasks launched `bash.exe` with Interactive logon, popping a visible console every watchdog interval (5 min). New `daemons/install-tasks.ps1` registers the watchdog/monitor tasks through a hidden `wscript` launcher (`daemons/run-hidden.vbs`) — same cadence, zero window flash.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- `daemons/install-tasks.ps1` — idempotent Windows task installer (install/uninstall/dry-run, configurable commands + intervals); shipped in the npm package.
|
|
40
|
+
|
|
8
41
|
## [0.3.1] - 2026-07-22 - Waves 28-31
|
|
9
42
|
|
|
10
43
|
### Added
|
|
@@ -373,6 +406,6 @@ This is the first public release of Aesop, a clean-room source-available impleme
|
|
|
373
406
|
- Legacy TUI interface (bash + jq)
|
|
374
407
|
- Configuration framework and examples
|
|
375
408
|
- Comprehensive documentation and guides
|
|
376
|
-
-
|
|
409
|
+
- PolyForm Strict License 1.0.0 (source-available, © 2026 Matt Culliton)
|
|
377
410
|
|
|
378
411
|
Aesop is production-ready and implements the complete cardinal rules for cost-optimized, durable multi-agent coordination.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<em>
|
|
6
|
+
<em>Crash-only multi-agent orchestration for any repository</em>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
## What It Does
|
|
16
16
|
|
|
17
|
-
**Aesop** is a
|
|
17
|
+
**Aesop** is a **crash-recoverable orchestration harness** for multi-agent workflows on any repository. One-line theme: *stateless agent execution over git-backed durable memory*.
|
|
18
|
+
|
|
19
|
+
Core idea: **agent behavior is source code.** Every decision lives in durable, human-diffable files—git history, plain-text STATE.md, append-only BUILDLOG.md, Python guardrails. When a machine fails, you re-read from disk. No vector DBs, no distributed consensus, no magic. This repo's own 251 merged PRs across 1088 commits were delivered by Aesop's own `/buildsystem` wave loop—a supervised loop under a human operator who sets goals and owns outward gates (npm publish, releases, history rewrites).
|
|
20
|
+
|
|
21
|
+
**Why it matters:** crash recovery is not a special path; it is how the system *always* starts. Stateless workers, persistent filesystem brain, Haiku-first dispatch (4.3× cheaper than hierarchical design, proven by real A/B), fail-closed guardrails (pre-push secret gate, kill-switch, cost ceiling), and observable heartbeats. The result: 251 PRs in 11 days; Haiku at 39/39 on a 39-task benchmark vs Opus 38/39, at ~1/3 the cost.
|
|
22
|
+
|
|
23
|
+
**Why it's built this way:** [The Aesop Hypothesis](./docs/THE-AESOP-HYPOTHESIS.md) — the design philosophy, the trade-offs, the cancelled architectures with published data.
|
|
18
24
|
|
|
19
25
|
## Feature Demo
|
|
20
26
|
|
|
@@ -48,7 +54,7 @@ python tools/health_score.py
|
|
|
48
54
|
```
|
|
49
55
|
Outputs a scorecard of system readiness; --json for parsing.
|
|
50
56
|
|
|
51
|
-
**Self-
|
|
57
|
+
**Self-monitoring daemon** — Runs every 150s: backs up work, scans secrets, detects drift:
|
|
52
58
|
```bash
|
|
53
59
|
npx @matt82198/aesop watch
|
|
54
60
|
```
|
|
@@ -60,6 +66,10 @@ python tools/secret_scan.py --staged # Blocks push if leak detected
|
|
|
60
66
|
```
|
|
61
67
|
Exits with failure on file-read errors (not silently passing). CI validates every merge.
|
|
62
68
|
|
|
69
|
+
**Parallel test battery** — Run all four test harnesses concurrently with isolated logs and enforced timeouts (`tools/test_battery.py` — ~5.4 min vs ~10 serial).
|
|
70
|
+
|
|
71
|
+
**Windows CI green** — Full parity support on Windows-latest GitHub Actions: promoted to a required check after 6 consecutive green main runs.
|
|
72
|
+
|
|
63
73
|
## Proof Numbers
|
|
64
74
|
|
|
65
75
|
Aesop builds itself. These numbers are live from git, verified by anyone who clones.
|
|
@@ -82,17 +92,21 @@ Aesop is built entirely by its own `/buildsystem` wave cycle—running parallel
|
|
|
82
92
|
|
|
83
93
|
| Metric | Value |
|
|
84
94
|
| --- | --- |
|
|
85
|
-
| Merged PRs |
|
|
86
|
-
| Total Commits |
|
|
87
|
-
| Project Age |
|
|
95
|
+
| Merged PRs | 251 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
96
|
+
| Total Commits | 1088 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
97
|
+
| Project Age | 11 days <!-- metrics-verified: self_stats.py (git log) --> |
|
|
88
98
|
| Waves | 30 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
89
|
-
| Insertions + Deletions |
|
|
90
|
-
| Files Tracked |
|
|
91
|
-
| Distinct Co-authors |
|
|
99
|
+
| Insertions + Deletions | 186,587 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
100
|
+
| Files Tracked | 546 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
101
|
+
| Distinct Co-authors | 11 <!-- metrics-verified: self_stats.py (git log) --> |
|
|
92
102
|
|
|
93
103
|
<!-- STATS:END -->
|
|
94
104
|
|
|
95
105
|
|
|
106
|
+
*Wave: one complete build cycle (intake → dispatch → verify → ship) run by the orchestration engine.*
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
96
110
|
|
|
97
111
|
|
|
98
112
|
|
|
@@ -101,17 +115,18 @@ Aesop is built entirely by its own `/buildsystem` wave cycle—running parallel
|
|
|
101
115
|
|
|
102
116
|
## Why Haiku-First Works
|
|
103
117
|
|
|
104
|
-
The benchmark proves it: across 39 judgment tasks (code review, severity calibration, root-cause analysis, refactor equivalence, security spots), Haiku scored **39/39** vs Opus **38/39** at ~1/3 the per-token cost. See [`bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md`](./bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md).
|
|
118
|
+
The benchmark proves it: across 39 judgment tasks (code review, severity calibration, root-cause analysis, refactor equivalence, security spots), Haiku scored **39/39** vs Opus **38/39** at ~1/3 the per-token cost. See [`bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md`](./bench/results/2026-07-17-judgment-v3-haiku-sonnet-opus.md). **Curated set, N=39** — Haiku 39/39 vs Opus 38/39; proves sufficiency for this workload, not frontier parity.
|
|
105
119
|
|
|
106
120
|
## Learn More
|
|
107
121
|
|
|
108
122
|
- **[docs/INSTALL.md](./docs/INSTALL.md)** — Setup and first wave
|
|
123
|
+
- **[docs/MICROKERNEL.md](./docs/MICROKERNEL.md)** — The two swappable seats (worker + orchestrator), the invariant Report/state boundary, and a 60-second quickstart for swapping either seat's model
|
|
109
124
|
- **[docs/PORTING.md](./docs/PORTING.md)** — Adopter's guide: port Aesop to your repo (prerequisites, scaffold, 10 failure modes)
|
|
110
125
|
- **[docs/HOW-THE-LOOP-WORKS.md](./docs/HOW-THE-LOOP-WORKS.md)** — Concrete walkthrough of a wave cycle
|
|
111
126
|
- **[docs/DISPATCH-MODEL.md](./docs/DISPATCH-MODEL.md)** — Cost analysis and scaling
|
|
112
127
|
- **[docs/CARDINAL-RULES.md](./docs/CARDINAL-RULES.md)** — 10 foundational principles
|
|
113
128
|
- **[docs/autonomous-swe.md](./docs/autonomous-swe.md)** — What "autonomous" means (and doesn't), evidence for all claims, honest limits
|
|
114
|
-
- **[RELEASE-NOTES.md](./RELEASE-NOTES.md)** — Version 0.3.0: non-Claude core proof (Codex wave), refinement-loop convergence, Windows-green CI
|
|
129
|
+
- **[RELEASE-NOTES.md](./RELEASE-NOTES.md)** — Version 0.3.1 (the 0.3.0 milestone): non-Claude core proof (Codex wave), refinement-loop convergence, Windows-green CI
|
|
115
130
|
|
|
116
131
|
## Contributing
|
|
117
132
|
|
|
@@ -46,6 +46,22 @@
|
|
|
46
46
|
"cardinal_rules": {
|
|
47
47
|
"subagent_model": "haiku"
|
|
48
48
|
},
|
|
49
|
+
"seats": {
|
|
50
|
+
"_comment": "Unified two-seat model selection (0.4.0). ONE block swaps BOTH seats: 'worker' = the coding agents (AgentDriver), 'orchestrator' = the decision seat (OrchestratorBackend). The values below ARE the defaults -- Claude Code worker + live-harness orchestrator -- so this block as shipped changes nothing; deleting it changes nothing either. To swap a seat, replace its block with one of the _worker_examples/_orchestrator_examples. Keys are read from env vars named by api_key_env at CALL time, never stored in this file. 'is_local': true = LOOPBACK endpoint only (localhost/127.0.0.1/::1 -- rejected otherwise): no API key needed, honest verification tier 3. Separate surface: cardinal_rules.subagent_model (above) pins the model for LIVE-HARNESS Claude Code subagents; seats.worker swaps the AgentDriver worker seam itself.",
|
|
51
|
+
"worker": { "backend": "claude" },
|
|
52
|
+
"orchestrator": { "backend": "harness" },
|
|
53
|
+
"_worker_examples": {
|
|
54
|
+
"_note": "Asymmetry: worker openai-compatible seats REQUIRE base_url; the orchestrator seat may omit base_url (defaults to the hosted OpenAI endpoint).",
|
|
55
|
+
"codex_hosted_openai": { "backend": "codex", "model": "gpt-4o-mini" },
|
|
56
|
+
"openai_compatible_openrouter": { "backend": "openai-compatible", "base_url": "https://openrouter.ai/api/v1", "model": "openai/gpt-4-turbo", "api_key_env": "OPENROUTER_API_KEY" },
|
|
57
|
+
"openai_compatible_local_ollama": { "backend": "openai-compatible", "base_url": "http://localhost:11434/v1", "model": "mistral", "is_local": true }
|
|
58
|
+
},
|
|
59
|
+
"_orchestrator_examples": {
|
|
60
|
+
"harness_default": { "backend": "harness" },
|
|
61
|
+
"openai_hosted": { "backend": "openai-compatible", "model": "gpt-4o-mini", "api_key_env": "OPENAI_API_KEY" },
|
|
62
|
+
"local_ollama": { "backend": "openai-compatible", "base_url": "http://localhost:11434/v1", "model": "mistral", "is_local": true }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
49
65
|
"limits": {
|
|
50
66
|
"_comment": "Cost ceiling / kill switch (tools/cost_ceiling.py, tools/halt.py). null disables a given ceiling (feature opt-in). When spend meets or exceeds a configured ceiling, cost_ceiling.py trips tools/halt.py's .HALT sentinel, which daemons/run-watchdog.sh checks at the top of every cycle and refuses to do work (no backup, no push, no scan) until cleared via 'python tools/halt.py --clear'.",
|
|
51
67
|
"max_wave_tokens": null,
|
package/bin/cli.js
CHANGED
|
@@ -259,7 +259,7 @@ const repoUrlsStr = getFlag('--repo-urls');
|
|
|
259
259
|
if (fs.existsSync(targetDir)) {
|
|
260
260
|
const contents = fs.readdirSync(targetDir);
|
|
261
261
|
// Allow .git and aesop scaffolded files to already exist (for idempotency)
|
|
262
|
-
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', 'state_store', 'skills', 'mcp', 'scan', 'hooks', '.git', 'state'];
|
|
262
|
+
const aesopDirs = ['daemons', 'dash', 'monitor', 'tools', 'ui', 'docs', 'state_store', 'skills', 'mcp', 'scan', 'hooks', 'driver', '.git', 'state'];
|
|
263
263
|
const aesopFiles = [
|
|
264
264
|
'aesop.config.example.json',
|
|
265
265
|
'aesop.config.json',
|
|
@@ -317,6 +317,7 @@ const filesToCopy = [
|
|
|
317
317
|
'mcp',
|
|
318
318
|
'scan',
|
|
319
319
|
'hooks',
|
|
320
|
+
'driver',
|
|
320
321
|
'aesop.config.example.json',
|
|
321
322
|
'README.md',
|
|
322
323
|
'LICENSE',
|
|
@@ -421,8 +422,10 @@ function generateConfigJson(targetDir, templateRoot, projectName, reposStr, repo
|
|
|
421
422
|
port: 8770
|
|
422
423
|
},
|
|
423
424
|
cardinal_rules: {
|
|
425
|
+
// NOTE: orchestrator_model was RETIRED in 0.4.0 (HS-1): it was written
|
|
426
|
+
// here but read by nothing. The orchestrator seat's model now lives in
|
|
427
|
+
// seats.orchestrator.model (see aesop.config.example.json).
|
|
424
428
|
subagent_model: 'haiku',
|
|
425
|
-
orchestrator_model: 'opus',
|
|
426
429
|
tdd_first: true,
|
|
427
430
|
never_push_main: true,
|
|
428
431
|
secret_scan_gates_push: true
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$BashExe = 'C:\Program Files\Git\bin\bash.exe',
|
|
3
|
+
[string]$WatchdogCommand = '',
|
|
4
|
+
[string]$MonitorCommand = '',
|
|
5
|
+
[int]$WatchdogIntervalMinutes = 5,
|
|
6
|
+
[int]$MonitorIntervalMinutes = 20,
|
|
7
|
+
[string]$TaskPrefix = 'Aesop',
|
|
8
|
+
[switch]$Uninstall,
|
|
9
|
+
[switch]$DryRun
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Enable strict error handling
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
|
|
15
|
+
function ConvertTo-PosixPath {
|
|
16
|
+
param([string]$WindowsPath)
|
|
17
|
+
# Convert C:\foo\bar to /c/foo/bar
|
|
18
|
+
# Rejects UNC paths (\\server\share) — error out instead of mangling
|
|
19
|
+
if ($WindowsPath -match '^\\\\') {
|
|
20
|
+
Write-Error "UNC paths are unsupported (got: $WindowsPath). Pass -WatchdogCommand explicitly with a valid path."
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
$posixPath = $WindowsPath -replace '\\', '/'
|
|
24
|
+
$posixPath = $posixPath -replace '^([A-Za-z]):', '/$1'
|
|
25
|
+
return $posixPath
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Get-WorktreeRoot {
|
|
29
|
+
# Derive worktree root from $PSScriptRoot (daemons/)
|
|
30
|
+
# $PSScriptRoot is C:\...\aesop\daemons
|
|
31
|
+
# Parent is C:\...\aesop
|
|
32
|
+
$daemonsDir = $PSScriptRoot
|
|
33
|
+
$aesopRoot = Split-Path -Parent $daemonsDir
|
|
34
|
+
return $aesopRoot
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Register-DaemonTask {
|
|
38
|
+
param(
|
|
39
|
+
[string]$TaskName,
|
|
40
|
+
[string]$Command,
|
|
41
|
+
[int]$IntervalMinutes,
|
|
42
|
+
[string]$RunHiddenVbs,
|
|
43
|
+
[string]$BashExe
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Build the action: wscript.exe //B //Nologo "path\to\run-hidden.vbs" "<bash>" -lc "<command>"
|
|
47
|
+
$action = New-ScheduledTaskAction `
|
|
48
|
+
-Execute 'wscript.exe' `
|
|
49
|
+
-Argument "//B //Nologo ""$RunHiddenVbs"" ""$BashExe"" -lc ""$Command"""
|
|
50
|
+
|
|
51
|
+
# Build the trigger: Once, starting in 1 minute, repeating every N minutes for 10 years
|
|
52
|
+
$startTime = (Get-Date).AddMinutes(1)
|
|
53
|
+
$trigger = New-ScheduledTaskTrigger `
|
|
54
|
+
-Once `
|
|
55
|
+
-At $startTime `
|
|
56
|
+
-RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) `
|
|
57
|
+
-RepetitionDuration (New-TimeSpan -Days 3650)
|
|
58
|
+
|
|
59
|
+
# Build the settings: Hidden, IgnoreNew for multiple instances, 1-hour timeout
|
|
60
|
+
$settings = New-ScheduledTaskSettingsSet `
|
|
61
|
+
-Hidden `
|
|
62
|
+
-MultipleInstances IgnoreNew `
|
|
63
|
+
-ExecutionTimeLimit (New-TimeSpan -Hours 1) `
|
|
64
|
+
-StartWhenAvailable
|
|
65
|
+
|
|
66
|
+
if ($DryRun) {
|
|
67
|
+
# Print DryRun output
|
|
68
|
+
Write-Host "DRYRUN: $TaskName -> wscript.exe //B //Nologo ""$RunHiddenVbs"" ""$BashExe"" -lc ""$Command"" (interval=$IntervalMinutes`m, Hidden=True)"
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
# Register the task (force overwrite if exists)
|
|
72
|
+
try {
|
|
73
|
+
Register-ScheduledTask `
|
|
74
|
+
-TaskName $TaskName `
|
|
75
|
+
-Action $action `
|
|
76
|
+
-Trigger $trigger `
|
|
77
|
+
-Settings $settings `
|
|
78
|
+
-Force `
|
|
79
|
+
-ErrorAction Stop | Out-Null
|
|
80
|
+
Write-Host "Registered task: $TaskName (interval=$IntervalMinutes minutes)"
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
Write-Error "Failed to register task $TaskName : $_"
|
|
84
|
+
exit 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Unregister-DaemonTask {
|
|
90
|
+
param([string]$TaskName)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
|
94
|
+
if ($task) {
|
|
95
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction Stop
|
|
96
|
+
Write-Host "Unregistered task: $TaskName"
|
|
97
|
+
return $true
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
Write-Host "Task not found: $TaskName (already unregistered or never existed)"
|
|
101
|
+
return $true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
Write-Error "Failed to unregister $TaskName : $_"
|
|
106
|
+
return $false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function Main {
|
|
111
|
+
# Resolve paths
|
|
112
|
+
$aesopRoot = Get-WorktreeRoot
|
|
113
|
+
$runHiddenVbs = Join-Path $PSScriptRoot 'run-hidden.vbs'
|
|
114
|
+
|
|
115
|
+
# VALIDATION: Check for double quotes in commands (contract violation)
|
|
116
|
+
# This check runs early and always, before any operations
|
|
117
|
+
if ($WatchdogCommand -like '*"*') {
|
|
118
|
+
Write-Error "WatchdogCommand contains double quotes, which are not allowed (vbs launcher contract violation)."
|
|
119
|
+
exit 1
|
|
120
|
+
}
|
|
121
|
+
if ($MonitorCommand -like '*"*') {
|
|
122
|
+
Write-Error "MonitorCommand contains double quotes, which are not allowed (vbs launcher contract violation)."
|
|
123
|
+
exit 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# PATH VALIDATION: Only enforce file existence checks if not in DryRun mode
|
|
127
|
+
# In DryRun, downgrade to warnings so preview works on machines without Git Bash
|
|
128
|
+
if (-not $DryRun) {
|
|
129
|
+
if (-not (Test-Path $runHiddenVbs)) {
|
|
130
|
+
Write-Error "run-hidden.vbs not found at: $runHiddenVbs"
|
|
131
|
+
exit 1
|
|
132
|
+
}
|
|
133
|
+
if (-not (Test-Path $BashExe)) {
|
|
134
|
+
Write-Error "bash.exe not found at: $BashExe"
|
|
135
|
+
exit 1
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
if (-not (Test-Path $runHiddenVbs)) {
|
|
140
|
+
Write-Warning "run-hidden.vbs not found at: $runHiddenVbs (DryRun mode)"
|
|
141
|
+
}
|
|
142
|
+
if (-not (Test-Path $BashExe)) {
|
|
143
|
+
Write-Warning "bash.exe not found at: $BashExe (DryRun mode)"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Handle Uninstall mode
|
|
148
|
+
if ($Uninstall) {
|
|
149
|
+
$watchdog_ok = Unregister-DaemonTask -TaskName "${TaskPrefix}WatchdogDaemon"
|
|
150
|
+
$monitor_ok = Unregister-DaemonTask -TaskName "${TaskPrefix}RefinementMonitor"
|
|
151
|
+
if (-not $watchdog_ok -or -not $monitor_ok) {
|
|
152
|
+
exit 1
|
|
153
|
+
}
|
|
154
|
+
exit 0
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Derive default commands if not provided
|
|
158
|
+
if (-not $WatchdogCommand) {
|
|
159
|
+
$posixRoot = ConvertTo-PosixPath $aesopRoot
|
|
160
|
+
|
|
161
|
+
# P2: Detect apostrophe in derived path (breaks bash syntax if not escaped)
|
|
162
|
+
if ($posixRoot -like "*'*") {
|
|
163
|
+
Write-Error "Repository path contains apostrophe, which would break the derived command: $posixRoot`nPass -WatchdogCommand explicitly."
|
|
164
|
+
exit 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
$WatchdogCommand = "bash '$posixRoot/daemons/run-watchdog.sh' --once >> '$posixRoot/state/cron-watchdog.log' 2>&1"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Register watchdog task
|
|
171
|
+
$watchdogTaskName = "${TaskPrefix}WatchdogDaemon"
|
|
172
|
+
Register-DaemonTask `
|
|
173
|
+
-TaskName $watchdogTaskName `
|
|
174
|
+
-Command $WatchdogCommand `
|
|
175
|
+
-IntervalMinutes $WatchdogIntervalMinutes `
|
|
176
|
+
-RunHiddenVbs $runHiddenVbs `
|
|
177
|
+
-BashExe $BashExe
|
|
178
|
+
|
|
179
|
+
# Register monitor task if command provided
|
|
180
|
+
if ($MonitorCommand) {
|
|
181
|
+
$monitorTaskName = "${TaskPrefix}RefinementMonitor"
|
|
182
|
+
Register-DaemonTask `
|
|
183
|
+
-TaskName $monitorTaskName `
|
|
184
|
+
-Command $MonitorCommand `
|
|
185
|
+
-IntervalMinutes $MonitorIntervalMinutes `
|
|
186
|
+
-RunHiddenVbs $runHiddenVbs `
|
|
187
|
+
-BashExe $BashExe
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
exit 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Main
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
' run-hidden.vbs — VBScript launcher for Windows Scheduled Tasks
|
|
2
|
+
'
|
|
3
|
+
' Purpose: Execute a command with a hidden console window (window style 0) and
|
|
4
|
+
' propagate its exit code.
|
|
5
|
+
'
|
|
6
|
+
' Usage:
|
|
7
|
+
' wscript.exe //B //Nologo run-hidden.vbs <bash-exe> -lc "<command>"
|
|
8
|
+
'
|
|
9
|
+
' The script rebuilds a quoted command line from WScript.Arguments and runs it
|
|
10
|
+
' via WScript.Shell.Run with window style 0 (hidden). CRITICAL: The task
|
|
11
|
+
' instance lives as long as the child process (shell.Run waits), so that
|
|
12
|
+
' - MultipleInstances IgnoreNew can prevent concurrent runs
|
|
13
|
+
' - ExecutionTimeLimit can kill hung processes
|
|
14
|
+
' - LastTaskResult reflects the actual child exit code (not always 0)
|
|
15
|
+
'
|
|
16
|
+
' By contract, arguments never contain double quotes.
|
|
17
|
+
|
|
18
|
+
Dim shell, cmd, i, arg
|
|
19
|
+
Dim windowStyle, rc
|
|
20
|
+
|
|
21
|
+
Set shell = CreateObject("WScript.Shell")
|
|
22
|
+
|
|
23
|
+
' Build command line from arguments
|
|
24
|
+
cmd = ""
|
|
25
|
+
For i = 0 To WScript.Arguments.Count - 1
|
|
26
|
+
arg = WScript.Arguments(i)
|
|
27
|
+
If i > 0 Then cmd = cmd & " "
|
|
28
|
+
' Arguments never contain quotes by contract; wrap in quotes for safety
|
|
29
|
+
cmd = cmd & """" & arg & """"
|
|
30
|
+
Next
|
|
31
|
+
|
|
32
|
+
' Window style 0 = hidden; wait for process exit (True = wait)
|
|
33
|
+
windowStyle = 0
|
|
34
|
+
|
|
35
|
+
' Execute the command and wait for it to complete, capturing exit code
|
|
36
|
+
rc = shell.Run(cmd, windowStyle, True)
|
|
37
|
+
|
|
38
|
+
' Exit with the child process's exit code
|
|
39
|
+
WScript.Quit rc
|
package/docs/INSTALL.md
CHANGED
|
@@ -200,19 +200,103 @@ export DEBUG=1
|
|
|
200
200
|
|
|
201
201
|
By default, Aesop uses Claude Code (the orchestration harness) as its backend. You can configure it to use other models via the **AgentDriver abstraction**—enabling Ollama, OpenAI-compatible endpoints, OpenRouter, and more.
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
For the conceptual picture (the two swappable seats, what's proven vs. bounded about swapping them, and a 60-second copy-paste quickstart), see **[MICROKERNEL.md](MICROKERNEL.md)**. The rest of this section is the config-mechanics reference.
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
### The unified two-seat config (0.4.0)
|
|
206
|
+
|
|
207
|
+
One namespaced `seats` block in `aesop.config.json` selects BOTH seats:
|
|
208
|
+
|
|
209
|
+
- **`seats.worker`** — the coding agents (AgentDriver). Same fields as the
|
|
210
|
+
legacy flat block below; takes precedence over it when both are present.
|
|
211
|
+
- **`seats.orchestrator`** — the decision seat (`OrchestratorDriver.decide()`).
|
|
212
|
+
`"harness"` (default) means the live Claude Code session itself makes
|
|
213
|
+
decisions; `"openai-compatible"` routes decisions to an API model.
|
|
214
|
+
|
|
215
|
+
**Swap a seat's model** — change just its block:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"seats": {
|
|
220
|
+
"worker": {
|
|
221
|
+
"backend": "openai-compatible",
|
|
222
|
+
"base_url": "http://localhost:11434/v1",
|
|
223
|
+
"model": "mistral",
|
|
224
|
+
"is_local": true
|
|
225
|
+
},
|
|
226
|
+
"orchestrator": {
|
|
227
|
+
"backend": "openai-compatible",
|
|
228
|
+
"model": "gpt-4o-mini",
|
|
229
|
+
"api_key_env": "OPENAI_API_KEY"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
That is a local-Ollama worker fleet with a hosted `gpt-4o-mini` decision seat.
|
|
236
|
+
To swap the worker to OpenRouter, replace `seats.worker` with
|
|
237
|
+
`{"backend": "openai-compatible", "base_url": "https://openrouter.ai/api/v1",
|
|
238
|
+
"model": "openai/gpt-4-turbo", "api_key_env": "OPENROUTER_API_KEY"}` — nothing
|
|
239
|
+
else changes. API keys are read from the env var named by `api_key_env` at
|
|
240
|
+
call time and are never stored in the config; `"is_local": true` endpoints
|
|
241
|
+
need no key at all — and for exactly that reason `is_local` is only accepted
|
|
242
|
+
with a loopback `base_url` (`localhost`, `127.0.0.1`, `::1`).
|
|
243
|
+
|
|
244
|
+
`api_key_env` is checked with a **best-effort heuristic**, allowlist-primary:
|
|
245
|
+
known LLM-provider key names (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
|
|
246
|
+
`OPENROUTER_API_KEY`, `TOGETHER_API_KEY`, `GROQ_API_KEY`, `MISTRAL_API_KEY`,
|
|
247
|
+
`DEEPSEEK_API_KEY`, `FIREWORKS_API_KEY`, `OLLAMA_API_KEY`,
|
|
248
|
+
`AZURE_OPENAI_API_KEY`, `GOOGLE_API_KEY`) are accepted silently; names that
|
|
249
|
+
don't look like key env vars or contain obvious non-LLM secret fragments
|
|
250
|
+
(`SECRET`/`TOKEN`/`PASSWORD`/`ACCESS`/...) are rejected; any other
|
|
251
|
+
key-shaped name (custom LLM gateways) is **allowed but prints a loud
|
|
252
|
+
NOTICE** — the env var's value will be sent as a Bearer token to your
|
|
253
|
+
`base_url`, so that NOTICE is the real signal: review it whenever a
|
|
254
|
+
non-provider name shows up at load time.
|
|
255
|
+
|
|
256
|
+
Asymmetry worth knowing: a **worker** `openai-compatible` seat REQUIRES
|
|
257
|
+
`base_url`; the **orchestrator** seat may omit it (defaults to the hosted
|
|
258
|
+
OpenAI endpoint).
|
|
259
|
+
|
|
260
|
+
**No `seats` block? Nothing changes.** Existing installs keep today's exact
|
|
261
|
+
behavior: Claude Code workers + harness orchestrator, no OpenAI backend
|
|
262
|
+
constructed, no key required. A pre-0.4.0 **legacy flat** backend block
|
|
263
|
+
(`{"backend": "codex", ...}` at the top level) also changes nothing by
|
|
264
|
+
itself: it still parses and validates, but it stays **inert** in the wave
|
|
265
|
+
scheduler's default path until you migrate it to `seats.worker` — on older
|
|
266
|
+
installs that block was documented but consumed by nothing, so activating it
|
|
267
|
+
silently would change behavior under you. (`cardinal_rules.orchestrator_model`
|
|
268
|
+
from older scaffolds was write-only and is retired; the orchestrator seat's
|
|
269
|
+
model now lives in `seats.orchestrator.model`.)
|
|
270
|
+
|
|
271
|
+
Consumers: `driver/wave_scheduler.py` builds its worker driver from this
|
|
272
|
+
config (CLI `--driver claude|codex` remains an override), and
|
|
273
|
+
`tools/shadow_adjudication.py` / `tools/seated_shadow_adjudication.py` build
|
|
274
|
+
their live orchestrator backend from `seats.orchestrator` (CLI `--model`
|
|
275
|
+
remains an override).
|
|
276
|
+
|
|
277
|
+
### The legacy flat block (parse-compatible, but migrate it)
|
|
278
|
+
|
|
279
|
+
Pre-0.4.0 docs described a flat top-level block:
|
|
206
280
|
|
|
207
281
|
```json
|
|
208
282
|
{
|
|
209
283
|
"backend": "openai-compatible",
|
|
210
284
|
"model": "ollama-mistral",
|
|
211
285
|
"base_url": "http://localhost:11434/v1",
|
|
212
|
-
"api_key_env": "OLLAMA_API_KEY"
|
|
286
|
+
"api_key_env": "OLLAMA_API_KEY",
|
|
287
|
+
"is_local": true
|
|
213
288
|
}
|
|
214
289
|
```
|
|
215
290
|
|
|
291
|
+
It still parses and validates (and direct `build_driver()` callers honor
|
|
292
|
+
it), but the wave scheduler's default path treats it as **inert** and keeps
|
|
293
|
+
the Claude Code worker: to actually activate a configured worker there, put
|
|
294
|
+
the same fields under `seats.worker` (`{"seats": {"worker": { ...this
|
|
295
|
+
block... }}}`).
|
|
296
|
+
|
|
297
|
+
Set `"is_local": true` for local/small models (Ollama etc.) — it raises the
|
|
298
|
+
verification tier honestly (tier 3 instead of hosted tier 2).
|
|
299
|
+
|
|
216
300
|
Supported backends:
|
|
217
301
|
- `"claude"` (default) — Claude Code CLI harness
|
|
218
302
|
- `"openai-compatible"` — OpenAI Chat Completions API (Ollama, OpenRouter, etc.)
|
|
@@ -229,17 +313,22 @@ ollama serve
|
|
|
229
313
|
# 2. In another terminal, pull a model
|
|
230
314
|
ollama pull mistral
|
|
231
315
|
|
|
232
|
-
# 3. Configure Aesop to use it
|
|
316
|
+
# 3. Configure Aesop to use it (seats.worker is the opt-in surface)
|
|
233
317
|
cat > aesop.config.json <<EOF
|
|
234
318
|
{
|
|
235
|
-
"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
319
|
+
"seats": {
|
|
320
|
+
"worker": {
|
|
321
|
+
"backend": "openai-compatible",
|
|
322
|
+
"model": "mistral",
|
|
323
|
+
"base_url": "http://localhost:11434/v1",
|
|
324
|
+
"api_key_env": "OLLAMA_API_KEY",
|
|
325
|
+
"is_local": true
|
|
326
|
+
}
|
|
327
|
+
}
|
|
239
328
|
}
|
|
240
329
|
EOF
|
|
241
330
|
|
|
242
|
-
# 4. Start Aesop (
|
|
331
|
+
# 4. Start Aesop (components that dispatch through the AgentDriver seam will use Mistral)
|
|
243
332
|
npx @matt82198/aesop my-fleet --name "my-api"
|
|
244
333
|
```
|
|
245
334
|
|
|
@@ -250,16 +339,16 @@ The AgentDriver framework applies **honest verification tiers** — weaker backe
|
|
|
250
339
|
| Backend | Accuracy | Verification Tier | What it means |
|
|
251
340
|
|---------|----------|-------------------|---------------|
|
|
252
341
|
| Claude Code | ~0.99 | 1 (minimal) | Orchestrator trusts output; spot-check tests |
|
|
253
|
-
| OpenAI (
|
|
254
|
-
|
|
|
342
|
+
| Hosted OpenAI-compatible (codex / OpenRouter) | ~0.92 | 2 | Validate all JSON, ~50% spot-check, adversarial review |
|
|
343
|
+
| Local small model (`"is_local": true`, e.g. Ollama) | ~0.80 | 3 | Validate all JSON, heavy spot-check, adversarial review |
|
|
255
344
|
|
|
256
|
-
|
|
345
|
+
Higher tiers mean MORE orchestrator verification work: weaker backends raise, never lower, the orchestrator's burden. See [driver/README.md](../driver/README.md) for full verification-policy details.
|
|
257
346
|
|
|
258
347
|
### Troubleshooting
|
|
259
348
|
|
|
260
349
|
**Backend won't connect**: Check `OLLAMA_API_KEY` (or your backend's API key env var) is set and the `base_url` is reachable.
|
|
261
350
|
|
|
262
|
-
**Verification tier too strict**:
|
|
351
|
+
**Verification tier too strict**: The tier comes from the driver's `probe_capabilities()` honesty contract — never inflate accuracy to lower it. For hosted-quality models, leave `"is_local"` unset (tier 2); reserve `"is_local": true` (tier 3) for genuinely small local models.
|
|
263
352
|
|
|
264
353
|
For more details, see [driver/README.md](../driver/README.md).
|
|
265
354
|
|
|
@@ -283,12 +372,57 @@ To bypass during testing: `git push --no-verify` (not recommended for production
|
|
|
283
372
|
|
|
284
373
|
---
|
|
285
374
|
|
|
375
|
+
## Windows: Register Daemons as Hidden Scheduled Tasks
|
|
376
|
+
|
|
377
|
+
On Windows, the watchdog and refinement monitor daemons can run silently in the background without flashing a console window. Use the provided PowerShell installer:
|
|
378
|
+
|
|
379
|
+
```powershell
|
|
380
|
+
# Register watchdog daemon (every 5m)
|
|
381
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File daemons/install-tasks.ps1
|
|
382
|
+
|
|
383
|
+
# Register both watchdog and monitor daemons (monitor script is external, customize path as needed)
|
|
384
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File daemons/install-tasks.ps1 `
|
|
385
|
+
-MonitorCommand "bash '/c/path/to/your/monitor/run-monitor.sh' --once"
|
|
386
|
+
|
|
387
|
+
# Customize intervals and task names
|
|
388
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File daemons/install-tasks.ps1 `
|
|
389
|
+
-TaskPrefix MyFleet `
|
|
390
|
+
-WatchdogIntervalMinutes 10 `
|
|
391
|
+
-MonitorIntervalMinutes 30 `
|
|
392
|
+
-MonitorCommand "bash '/c/path/to/your/monitor/run-monitor.sh' --once"
|
|
393
|
+
|
|
394
|
+
# Uninstall tasks
|
|
395
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File daemons/install-tasks.ps1 -Uninstall
|
|
396
|
+
|
|
397
|
+
# Preview without registering (dry-run mode)
|
|
398
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File daemons/install-tasks.ps1 -DryRun
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**How it works**: The installer creates Scheduled Tasks that launch `wscript.exe` with a hidden VBScript launcher (`daemons/run-hidden.vbs`). This avoids the console window that appears when bash.exe is run directly as a Scheduled Task action.
|
|
402
|
+
|
|
403
|
+
**Parameters**:
|
|
404
|
+
- `-TaskPrefix AesopMyFleet` — Task names: `AesopMyFleetWatchdogDaemon`, `AesopMyFleetRefinementMonitor` (default: `Aesop`)
|
|
405
|
+
- `-WatchdogIntervalMinutes N` — Watchdog cycle interval in minutes (default: 5)
|
|
406
|
+
- `-MonitorIntervalMinutes N` — Monitor cycle interval in minutes (default: 20)
|
|
407
|
+
- `-WatchdogCommand "bash '...' ..."` — Custom watchdog command (default: `run-watchdog.sh --once >> state/cron-watchdog.log`)
|
|
408
|
+
- `-MonitorCommand "bash '...' ..."` — Custom monitor command; omit to skip registering the monitor task (default: empty)
|
|
409
|
+
- `-Uninstall` — Remove all registered tasks
|
|
410
|
+
- `-DryRun` — Preview task configuration without registering
|
|
411
|
+
|
|
412
|
+
**Constraints**:
|
|
413
|
+
- Commands (`-WatchdogCommand`, `-MonitorCommand`) must NOT contain double quotes (vbs launcher contract)
|
|
414
|
+
- UNC paths (e.g., `\\server\share`) are not supported; use local Windows or POSIX paths only
|
|
415
|
+
- `-DryRun` mode works even if `bash.exe` or run-hidden.vbs is missing (validation downgraded to warnings for preview)
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
286
419
|
## Next Steps
|
|
287
420
|
|
|
288
421
|
1. **Read [PORTING.md](PORTING.md)** — Step-by-step guide for adopting Aesop on a foreign repo (10 common failure modes)
|
|
289
422
|
2. **Read [CONFIGURE.md](CONFIGURE.md)** — Customize repos, ports, and brain root
|
|
290
423
|
3. **Run [FIRST-WAVE.md](FIRST-WAVE.md)** — Test a full `/power` → `/buildsystem` cycle
|
|
291
424
|
4. **Understand [CONCEPTS.md](CONCEPTS.md)** — Learn the dispatch model and state model
|
|
292
|
-
|
|
425
|
+
5. **Read [MICROKERNEL.md](MICROKERNEL.md)** — Swap the worker or orchestrator seat to a non-Claude model
|
|
426
|
+
6. **Explore the dashboard** — `python3 ui/serve.py` then open http://localhost:8770
|
|
293
427
|
|
|
294
428
|
For troubleshooting, see the [Aesop README](../README.md#troubleshooting) or [GOVERNANCE.md](GOVERNANCE.md) for operational policies.
|