@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.
Files changed (36) hide show
  1. package/.claude/commands/init-maestro.md +15 -2
  2. package/.gitignore +7 -0
  3. package/README.md +62 -11
  4. package/bin/maestro.mjs +338 -2
  5. package/bin/maestro.test.mjs +299 -0
  6. package/docs/guides/poller-daemon-setup.md +21 -8
  7. package/docs/runbooks/perpetual-operations.md +19 -15
  8. package/docs/runbooks/recovery-and-failover.md +42 -0
  9. package/lib/cadence-bus.mjs +625 -0
  10. package/lib/cadence-bus.test.mjs +354 -0
  11. package/package.json +6 -1
  12. package/scaffold/CLAUDE.md +11 -7
  13. package/scripts/cadence/cadence-status.mjs +36 -0
  14. package/scripts/cadence/enqueue-cadence-tick.mjs +158 -0
  15. package/scripts/cadence/enqueue-cadence-tick.test.mjs +154 -0
  16. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  17. package/scripts/daemon/cadence-consumer.mjs +439 -0
  18. package/scripts/daemon/cadence-consumer.test.mjs +397 -0
  19. package/scripts/daemon/cadence-handlers.mjs +263 -0
  20. package/scripts/daemon/maestro-daemon.mjs +20 -0
  21. package/scripts/local-triggers/generate-plists.sh +62 -17
  22. package/scripts/local-triggers/generate-plists.test.mjs +254 -0
  23. package/scripts/local-triggers/plists/.gitkeep +0 -0
  24. package/scripts/local-triggers/run-trigger.sh +22 -3
  25. package/scripts/local-triggers/plists/ai.adaptic.sophie-backlog-executor.plist +0 -21
  26. package/scripts/local-triggers/plists/ai.adaptic.sophie-daemon.plist +0 -32
  27. package/scripts/local-triggers/plists/ai.adaptic.sophie-inbox-processor.plist +0 -21
  28. package/scripts/local-triggers/plists/ai.adaptic.sophie-meeting-action-capture.plist +0 -21
  29. package/scripts/local-triggers/plists/ai.adaptic.sophie-meeting-prep.plist +0 -21
  30. package/scripts/local-triggers/plists/ai.adaptic.sophie-midday-sweep.plist +0 -26
  31. package/scripts/local-triggers/plists/ai.adaptic.sophie-quarterly-self-assessment.plist +0 -62
  32. package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-engineering-health.plist +0 -28
  33. package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-execution.plist +0 -28
  34. package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-hiring.plist +0 -28
  35. package/scripts/local-triggers/plists/ai.adaptic.sophie-weekly-priorities.plist +0 -28
  36. 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
- Agent-specific files (`config/`, `CLAUDE.md`, `knowledge/`, `memory/`, `state/`, `logs/`) are never touched.
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 for 16 essential files:
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
- - Core: `config/agent.ts`, `CLAUDE.md`, `.claude/settings.json`, `.claude/commands/init-maestro.md`, `package.json`
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 triggered via macOS launchd.
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
  // ---------------------------------------------------------------------------