@adaptic/maestro 1.7.3 → 1.8.1
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/.claude/commands/init-maestro.md +15 -2
- package/.gitignore +7 -0
- package/README.md +62 -11
- package/bin/maestro.mjs +338 -2
- package/bin/maestro.test.mjs +299 -0
- package/docs/guides/poller-daemon-setup.md +21 -8
- package/docs/runbooks/perpetual-operations.md +19 -15
- package/docs/runbooks/recovery-and-failover.md +42 -0
- package/lib/cadence-bus.mjs +625 -0
- package/lib/cadence-bus.test.mjs +354 -0
- package/package.json +6 -1
- package/scaffold/CLAUDE.md +11 -7
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +158 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +154 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/daemon/cadence-consumer.mjs +439 -0
- package/scripts/daemon/cadence-consumer.test.mjs +397 -0
- package/scripts/daemon/cadence-handlers.mjs +263 -0
- package/scripts/daemon/maestro-daemon.mjs +20 -0
- package/scripts/local-triggers/generate-plists.sh +62 -17
- package/scripts/local-triggers/generate-plists.test.mjs +254 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +22 -3
- package/scripts/local-triggers/plists/ai.adaptic.sophie-backlog-executor.plist +0 -21
- package/scripts/local-triggers/plists/ai.adaptic.sophie-daemon.plist +0 -32
- package/scripts/local-triggers/plists/ai.adaptic.sophie-inbox-processor.plist +0 -21
- package/scripts/local-triggers/plists/ai.adaptic.sophie-meeting-action-capture.plist +0 -21
- package/scripts/local-triggers/plists/ai.adaptic.sophie-meeting-prep.plist +0 -21
- package/scripts/local-triggers/plists/ai.adaptic.sophie-midday-sweep.plist +0 -26
- package/scripts/local-triggers/plists/ai.adaptic.sophie-quarterly-self-assessment.plist +0 -62
- package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-engineering-health.plist +0 -28
- package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-execution.plist +0 -28
- package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-hiring.plist +0 -28
- package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-priorities.plist +0 -28
- package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-strategic-memo.plist +0 -28
|
@@ -435,7 +435,9 @@ The generated tools should be COMPREHENSIVE — every responsibility listed in P
|
|
|
435
435
|
|
|
436
436
|
## Phase 3: Machine Configuration
|
|
437
437
|
|
|
438
|
-
After identity rewriting completes, generate and install the launchd plists (these need the agent name from Phase 1)
|
|
438
|
+
After identity rewriting completes, generate and install the launchd plists (these need the agent name from Phase 1).
|
|
439
|
+
|
|
440
|
+
The plists generated here use the **cadence bus architecture** (maestro 1.8+): scheduled cadence ticks no longer spawn a fresh Claude Code session per tick. Instead, launchd invokes `scripts/cadence/enqueue-cadence-tick.mjs` (≈10 ms, no Claude) which drops a JSON event onto `state/cadence-bus/inbox/`. The persistent daemon (started by the `*-daemon` plist) drains the bus and decides — per cadence — whether to handle the tick inline or escalate to a managed sub-session.
|
|
439
441
|
|
|
440
442
|
### Step 1: Generate launchd plists
|
|
441
443
|
|
|
@@ -443,7 +445,7 @@ After identity rewriting completes, generate and install the launchd plists (the
|
|
|
443
445
|
bash scripts/local-triggers/generate-plists.sh
|
|
444
446
|
```
|
|
445
447
|
|
|
446
|
-
This reads `config/agent.ts` to get the agent's first name and generates all 13 launchd plist files with correct labels and paths.
|
|
448
|
+
This reads `config/agent.ts` to get the agent's first name and generates all 13 launchd plist files with correct labels and paths. Every plist carries the `maestro-plist-arch: cadence-bus v1` marker.
|
|
447
449
|
|
|
448
450
|
### Step 2: Install launchd agents
|
|
449
451
|
|
|
@@ -462,6 +464,17 @@ done
|
|
|
462
464
|
|
|
463
465
|
Report how many triggers were installed.
|
|
464
466
|
|
|
467
|
+
### Step 2b: Cadence bus smoke test (verifies end-to-end delivery)
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
# Enqueue a heartbeat tick and confirm the daemon drained it.
|
|
471
|
+
node scripts/cadence/enqueue-cadence-tick.mjs cadence-bus-heartbeat --source=init-maestro
|
|
472
|
+
sleep 4
|
|
473
|
+
node scripts/cadence/cadence-status.mjs
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
The heartbeat must show `depth.inbox: 0` and a fresh `health.ts` within the last minute. If not, check `logs/cadence-bus/<date>.jsonl` and `logs/daemon/launchd-stderr.log`.
|
|
477
|
+
|
|
465
478
|
### Step 3: macOS headless configuration (optional)
|
|
466
479
|
|
|
467
480
|
Offer to configure the Mac mini for headless 24/7 operation:
|
package/.gitignore
CHANGED
|
@@ -18,6 +18,13 @@ memory/
|
|
|
18
18
|
outputs/
|
|
19
19
|
knowledge/
|
|
20
20
|
|
|
21
|
+
# Generated launchd plists — produced per-agent by generate-plists.sh from
|
|
22
|
+
# the agent's own config/agent.ts. Never committed to the framework; each
|
|
23
|
+
# agent regenerates them at init-agent.sh time. Keep the directory via
|
|
24
|
+
# scripts/local-triggers/plists/.gitkeep.
|
|
25
|
+
scripts/local-triggers/plists/*.plist
|
|
26
|
+
scripts/poller-launchd/*.plist
|
|
27
|
+
|
|
21
28
|
# Editor
|
|
22
29
|
*.swp
|
|
23
30
|
*.swo
|
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ After `create`, run `claude "/init-maestro"` to configure the agent's identity a
|
|
|
103
103
|
|
|
104
104
|
Updates framework files in an existing agent repo. This command copies the latest versions of:
|
|
105
105
|
|
|
106
|
-
- `scripts/` -- poller, daemon, hooks, PDF generation, setup scripts
|
|
106
|
+
- `scripts/` -- poller, daemon, hooks, PDF generation, setup scripts, **cadence bus enqueue + consumer + wrapper**
|
|
107
|
+
- `lib/` -- shared framework primitives (singleton lock, **cadence bus**, action executor, tool definitions)
|
|
107
108
|
- `policies/` -- action classification, information barriers, prompt injection defence
|
|
108
109
|
- `docs/` -- architecture, governance, runbooks
|
|
109
110
|
- `public/assets/` -- brand assets
|
|
@@ -112,20 +113,34 @@ Updates framework files in an existing agent repo. This command copies the lates
|
|
|
112
113
|
- `plugins/maestro-skills/` -- operational skills
|
|
113
114
|
- `agents/` -- new agents are added, existing ones preserved (no deletions)
|
|
114
115
|
|
|
115
|
-
It also creates any missing directories from the expanded directory set
|
|
116
|
+
It also creates any missing directories from the expanded directory set, including `state/cadence-bus/{inbox,claimed,processed,failed,dlq}` and `logs/cadence-bus/`.
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
**Cadence-bus migration (1.8+):** `upgrade` automatically migrates legacy spawn-per-tick launchd plists:
|
|
119
|
+
|
|
120
|
+
1. Detects generated plists at `scripts/local-triggers/plists/` that still call `run-trigger.sh` directly.
|
|
121
|
+
2. Backs them up to `.maestro/backup/plists/<utc-timestamp>/`.
|
|
122
|
+
3. Regenerates them via `scripts/local-triggers/generate-plists.sh` so they invoke the lightweight cadence enqueue script instead.
|
|
123
|
+
4. Surfaces any installed plists at `~/Library/LaunchAgents/ai.adaptic.*` that still match the legacy pattern, with exact `launchctl unload && launchctl load` commands the operator can run to roll the live job.
|
|
124
|
+
|
|
125
|
+
The migration is idempotent — a second `upgrade` run on a fully-migrated repo is a clean no-op.
|
|
126
|
+
|
|
127
|
+
Agent-specific files (`config/`, `CLAUDE.md`, `knowledge/`, `memory/`, `state/`, `logs/`, `outputs/`, `.env`) are never touched.
|
|
118
128
|
|
|
119
129
|
### `npx @adaptic/maestro doctor`
|
|
120
130
|
|
|
121
|
-
Verifies the agent installation. Checks
|
|
131
|
+
Verifies the agent installation end-to-end. Checks include:
|
|
132
|
+
|
|
133
|
+
- **Framework files:** `config/agent.ts`, `CLAUDE.md`, `.claude/settings.json`, `.claude/commands/init-maestro.md`, `package.json`, `scripts/setup/init-agent.sh`, `scripts/healthcheck.sh`, `scripts/daemon/maestro-daemon.mjs`, `scripts/local-triggers/generate-plists.sh`.
|
|
134
|
+
- **Cadence bus (1.8+):** `lib/cadence-bus.mjs`, `scripts/cadence/enqueue-cadence-tick.mjs`, `scripts/cadence/launchd-cadence-wrapper.sh`, `scripts/daemon/cadence-consumer.mjs`, `scripts/daemon/cadence-handlers.mjs`, plus the `state/cadence-bus/{inbox,claimed,processed,failed,dlq}` directory tree.
|
|
135
|
+
- **Plist architecture:** every generated plist at `scripts/local-triggers/plists/` carries the `maestro-plist-arch: cadence-bus` marker, and NO plist still calls `run-trigger.sh` directly. Installed plists under `~/Library/LaunchAgents/ai.adaptic.*` are spot-checked too; doctor prints the exact `launchctl unload && launchctl load` commands to migrate any stragglers.
|
|
136
|
+
- **Daemon heartbeat:** `state/cadence-bus/health.json` is fresh (under 60s) when the daemon is running.
|
|
137
|
+
- **Smoke test:** doctor enqueues a `cadence-bus-heartbeat` tick to confirm the producer side works end-to-end.
|
|
138
|
+
- **Config:** `config/environment.yaml`, `config/contacts.yaml`, `config/priorities.yaml`, `config/sla-defaults.yaml`.
|
|
139
|
+
- **State:** `state/dashboards/executive-summary.yaml`, `state/queues/action-stack.yaml`, `knowledge/decisions/decision-schema.yaml`.
|
|
140
|
+
- **Environment:** `.env` file with `ANTHROPIC_API_KEY` (required), `SLACK_USER_TOKEN`, `GMAIL_APP_PASSWORD` (optional).
|
|
141
|
+
- **Dependencies:** `node_modules` installed, Claude CLI available, jq available, emergency-stop script present.
|
|
122
142
|
|
|
123
|
-
-
|
|
124
|
-
- Setup: `scripts/setup/init-agent.sh`, `scripts/healthcheck.sh`, `scripts/daemon/maestro-daemon.mjs`, `scripts/local-triggers/generate-plists.sh`
|
|
125
|
-
- Config: `config/environment.yaml`, `config/contacts.yaml`, `config/priorities.yaml`, `config/sla-defaults.yaml`
|
|
126
|
-
- State: `state/dashboards/executive-summary.yaml`, `state/queues/action-stack.yaml`, `knowledge/decisions/decision-schema.yaml`
|
|
127
|
-
- Environment: `.env` file with `ANTHROPIC_API_KEY` (required), `SLACK_USER_TOKEN`, `GMAIL_APP_PASSWORD` (optional)
|
|
128
|
-
- Dependencies: `node_modules` installed, Claude CLI available
|
|
143
|
+
Doctor exits non-zero when issues are found and prints actionable remediation (most commonly: `npx @adaptic/maestro upgrade`).
|
|
129
144
|
|
|
130
145
|
---
|
|
131
146
|
|
|
@@ -253,10 +268,46 @@ All Maestro agents operate in three concurrent modes:
|
|
|
253
268
|
|
|
254
269
|
**Mode 1: Reactive** -- The nervous system. A lightweight poller checks Slack, Gmail, and Calendar every 60 seconds. An inbox processor classifies and routes incoming items every 5 minutes. Priority events trigger immediate processing.
|
|
255
270
|
|
|
256
|
-
**Mode 2: Scheduled** -- The heartbeat. Daily morning brief, midday sweep, evening wrap. Weekly strategic memo, pipeline review, execution review. Monthly board readiness, risk refresh. Quarterly self-assessment and board pack. All
|
|
271
|
+
**Mode 2: Scheduled** -- The heartbeat. Daily morning brief, midday sweep, evening wrap. Weekly strategic memo, pipeline review, execution review. Monthly board readiness, risk refresh. Quarterly self-assessment and board pack. All scheduled via macOS launchd; cadence ticks flow through the cadence bus (see below) and are serviced by the persistent daemon, NOT by spawning a fresh Claude Code session per tick.
|
|
257
272
|
|
|
258
273
|
**Mode 3: Proactive** -- The engine. The backlog executor runs every 10 minutes, reads all queues, selects the top actionable items by priority, and spawns parallel agents to execute them. Items move continuously from `open` to `in_progress` to `resolved` to `closed`.
|
|
259
274
|
|
|
275
|
+
### Cadence Bus
|
|
276
|
+
|
|
277
|
+
Scheduled cadence ticks (every 5 / 10 / 15 / 30 minutes, daily, weekly, monthly, quarterly) are decoupled from Claude Code via a local file-backed event bus at `state/cadence-bus/`:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
launchd ──► scripts/cadence/enqueue-cadence-tick.mjs (≈10 ms, no Claude)
|
|
281
|
+
│
|
|
282
|
+
▼
|
|
283
|
+
state/cadence-bus/inbox/<event>.json
|
|
284
|
+
│
|
|
285
|
+
▼
|
|
286
|
+
maestro-daemon.mjs ──► cadence-consumer.mjs (single persistent owner)
|
|
287
|
+
│
|
|
288
|
+
┌──────┴──────┐
|
|
289
|
+
▼ ▼
|
|
290
|
+
inline sub-session
|
|
291
|
+
(housekeeping) (substantive work)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Why this matters.** The previous architecture spawned a fresh `claude --print` session per cadence tick — dozens of full Claude Code spawns per day, each paying full auth/context/token overhead even when the tick had nothing to do. The cadence bus:
|
|
295
|
+
|
|
296
|
+
- Routes every tick through ONE persistent main session (the daemon).
|
|
297
|
+
- Handles lightweight ticks **inline** (no Claude spawned) — heartbeats, housekeeping, queue sweeps with cheap pre-checks.
|
|
298
|
+
- Only spawns a sub-session when the cadence genuinely warrants isolated work: substantive drafting, multi-step outreach, large audits, research, work requiring separate context/audit boundaries.
|
|
299
|
+
- Honours `.emergency-stop` at both producer and consumer.
|
|
300
|
+
- Is safe if the daemon is briefly down — events accumulate in `inbox/` and drain on next startup.
|
|
301
|
+
- Records the full lifecycle (received → claimed → processed | escalated | failed | dlq) under `logs/cadence-bus/<date>.jsonl`.
|
|
302
|
+
|
|
303
|
+
Per-cadence policy lives in `scripts/daemon/cadence-handlers.mjs`:
|
|
304
|
+
|
|
305
|
+
- **inline** — handler runs entirely in-process (e.g. heartbeat, stale-claim sweep).
|
|
306
|
+
- **guarded** — cheap pre-check (queues empty? inbox empty?); only escalates if there's substantive work.
|
|
307
|
+
- **escalate** — spawns a sub-session running the cadence's trigger prompt under `schedules/triggers/<name>.md`.
|
|
308
|
+
|
|
309
|
+
See `docs/runbooks/perpetual-operations.md` for ops procedures.
|
|
310
|
+
|
|
260
311
|
---
|
|
261
312
|
|
|
262
313
|
## Auto-Publishing
|
package/bin/maestro.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* npx @adaptic/maestro doctor # Verify installation and configuration
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { resolve, join, dirname, relative, sep } from "node:path";
|
|
11
|
+
import { resolve, join, dirname, relative, sep, basename } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import {
|
|
14
14
|
mkdirSync,
|
|
@@ -21,13 +21,20 @@ import {
|
|
|
21
21
|
statSync,
|
|
22
22
|
lstatSync,
|
|
23
23
|
} from "node:fs";
|
|
24
|
-
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
|
|
27
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const MAESTRO_ROOT = resolve(__dirname, "..");
|
|
29
29
|
const SCAFFOLD_DIR = join(MAESTRO_ROOT, "scaffold");
|
|
30
30
|
|
|
31
|
+
// Plist architecture marker — must match the value emitted by
|
|
32
|
+
// scripts/local-triggers/generate-plists.sh. Used by doctor + upgrade
|
|
33
|
+
// migration to distinguish cadence-bus plists from the legacy
|
|
34
|
+
// spawn-per-tick pattern (which invoked run-trigger.sh directly).
|
|
35
|
+
const PLIST_ARCH_MARKER = "maestro-plist-arch: cadence-bus";
|
|
36
|
+
const LEGACY_PLIST_INDICATOR = "run-trigger.sh";
|
|
37
|
+
|
|
31
38
|
// ---------------------------------------------------------------------------
|
|
32
39
|
// Colours
|
|
33
40
|
// ---------------------------------------------------------------------------
|
|
@@ -87,6 +94,11 @@ function create(targetName) {
|
|
|
87
94
|
"ingest",
|
|
88
95
|
"mcp",
|
|
89
96
|
"services",
|
|
97
|
+
// lib/ holds shared framework primitives — singleton lock, cadence bus,
|
|
98
|
+
// action executor, tool definitions — that scripts/ imports via relative
|
|
99
|
+
// paths. Must travel with each agent repo so the relative imports work
|
|
100
|
+
// without depending on a checkout of @adaptic/maestro on the same host.
|
|
101
|
+
"lib",
|
|
90
102
|
];
|
|
91
103
|
|
|
92
104
|
for (const dir of frameworkDirs) {
|
|
@@ -153,6 +165,12 @@ function create(targetName) {
|
|
|
153
165
|
"state/slack-responded",
|
|
154
166
|
"state/slack-thread-tracker",
|
|
155
167
|
"state/tmp",
|
|
168
|
+
"state/cadence-bus",
|
|
169
|
+
"state/cadence-bus/inbox",
|
|
170
|
+
"state/cadence-bus/claimed",
|
|
171
|
+
"state/cadence-bus/processed",
|
|
172
|
+
"state/cadence-bus/failed",
|
|
173
|
+
"state/cadence-bus/dlq",
|
|
156
174
|
"outputs/briefs",
|
|
157
175
|
"outputs/drafts",
|
|
158
176
|
"outputs/memos",
|
|
@@ -167,6 +185,7 @@ function create(targetName) {
|
|
|
167
185
|
"logs/evolution",
|
|
168
186
|
"logs/huddle",
|
|
169
187
|
"logs/daemon",
|
|
188
|
+
"logs/cadence-bus",
|
|
170
189
|
"logs/infra",
|
|
171
190
|
"logs/monitor",
|
|
172
191
|
"logs/phone",
|
|
@@ -583,6 +602,9 @@ rubrics: []
|
|
|
583
602
|
"configure-macos": "sudo ./scripts/setup/configure-macos.sh",
|
|
584
603
|
"configure-macos:check": "./scripts/setup/configure-macos.sh --check",
|
|
585
604
|
daemon: "node scripts/daemon/maestro-daemon.mjs",
|
|
605
|
+
"cadence:enqueue": "node scripts/cadence/enqueue-cadence-tick.mjs",
|
|
606
|
+
"cadence:consume": "node scripts/daemon/cadence-consumer.mjs",
|
|
607
|
+
"cadence:status": "node scripts/cadence/cadence-status.mjs",
|
|
586
608
|
healthcheck: "./scripts/healthcheck.sh",
|
|
587
609
|
"emergency-stop": "./scripts/emergency-stop.sh",
|
|
588
610
|
resume: "./scripts/resume-operations.sh",
|
|
@@ -710,6 +732,10 @@ const UPGRADE_PATHS = [
|
|
|
710
732
|
{ path: "plugins/maestro-skills", mode: "smart" },
|
|
711
733
|
{ path: "teams", mode: "smart" },
|
|
712
734
|
{ path: "agents", mode: "merge" },
|
|
735
|
+
// Framework primitives — colocated with scripts/ so relative imports
|
|
736
|
+
// (e.g. ../../lib/cadence-bus.mjs from scripts/cadence/*) resolve in
|
|
737
|
+
// every agent repo without needing @adaptic/maestro in node_modules.
|
|
738
|
+
{ path: "lib", mode: "smart" },
|
|
713
739
|
];
|
|
714
740
|
|
|
715
741
|
function sha256File(p) {
|
|
@@ -832,6 +858,186 @@ function dirtyPathSet(cwd) {
|
|
|
832
858
|
return set;
|
|
833
859
|
}
|
|
834
860
|
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
// Cadence-bus migration (called from upgrade)
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
//
|
|
865
|
+
// As of maestro 1.8 scheduled cadence ticks no longer spawn a fresh Claude
|
|
866
|
+
// Code session per tick. launchd plists now invoke a lightweight Node
|
|
867
|
+
// enqueue script that drops a JSON event onto state/cadence-bus/; the
|
|
868
|
+
// persistent maestro-daemon.mjs consumes the bus.
|
|
869
|
+
//
|
|
870
|
+
// This migration runs idempotently on every upgrade:
|
|
871
|
+
// 1. Detect generated plists at scripts/local-triggers/plists/ that still
|
|
872
|
+
// reference run-trigger.sh (the legacy per-tick spawn path).
|
|
873
|
+
// 2. Back them up to .maestro/backup/plists/<utc-timestamp>/.
|
|
874
|
+
// 3. Regenerate plists via scripts/local-triggers/generate-plists.sh so
|
|
875
|
+
// the on-disk files match the cadence-bus architecture.
|
|
876
|
+
// 4. Surface installed plists at ~/Library/LaunchAgents/ that still match
|
|
877
|
+
// the legacy pattern, with exact launchctl unload/load commands the
|
|
878
|
+
// operator can run.
|
|
879
|
+
//
|
|
880
|
+
// Returns a summary object the caller uses to print user-visible output.
|
|
881
|
+
|
|
882
|
+
function plistRefersToLegacy(path) {
|
|
883
|
+
try {
|
|
884
|
+
const body = readFileSync(path, "utf-8");
|
|
885
|
+
return body.includes(LEGACY_PLIST_INDICATOR);
|
|
886
|
+
} catch {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function plistMatchesArch(path) {
|
|
892
|
+
try {
|
|
893
|
+
const body = readFileSync(path, "utf-8");
|
|
894
|
+
return body.includes(PLIST_ARCH_MARKER);
|
|
895
|
+
} catch {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function listPlists(dir) {
|
|
901
|
+
if (!existsSync(dir)) return [];
|
|
902
|
+
try {
|
|
903
|
+
return readdirSync(dir).filter((n) => n.endsWith(".plist")).map((n) => join(dir, n));
|
|
904
|
+
} catch {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function migrateCadenceBus(cwd, flags) {
|
|
910
|
+
const summary = {
|
|
911
|
+
generated_legacy: [],
|
|
912
|
+
generated_modern: [],
|
|
913
|
+
installed_legacy: [],
|
|
914
|
+
backed_up: [],
|
|
915
|
+
regenerated: false,
|
|
916
|
+
skipped_regen_reason: null,
|
|
917
|
+
bus_dir: null,
|
|
918
|
+
notes: [],
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// Bootstrap the cadence-bus directory tree even when nothing else is
|
|
922
|
+
// moving — needed for fresh installs that upgrade through this path.
|
|
923
|
+
const busBase = join(cwd, "state", "cadence-bus");
|
|
924
|
+
summary.bus_dir = busBase;
|
|
925
|
+
for (const d of ["inbox", "claimed", "processed", "failed", "dlq"]) {
|
|
926
|
+
const full = join(busBase, d);
|
|
927
|
+
if (!existsSync(full) && !flags.dryRun) {
|
|
928
|
+
mkdirSync(full, { recursive: true });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const versionFile = join(busBase, "VERSION");
|
|
932
|
+
if (!existsSync(versionFile) && !flags.dryRun) {
|
|
933
|
+
writeFileSync(versionFile, "1\n");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Inspect generated plists.
|
|
937
|
+
const generatedDir = join(cwd, "scripts/local-triggers/plists");
|
|
938
|
+
const generated = listPlists(generatedDir);
|
|
939
|
+
for (const p of generated) {
|
|
940
|
+
const rel = relative(cwd, p);
|
|
941
|
+
if (plistRefersToLegacy(p)) summary.generated_legacy.push(rel);
|
|
942
|
+
else if (plistMatchesArch(p)) summary.generated_modern.push(rel);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Inspect installed plists.
|
|
946
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
947
|
+
for (const p of listPlists(installedDir)) {
|
|
948
|
+
const name = basename(p);
|
|
949
|
+
// Only consider plists owned by Maestro — labels start with ai.adaptic.
|
|
950
|
+
if (!name.startsWith("ai.adaptic.")) continue;
|
|
951
|
+
if (plistRefersToLegacy(p)) summary.installed_legacy.push(p);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Back up any legacy generated plists.
|
|
955
|
+
if (summary.generated_legacy.length > 0) {
|
|
956
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
957
|
+
const backupDir = join(cwd, ".maestro/backup/plists", ts);
|
|
958
|
+
if (!flags.dryRun) {
|
|
959
|
+
mkdirSync(backupDir, { recursive: true });
|
|
960
|
+
for (const rel of summary.generated_legacy) {
|
|
961
|
+
const src = join(cwd, rel);
|
|
962
|
+
const dst = join(backupDir, basename(src));
|
|
963
|
+
try {
|
|
964
|
+
copyFileSync(src, dst);
|
|
965
|
+
summary.backed_up.push(relative(cwd, dst));
|
|
966
|
+
} catch (err) {
|
|
967
|
+
summary.notes.push(`backup failed for ${rel}: ${err.message}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
// Dry-run: still report what would be backed up.
|
|
972
|
+
summary.notes.push(`would back up ${summary.generated_legacy.length} legacy plist(s) to .maestro/backup/plists/${ts}/`);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Regenerate via the bundled script. Soft failure — operator can rerun.
|
|
976
|
+
const generator = join(cwd, "scripts/local-triggers/generate-plists.sh");
|
|
977
|
+
if (existsSync(generator)) {
|
|
978
|
+
if (!flags.dryRun) {
|
|
979
|
+
try {
|
|
980
|
+
execFileSync("/bin/bash", [generator], { cwd, stdio: "pipe" });
|
|
981
|
+
summary.regenerated = true;
|
|
982
|
+
} catch (err) {
|
|
983
|
+
summary.skipped_regen_reason = `generate-plists.sh failed: ${err.message.split("\n")[0]}`;
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
summary.notes.push("dry-run: would regenerate plists via scripts/local-triggers/generate-plists.sh");
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
summary.skipped_regen_reason = "scripts/local-triggers/generate-plists.sh missing";
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return summary;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function printCadenceBusSummary(summary, flags) {
|
|
997
|
+
console.log();
|
|
998
|
+
console.log(`${C.bold}Cadence bus migration${C.reset}${flags.dryRun ? " (dry run)" : ""}`);
|
|
999
|
+
if (summary.generated_legacy.length === 0 && summary.installed_legacy.length === 0) {
|
|
1000
|
+
ok("Already on cadence-bus architecture (no legacy plists found).");
|
|
1001
|
+
if (summary.generated_modern.length > 0) {
|
|
1002
|
+
ok(`${summary.generated_modern.length} generated plist(s) carry the cadence-bus marker.`);
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (summary.generated_legacy.length > 0) {
|
|
1007
|
+
warn(`${summary.generated_legacy.length} generated plist(s) still call run-trigger.sh directly:`);
|
|
1008
|
+
for (const p of summary.generated_legacy.slice(0, 8)) console.log(` ${p}`);
|
|
1009
|
+
if (summary.generated_legacy.length > 8) {
|
|
1010
|
+
console.log(` … and ${summary.generated_legacy.length - 8} more`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (summary.backed_up.length > 0) {
|
|
1014
|
+
ok(`Backed up ${summary.backed_up.length} legacy plist(s) under ${dirname(summary.backed_up[0])}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (summary.regenerated) {
|
|
1017
|
+
ok("Regenerated plists with the cadence-bus architecture.");
|
|
1018
|
+
} else if (summary.skipped_regen_reason) {
|
|
1019
|
+
warn(`Skipped plist regeneration: ${summary.skipped_regen_reason}`);
|
|
1020
|
+
warn("Run manually: ./scripts/local-triggers/generate-plists.sh");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (summary.installed_legacy.length > 0) {
|
|
1024
|
+
console.log();
|
|
1025
|
+
warn(`${summary.installed_legacy.length} installed launchd plist(s) at ~/Library/LaunchAgents/ still use the legacy spawn-per-tick pattern.`);
|
|
1026
|
+
warn("Reload them so launchctl picks up the cadence-bus versions:");
|
|
1027
|
+
for (const p of summary.installed_legacy.slice(0, 8)) {
|
|
1028
|
+
const label = basename(p, ".plist");
|
|
1029
|
+
console.log(` launchctl unload "${p}" && launchctl load "${p}"`);
|
|
1030
|
+
// Cosmetic alias for label clarity.
|
|
1031
|
+
void label;
|
|
1032
|
+
}
|
|
1033
|
+
if (summary.installed_legacy.length > 8) {
|
|
1034
|
+
console.log(` … and ${summary.installed_legacy.length - 8} more`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
for (const note of summary.notes) console.log(` note: ${note}`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
835
1041
|
function parseUpgradeFlags(args) {
|
|
836
1042
|
const flags = {
|
|
837
1043
|
dryRun: false,
|
|
@@ -1005,6 +1211,10 @@ Per-file behaviour:
|
|
|
1005
1211
|
"knowledge/decisions/archive", "self-optimization/scenarios", "tests",
|
|
1006
1212
|
"logs/infra", "logs/monitor", "logs/phone", "logs/sms",
|
|
1007
1213
|
"logs/whatsapp", "logs/email",
|
|
1214
|
+
// Cadence bus (architecture introduced in maestro 1.8). Idempotent.
|
|
1215
|
+
"state/cadence-bus", "state/cadence-bus/inbox", "state/cadence-bus/claimed",
|
|
1216
|
+
"state/cadence-bus/processed", "state/cadence-bus/failed", "state/cadence-bus/dlq",
|
|
1217
|
+
"logs/cadence-bus",
|
|
1008
1218
|
];
|
|
1009
1219
|
let newDirs = 0;
|
|
1010
1220
|
for (const dir of ensureDirs) {
|
|
@@ -1015,6 +1225,10 @@ Per-file behaviour:
|
|
|
1015
1225
|
}
|
|
1016
1226
|
}
|
|
1017
1227
|
|
|
1228
|
+
// Cadence-bus migration: detect legacy spawn-per-tick plists and rewrite
|
|
1229
|
+
// them so launchd enqueues cadence events instead of spawning Claude.
|
|
1230
|
+
const cadenceMigration = migrateCadenceBus(cwd, flags);
|
|
1231
|
+
|
|
1018
1232
|
// Regenerate config/agent.env from config/agent.json so newly-installed
|
|
1019
1233
|
// (or already-installed) shell scripts find the agent identity vars.
|
|
1020
1234
|
// Soft failure — older agents that haven't migrated to the SOT layout
|
|
@@ -1052,6 +1266,10 @@ Per-file behaviour:
|
|
|
1052
1266
|
if (preservedFiles.length > 5) console.log(` … and ${preservedFiles.length - 5} more`);
|
|
1053
1267
|
}
|
|
1054
1268
|
|
|
1269
|
+
// Surface the cadence-bus migration summary AFTER the file-by-file
|
|
1270
|
+
// upgrade summary so the operator sees both layers of change.
|
|
1271
|
+
printCadenceBusSummary(cadenceMigration, flags);
|
|
1272
|
+
|
|
1055
1273
|
console.log();
|
|
1056
1274
|
log("Agent-specific paths (config/, CLAUDE.md, knowledge/, memory/, state/, outputs/, logs/, .env) were NOT touched.");
|
|
1057
1275
|
}
|
|
@@ -1068,6 +1286,7 @@ function doctor() {
|
|
|
1068
1286
|
|
|
1069
1287
|
let issues = 0;
|
|
1070
1288
|
|
|
1289
|
+
// ── Framework files ─────────────────────────────────────────────────────
|
|
1071
1290
|
const essentialFiles = [
|
|
1072
1291
|
"config/agent.ts", "CLAUDE.md", ".claude/settings.json",
|
|
1073
1292
|
".claude/commands/init-maestro.md", "package.json",
|
|
@@ -1079,6 +1298,13 @@ function doctor() {
|
|
|
1079
1298
|
"state/dashboards/executive-summary.yaml",
|
|
1080
1299
|
"state/queues/action-stack.yaml",
|
|
1081
1300
|
"knowledge/decisions/decision-schema.yaml",
|
|
1301
|
+
// Cadence-bus architecture (maestro 1.8+)
|
|
1302
|
+
"lib/cadence-bus.mjs",
|
|
1303
|
+
"scripts/cadence/enqueue-cadence-tick.mjs",
|
|
1304
|
+
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
1305
|
+
"scripts/cadence/cadence-status.mjs",
|
|
1306
|
+
"scripts/daemon/cadence-consumer.mjs",
|
|
1307
|
+
"scripts/daemon/cadence-handlers.mjs",
|
|
1082
1308
|
];
|
|
1083
1309
|
|
|
1084
1310
|
for (const file of essentialFiles) {
|
|
@@ -1086,6 +1312,102 @@ function doctor() {
|
|
|
1086
1312
|
else { fail(`Missing: ${file}`); issues++; }
|
|
1087
1313
|
}
|
|
1088
1314
|
|
|
1315
|
+
// ── Cadence bus state directories ───────────────────────────────────────
|
|
1316
|
+
const busDirs = [
|
|
1317
|
+
"state/cadence-bus",
|
|
1318
|
+
"state/cadence-bus/inbox",
|
|
1319
|
+
"state/cadence-bus/claimed",
|
|
1320
|
+
"state/cadence-bus/processed",
|
|
1321
|
+
"state/cadence-bus/failed",
|
|
1322
|
+
"state/cadence-bus/dlq",
|
|
1323
|
+
"logs/cadence-bus",
|
|
1324
|
+
];
|
|
1325
|
+
for (const d of busDirs) {
|
|
1326
|
+
if (existsSync(join(cwd, d))) ok(d);
|
|
1327
|
+
else { warn(`Missing: ${d} — run: npx @adaptic/maestro upgrade`); issues++; }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// ── Launchd plist architecture ──────────────────────────────────────────
|
|
1331
|
+
const plistDir = join(cwd, "scripts/local-triggers/plists");
|
|
1332
|
+
if (existsSync(plistDir)) {
|
|
1333
|
+
const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
|
|
1334
|
+
let legacy = 0;
|
|
1335
|
+
let modern = 0;
|
|
1336
|
+
for (const name of plists) {
|
|
1337
|
+
const body = readFileSync(join(plistDir, name), "utf-8");
|
|
1338
|
+
if (body.includes(LEGACY_PLIST_INDICATOR)) legacy++;
|
|
1339
|
+
else if (body.includes(PLIST_ARCH_MARKER)) modern++;
|
|
1340
|
+
}
|
|
1341
|
+
if (plists.length === 0) {
|
|
1342
|
+
warn("scripts/local-triggers/plists/ is empty — run: scripts/local-triggers/generate-plists.sh");
|
|
1343
|
+
issues++;
|
|
1344
|
+
} else if (legacy > 0) {
|
|
1345
|
+
// Legacy plists are a migration warning rather than a fatal — the
|
|
1346
|
+
// operator can fix them with `maestro upgrade`. Use warn() so the
|
|
1347
|
+
// message lands on stdout where scripted callers will see it.
|
|
1348
|
+
warn(`${legacy} plist(s) still call run-trigger.sh directly (legacy spawn-per-tick).`);
|
|
1349
|
+
warn(` Fix: npx @adaptic/maestro upgrade (will back up + regenerate)`);
|
|
1350
|
+
issues++;
|
|
1351
|
+
} else {
|
|
1352
|
+
ok(`${modern}/${plists.length} plist(s) use the cadence-bus architecture`);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Spot-check installed plists at ~/Library/LaunchAgents/.
|
|
1356
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
1357
|
+
if (existsSync(installedDir)) {
|
|
1358
|
+
let installedLegacy = 0;
|
|
1359
|
+
for (const name of readdirSync(installedDir)) {
|
|
1360
|
+
if (!name.startsWith("ai.adaptic.")) continue;
|
|
1361
|
+
if (!name.endsWith(".plist")) continue;
|
|
1362
|
+
const body = readFileSync(join(installedDir, name), "utf-8");
|
|
1363
|
+
if (body.includes(LEGACY_PLIST_INDICATOR)) installedLegacy++;
|
|
1364
|
+
}
|
|
1365
|
+
if (installedLegacy > 0) {
|
|
1366
|
+
warn(`${installedLegacy} installed launchd plist(s) still use the legacy pattern.`);
|
|
1367
|
+
warn(" Fix: npx @adaptic/maestro upgrade then follow the printed launchctl commands.");
|
|
1368
|
+
issues++;
|
|
1369
|
+
} else {
|
|
1370
|
+
ok("Installed launchd plists (ai.adaptic.*) are on cadence-bus or absent");
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
} else {
|
|
1374
|
+
warn("scripts/local-triggers/plists/ does not exist yet — run scripts/setup/init-agent.sh");
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ── Cadence consumer heartbeat ──────────────────────────────────────────
|
|
1378
|
+
try {
|
|
1379
|
+
// Read via the bundled lib so we don't reinvent path resolution.
|
|
1380
|
+
const mod = readFileSync(join(cwd, "state/cadence-bus/health.json"), "utf-8");
|
|
1381
|
+
const health = JSON.parse(mod);
|
|
1382
|
+
const ageMs = Date.now() - new Date(health.ts).getTime();
|
|
1383
|
+
if (ageMs < 60_000) ok(`Cadence consumer heartbeat fresh (${Math.round(ageMs / 1000)}s)`);
|
|
1384
|
+
else if (ageMs < 5 * 60_000) warn(`Cadence consumer heartbeat stale (${Math.round(ageMs / 1000)}s) — daemon may be slow.`);
|
|
1385
|
+
else {
|
|
1386
|
+
warn(`Cadence consumer heartbeat very stale (${Math.round(ageMs / 1000)}s) — start the daemon: npm run daemon`);
|
|
1387
|
+
issues++;
|
|
1388
|
+
}
|
|
1389
|
+
} catch {
|
|
1390
|
+
warn("No cadence consumer heartbeat yet — start the daemon: npm run daemon");
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ── Manual tick smoke test ──────────────────────────────────────────────
|
|
1394
|
+
// Enqueue a heartbeat tick if cadence files are in place. The bus
|
|
1395
|
+
// accepts the event even when the consumer isn't running; the file lands
|
|
1396
|
+
// on disk and is recoverable. This proves the producer side end-to-end.
|
|
1397
|
+
const enqueueScript = join(cwd, "scripts/cadence/enqueue-cadence-tick.mjs");
|
|
1398
|
+
if (existsSync(enqueueScript)) {
|
|
1399
|
+
const result = spawnSync(process.execPath, [
|
|
1400
|
+
enqueueScript, "cadence-bus-heartbeat",
|
|
1401
|
+
"--source=daemon", "--metadata=note=doctor-smoke", "--quiet",
|
|
1402
|
+
], { cwd, encoding: "utf-8", env: { ...process.env, AGENT_ROOT: cwd } });
|
|
1403
|
+
if (result.status === 0) ok("Cadence enqueue smoke test passed");
|
|
1404
|
+
else {
|
|
1405
|
+
fail(`Cadence enqueue smoke test failed (exit ${result.status}): ${(result.stderr || "").trim()}`);
|
|
1406
|
+
issues++;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ── .env ────────────────────────────────────────────────────────────────
|
|
1089
1411
|
if (existsSync(join(cwd, ".env"))) {
|
|
1090
1412
|
const env = readFileSync(join(cwd, ".env"), "utf-8");
|
|
1091
1413
|
const check = (key, required) => {
|
|
@@ -1102,6 +1424,7 @@ function doctor() {
|
|
|
1102
1424
|
issues++;
|
|
1103
1425
|
}
|
|
1104
1426
|
|
|
1427
|
+
// ── Dependencies ────────────────────────────────────────────────────────
|
|
1105
1428
|
if (existsSync(join(cwd, "node_modules"))) ok("node_modules installed");
|
|
1106
1429
|
else { fail("node_modules not found — run: npm install"); issues++; }
|
|
1107
1430
|
|
|
@@ -1113,9 +1436,22 @@ function doctor() {
|
|
|
1113
1436
|
issues++;
|
|
1114
1437
|
}
|
|
1115
1438
|
|
|
1439
|
+
try {
|
|
1440
|
+
execFileSync("which", ["jq"], { stdio: "pipe" });
|
|
1441
|
+
ok("jq installed");
|
|
1442
|
+
} catch {
|
|
1443
|
+
warn("jq not found — install: brew install jq (used by various shell helpers)");
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// ── Emergency-stop wiring ───────────────────────────────────────────────
|
|
1447
|
+
const stopScript = join(cwd, "scripts/emergency-stop.sh");
|
|
1448
|
+
if (existsSync(stopScript)) ok("scripts/emergency-stop.sh present");
|
|
1449
|
+
else { warn("scripts/emergency-stop.sh missing — kill-switch unavailable"); issues++; }
|
|
1450
|
+
|
|
1116
1451
|
console.log();
|
|
1117
1452
|
if (issues === 0) ok("All checks passed.");
|
|
1118
1453
|
else warn(`${issues} issue(s) found.`);
|
|
1454
|
+
process.exitCode = issues === 0 ? 0 : 1;
|
|
1119
1455
|
}
|
|
1120
1456
|
|
|
1121
1457
|
// ---------------------------------------------------------------------------
|