@adaptic/maestro 1.7.3 → 1.8.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/.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 +33 -12
- package/scripts/local-triggers/generate-plists.test.mjs +185 -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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* maestro.test.mjs — CLI tests for the bin/maestro.mjs commands.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* • `create` scaffolds the cadence-bus directory tree, the enqueue
|
|
6
|
+
* script, the consumer, and the cadence handlers under the new agent.
|
|
7
|
+
* • `upgrade` migrates a legacy-style agent repo: backs up old plists,
|
|
8
|
+
* bootstraps state/cadence-bus/, regenerates plists.
|
|
9
|
+
* • `upgrade` preserves agent-specific files (CLAUDE.md, config/,
|
|
10
|
+
* knowledge/, memory/, state/, logs/, outputs/, .env) untouched.
|
|
11
|
+
* • `upgrade` is idempotent when run twice on the same repo.
|
|
12
|
+
* • `doctor` flags legacy plists with actionable remediation.
|
|
13
|
+
*
|
|
14
|
+
* Note: `create` runs `npm install` at the end, which is heavyweight. To
|
|
15
|
+
* keep test runtime reasonable we skip the npm install step by passing
|
|
16
|
+
* an alternate npm shim on PATH that is a no-op. Same trick for git.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { promises as fsp } from "fs";
|
|
22
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "os";
|
|
24
|
+
import { join, resolve, dirname } from "path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { spawnSync, execFileSync } from "node:child_process";
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const MAESTRO_ROOT = resolve(__dirname, "..");
|
|
30
|
+
const MAESTRO_CLI = resolve(__dirname, "maestro.mjs");
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
async function tmpRoot(prefix = "maestro-cli-test") {
|
|
37
|
+
const path = join(
|
|
38
|
+
tmpdir(),
|
|
39
|
+
`${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
40
|
+
);
|
|
41
|
+
await fsp.mkdir(path, { recursive: true });
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function rmRoot(path) {
|
|
46
|
+
try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a PATH that shadows `npm` (and optionally `git`) with no-op shims
|
|
51
|
+
* so `create` doesn't actually try to install dependencies. Returns the new
|
|
52
|
+
* env block plus the shim dir (for cleanup).
|
|
53
|
+
*/
|
|
54
|
+
async function withShim() {
|
|
55
|
+
const dir = await tmpRoot("maestro-shim");
|
|
56
|
+
const npm = join(dir, "npm");
|
|
57
|
+
writeFileSync(npm, "#!/bin/bash\necho '[shim npm]' \"$@\" >&2\nexit 0\n");
|
|
58
|
+
await fsp.chmod(npm, 0o755);
|
|
59
|
+
// Keep real git (we want git init to actually init).
|
|
60
|
+
const env = { ...process.env, PATH: `${dir}:${process.env.PATH || ""}` };
|
|
61
|
+
return { env, dir };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runCli(args, cwd, env) {
|
|
65
|
+
return spawnSync(process.execPath, [MAESTRO_CLI, ...args], {
|
|
66
|
+
cwd: cwd || process.cwd(),
|
|
67
|
+
env: env || process.env,
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// create
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
test("create scaffolds cadence-bus dirs and enqueue/consumer/handlers", async () => {
|
|
77
|
+
const parent = await tmpRoot("maestro-create");
|
|
78
|
+
const { env, dir: shim } = await withShim();
|
|
79
|
+
try {
|
|
80
|
+
const r = runCli(["create", "test-agent"], parent, env);
|
|
81
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
82
|
+
const agent = join(parent, "test-agent");
|
|
83
|
+
|
|
84
|
+
// Cadence bus directory tree
|
|
85
|
+
for (const d of [
|
|
86
|
+
"state/cadence-bus",
|
|
87
|
+
"state/cadence-bus/inbox",
|
|
88
|
+
"state/cadence-bus/claimed",
|
|
89
|
+
"state/cadence-bus/processed",
|
|
90
|
+
"state/cadence-bus/failed",
|
|
91
|
+
"state/cadence-bus/dlq",
|
|
92
|
+
"logs/cadence-bus",
|
|
93
|
+
]) {
|
|
94
|
+
assert.ok(existsSync(join(agent, d)), `missing ${d}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// New framework files copied into the agent repo.
|
|
98
|
+
for (const f of [
|
|
99
|
+
"lib/cadence-bus.mjs",
|
|
100
|
+
"scripts/cadence/enqueue-cadence-tick.mjs",
|
|
101
|
+
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
102
|
+
"scripts/cadence/cadence-status.mjs",
|
|
103
|
+
"scripts/daemon/cadence-consumer.mjs",
|
|
104
|
+
"scripts/daemon/cadence-handlers.mjs",
|
|
105
|
+
]) {
|
|
106
|
+
assert.ok(existsSync(join(agent, f)), `missing ${f}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Agent package.json has the cadence:* scripts.
|
|
110
|
+
const pkg = JSON.parse(readFileSync(join(agent, "package.json"), "utf-8"));
|
|
111
|
+
assert.ok(pkg.scripts["cadence:enqueue"]);
|
|
112
|
+
assert.ok(pkg.scripts["cadence:consume"]);
|
|
113
|
+
assert.ok(pkg.scripts["cadence:status"]);
|
|
114
|
+
} finally {
|
|
115
|
+
await rmRoot(parent);
|
|
116
|
+
await rmRoot(shim);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// upgrade — migration
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
async function makeLegacyAgent() {
|
|
125
|
+
const root = await tmpRoot("maestro-legacy");
|
|
126
|
+
// Minimal identity so `upgrade` recognises this as a Maestro repo.
|
|
127
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
128
|
+
writeFileSync(join(root, "CLAUDE.md"), "# legacy agent\n");
|
|
129
|
+
writeFileSync(join(root, "config/agent.ts"), "export const AGENT = { firstName: 'legacy' };\n");
|
|
130
|
+
writeFileSync(join(root, "package.json"), '{"name":"legacy-agent","version":"1.0.0"}\n');
|
|
131
|
+
// Personal/local agent state that must NOT be touched.
|
|
132
|
+
mkdirSync(join(root, "knowledge/decisions"), { recursive: true });
|
|
133
|
+
mkdirSync(join(root, "memory/interactions"), { recursive: true });
|
|
134
|
+
mkdirSync(join(root, "state/queues"), { recursive: true });
|
|
135
|
+
mkdirSync(join(root, "outputs"), { recursive: true });
|
|
136
|
+
mkdirSync(join(root, "logs"), { recursive: true });
|
|
137
|
+
writeFileSync(join(root, "knowledge/decisions/local-decision.md"), "# do not touch");
|
|
138
|
+
writeFileSync(join(root, "memory/interactions/local-memory.yaml"), "# do not touch");
|
|
139
|
+
writeFileSync(join(root, "state/queues/action-stack.yaml"), "items:\n - id: P1\n title: local item\n");
|
|
140
|
+
writeFileSync(join(root, "outputs/local-output.md"), "# my draft");
|
|
141
|
+
writeFileSync(join(root, "logs/local.log"), "audit data");
|
|
142
|
+
writeFileSync(join(root, ".env"), "ANTHROPIC_API_KEY=sk-x\n");
|
|
143
|
+
|
|
144
|
+
// Legacy plist that calls run-trigger.sh directly.
|
|
145
|
+
mkdirSync(join(root, "scripts/local-triggers/plists"), { recursive: true });
|
|
146
|
+
writeFileSync(
|
|
147
|
+
join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"),
|
|
148
|
+
`<?xml version="1.0"?><plist><dict>
|
|
149
|
+
<key>ProgramArguments</key><array>
|
|
150
|
+
<string>/foo/scripts/local-triggers/run-trigger.sh</string>
|
|
151
|
+
<string>inbox-processor</string>
|
|
152
|
+
</array></dict></plist>`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Init git so smart-merge sees the working tree as clean.
|
|
156
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
157
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
158
|
+
execFileSync(
|
|
159
|
+
"git",
|
|
160
|
+
["-c", "user.email=test@test", "-c", "user.name=test", "commit", "-q", "-m", "init"],
|
|
161
|
+
{ cwd: root },
|
|
162
|
+
);
|
|
163
|
+
return root;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
test("upgrade backs up legacy plists and regenerates with cadence-bus marker", async () => {
|
|
167
|
+
const root = await makeLegacyAgent();
|
|
168
|
+
try {
|
|
169
|
+
const r = runCli(["upgrade"], root);
|
|
170
|
+
assert.equal(r.status, 0, r.stderr);
|
|
171
|
+
// Backup directory exists.
|
|
172
|
+
const backupRoot = join(root, ".maestro/backup/plists");
|
|
173
|
+
assert.ok(existsSync(backupRoot), "backup root must exist");
|
|
174
|
+
const stamps = readdirSync(backupRoot);
|
|
175
|
+
assert.ok(stamps.length >= 1);
|
|
176
|
+
const backedUp = readdirSync(join(backupRoot, stamps[0]));
|
|
177
|
+
assert.ok(backedUp.some((n) => /legacy-inbox-processor\.plist$/.test(n)));
|
|
178
|
+
|
|
179
|
+
// After regeneration, plists carry the cadence-bus marker and none
|
|
180
|
+
// reference run-trigger.sh.
|
|
181
|
+
const plistDir = join(root, "scripts/local-triggers/plists");
|
|
182
|
+
const plists = readdirSync(plistDir).filter((n) => n.endsWith(".plist"));
|
|
183
|
+
assert.ok(plists.length >= 1);
|
|
184
|
+
let legacy = 0;
|
|
185
|
+
let modern = 0;
|
|
186
|
+
for (const name of plists) {
|
|
187
|
+
const body = readFileSync(join(plistDir, name), "utf-8");
|
|
188
|
+
if (body.includes("run-trigger.sh")) legacy++;
|
|
189
|
+
if (body.includes("maestro-plist-arch: cadence-bus")) modern++;
|
|
190
|
+
}
|
|
191
|
+
assert.equal(legacy, 0, "no legacy plists should remain post-upgrade");
|
|
192
|
+
assert.equal(modern, plists.length, "every plist must carry the cadence-bus marker");
|
|
193
|
+
} finally { await rmRoot(root); }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("upgrade preserves agent-specific files untouched", async () => {
|
|
197
|
+
const root = await makeLegacyAgent();
|
|
198
|
+
try {
|
|
199
|
+
const before = {
|
|
200
|
+
claudemd: readFileSync(join(root, "CLAUDE.md"), "utf-8"),
|
|
201
|
+
agentTs: readFileSync(join(root, "config/agent.ts"), "utf-8"),
|
|
202
|
+
decision: readFileSync(join(root, "knowledge/decisions/local-decision.md"), "utf-8"),
|
|
203
|
+
memory: readFileSync(join(root, "memory/interactions/local-memory.yaml"), "utf-8"),
|
|
204
|
+
queue: readFileSync(join(root, "state/queues/action-stack.yaml"), "utf-8"),
|
|
205
|
+
output: readFileSync(join(root, "outputs/local-output.md"), "utf-8"),
|
|
206
|
+
log: readFileSync(join(root, "logs/local.log"), "utf-8"),
|
|
207
|
+
env: readFileSync(join(root, ".env"), "utf-8"),
|
|
208
|
+
};
|
|
209
|
+
const r = runCli(["upgrade"], root);
|
|
210
|
+
assert.equal(r.status, 0, r.stderr);
|
|
211
|
+
for (const [k, v] of Object.entries(before)) {
|
|
212
|
+
const path = ({
|
|
213
|
+
claudemd: "CLAUDE.md",
|
|
214
|
+
agentTs: "config/agent.ts",
|
|
215
|
+
decision: "knowledge/decisions/local-decision.md",
|
|
216
|
+
memory: "memory/interactions/local-memory.yaml",
|
|
217
|
+
queue: "state/queues/action-stack.yaml",
|
|
218
|
+
output: "outputs/local-output.md",
|
|
219
|
+
log: "logs/local.log",
|
|
220
|
+
env: ".env",
|
|
221
|
+
})[k];
|
|
222
|
+
assert.equal(
|
|
223
|
+
readFileSync(join(root, path), "utf-8"),
|
|
224
|
+
v,
|
|
225
|
+
`${path} was modified by upgrade`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} finally { await rmRoot(root); }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("upgrade is idempotent (second run is a clean no-op for plists)", async () => {
|
|
232
|
+
const root = await makeLegacyAgent();
|
|
233
|
+
try {
|
|
234
|
+
const first = runCli(["upgrade"], root);
|
|
235
|
+
assert.equal(first.status, 0);
|
|
236
|
+
// Snapshot the regenerated plist contents.
|
|
237
|
+
const plistDir = join(root, "scripts/local-triggers/plists");
|
|
238
|
+
const before = {};
|
|
239
|
+
for (const name of readdirSync(plistDir)) {
|
|
240
|
+
before[name] = readFileSync(join(plistDir, name), "utf-8");
|
|
241
|
+
}
|
|
242
|
+
const second = runCli(["upgrade"], root);
|
|
243
|
+
assert.equal(second.status, 0);
|
|
244
|
+
// The second run should NOT introduce any new backup directory beyond
|
|
245
|
+
// what the first run created, because there are no legacy plists left.
|
|
246
|
+
const backups = readdirSync(join(root, ".maestro/backup/plists"));
|
|
247
|
+
assert.equal(backups.length, 1, "no new backup directory on idempotent run");
|
|
248
|
+
// Plist contents byte-identical.
|
|
249
|
+
for (const [name, body] of Object.entries(before)) {
|
|
250
|
+
assert.equal(readFileSync(join(plistDir, name), "utf-8"), body, `${name} changed`);
|
|
251
|
+
}
|
|
252
|
+
} finally { await rmRoot(root); }
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("upgrade --dry-run reports legacy without modifying anything", async () => {
|
|
256
|
+
const root = await makeLegacyAgent();
|
|
257
|
+
try {
|
|
258
|
+
// Snapshot a known sentinel file.
|
|
259
|
+
const sentinel = readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8");
|
|
260
|
+
const r = runCli(["upgrade", "--dry-run"], root);
|
|
261
|
+
assert.equal(r.status, 0);
|
|
262
|
+
assert.match(r.stdout, /still call run-trigger\.sh/);
|
|
263
|
+
// Sentinel must not have changed.
|
|
264
|
+
assert.equal(
|
|
265
|
+
readFileSync(join(root, "scripts/local-triggers/plists/ai.adaptic.legacy-inbox-processor.plist"), "utf-8"),
|
|
266
|
+
sentinel,
|
|
267
|
+
);
|
|
268
|
+
// No backup directory created.
|
|
269
|
+
assert.ok(!existsSync(join(root, ".maestro/backup")), "dry-run must not create backups");
|
|
270
|
+
} finally { await rmRoot(root); }
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// doctor
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
test("doctor flags legacy plists with actionable remediation", async () => {
|
|
278
|
+
const root = await makeLegacyAgent();
|
|
279
|
+
try {
|
|
280
|
+
const r = runCli(["doctor"], root);
|
|
281
|
+
// doctor exits 1 when issues are found; that's expected here.
|
|
282
|
+
assert.ok(r.status >= 0);
|
|
283
|
+
const combined = (r.stdout || "") + (r.stderr || "");
|
|
284
|
+
assert.match(combined, /run-trigger\.sh/);
|
|
285
|
+
assert.match(combined, /maestro upgrade/);
|
|
286
|
+
} finally { await rmRoot(root); }
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("doctor recognises new cadence-bus architecture after upgrade", async () => {
|
|
290
|
+
const root = await makeLegacyAgent();
|
|
291
|
+
try {
|
|
292
|
+
runCli(["upgrade"], root);
|
|
293
|
+
const r = runCli(["doctor"], root);
|
|
294
|
+
const combined = (r.stdout || "") + (r.stderr || "");
|
|
295
|
+
assert.match(combined, /cadence-bus architecture/);
|
|
296
|
+
// 'plist(s) still call run-trigger.sh' must NOT appear.
|
|
297
|
+
assert.ok(!combined.includes("plist(s) still call run-trigger.sh"));
|
|
298
|
+
} finally { await rmRoot(root); }
|
|
299
|
+
});
|
|
@@ -43,16 +43,29 @@ The agent has three concurrent execution modes, powered by different subsystems:
|
|
|
43
43
|
│ │ Up to 10 parallel claude --print sessions │ │
|
|
44
44
|
│ └────────────────────────────────────────────────────────────┘ │
|
|
45
45
|
├─────────────────────────────────────────────────────────────────────┤
|
|
46
|
-
│ MODE 2: SCHEDULED — Run Cadence Workflows
|
|
46
|
+
│ MODE 2: SCHEDULED — Run Cadence Workflows via the Cadence Bus │
|
|
47
47
|
│ │
|
|
48
|
-
│ ┌──────────┐
|
|
49
|
-
│ │ launchd │─▶│
|
|
50
|
-
│ │ (schedule)│ │
|
|
51
|
-
│ └──────────┘
|
|
48
|
+
│ ┌──────────┐ ┌────────────────────┐ ┌─────────────────────────┐ │
|
|
49
|
+
│ │ launchd │─▶│ enqueue-cadence- │─▶│ state/cadence-bus/ │ │
|
|
50
|
+
│ │ (schedule)│ │ tick.mjs (~10ms) │ │ inbox/<event>.json │ │
|
|
51
|
+
│ └──────────┘ └────────────────────┘ └────────────┬────────────┘ │
|
|
52
|
+
│ │ │
|
|
53
|
+
│ ┌──────────▼───────────┐ │
|
|
54
|
+
│ │ cadence-consumer.mjs │ │
|
|
55
|
+
│ │ (inside daemon) │ │
|
|
56
|
+
│ │ │ │
|
|
57
|
+
│ │ ┌─────┐ ┌──────────┐ │ │
|
|
58
|
+
│ │ │inline│ │sub-session│ │ │
|
|
59
|
+
│ │ └─────┘ └──────────┘ │ │
|
|
60
|
+
│ └──────────────────────┘ │
|
|
52
61
|
│ │
|
|
53
|
-
│
|
|
54
|
-
│
|
|
55
|
-
│
|
|
62
|
+
│ Per-cadence policy in scripts/daemon/cadence-handlers.mjs: │
|
|
63
|
+
│ inline → housekeeping (heartbeat, recovery) │
|
|
64
|
+
│ guarded → cheap pre-check; escalate only if work pending │
|
|
65
|
+
│ escalate → spawn claude --print with schedules/triggers/*.md │
|
|
66
|
+
│ │
|
|
67
|
+
│ Cadences: inbox-processor, backlog-executor, meeting-prep, │
|
|
68
|
+
│ meeting-action-capture, daily-*, weekly-*, quarterly-* │
|
|
56
69
|
├─────────────────────────────────────────────────────────────────────┤
|
|
57
70
|
│ MODE 3: PROACTIVE — Execute the Backlog │
|
|
58
71
|
│ │
|
|
@@ -47,39 +47,43 @@ Key configuration:
|
|
|
47
47
|
- **KeepAlive.SuccessfulExit**: false -- restarts if the process exits with a non-zero code
|
|
48
48
|
- **ThrottleInterval**: 30 seconds -- prevents rapid restart loops
|
|
49
49
|
|
|
50
|
-
### Per-Workflow Scheduling
|
|
50
|
+
### Per-Workflow Scheduling (Cadence Bus, 1.8+)
|
|
51
51
|
|
|
52
|
-
Each workflow
|
|
52
|
+
As of maestro 1.8, scheduled cadence ticks no longer spawn `claude --print` per tick. Each scheduled workflow has a launchd `.plist` at `scripts/local-triggers/plists/` that calls the lightweight Node cadence enqueue script. That script drops a JSON event onto `state/cadence-bus/inbox/`. The persistent daemon (`scripts/daemon/maestro-daemon.mjs`) hosts the consumer that drains the bus and decides whether to handle each tick inline or escalate to a managed sub-session.
|
|
53
|
+
|
|
54
|
+
Example trigger plist (generated by `scripts/local-triggers/generate-plists.sh`):
|
|
53
55
|
|
|
54
56
|
```xml
|
|
55
57
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
58
|
+
<!-- maestro-plist-arch: cadence-bus v1 -->
|
|
56
59
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
57
60
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
58
61
|
<plist version="1.0">
|
|
59
62
|
<dict>
|
|
60
63
|
<key>Label</key>
|
|
61
|
-
<string>
|
|
64
|
+
<string>ai.adaptic.<agent>-inbox-processor</string>
|
|
62
65
|
<key>ProgramArguments</key>
|
|
63
66
|
<array>
|
|
64
|
-
<string>/
|
|
65
|
-
<string
|
|
66
|
-
<string
|
|
67
|
+
<string>/Users/<agent>/<agent>-ai/scripts/cadence/launchd-cadence-wrapper.sh</string>
|
|
68
|
+
<string>/Users/<agent>/<agent>-ai/scripts/cadence/enqueue-cadence-tick.mjs</string>
|
|
69
|
+
<string>inbox-processor</string>
|
|
70
|
+
<string>--source=launchd</string>
|
|
71
|
+
<string>--quiet</string>
|
|
67
72
|
</array>
|
|
68
|
-
<key>
|
|
69
|
-
<
|
|
70
|
-
<key>Hour</key>
|
|
71
|
-
<integer>6</integer>
|
|
72
|
-
<key>Minute</key>
|
|
73
|
-
<integer>0</integer>
|
|
74
|
-
</dict>
|
|
73
|
+
<key>StartInterval</key>
|
|
74
|
+
<integer>300</integer>
|
|
75
75
|
<key>StandardOutPath</key>
|
|
76
|
-
<string>/Users
|
|
76
|
+
<string>/Users/<agent>/<agent>-ai/logs/daemon/launchd-stdout.log</string>
|
|
77
77
|
<key>StandardErrorPath</key>
|
|
78
|
-
<string>/Users
|
|
78
|
+
<string>/Users/<agent>/<agent>-ai/logs/daemon/launchd-stderr.log</string>
|
|
79
79
|
</dict>
|
|
80
80
|
</plist>
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
The bus consumer's per-tick decisions are logged to `logs/cadence-bus/<date>.jsonl`. Bus depth and consumer heartbeat are available via `npm run cadence:status`. The consumer writes a heartbeat to `state/cadence-bus/health.json` every ~15 seconds; doctor flags it as stale if it's older than 5 minutes.
|
|
84
|
+
|
|
85
|
+
**Legacy plists (run-trigger.sh):** Plists that still call `scripts/local-triggers/run-trigger.sh` directly are detected by `maestro doctor` and auto-migrated by `maestro upgrade` (backed up under `.maestro/backup/plists/<utc-timestamp>/`). `run-trigger.sh` itself is preserved for manual one-shot operator use only.
|
|
86
|
+
|
|
83
87
|
### Managing Jobs
|
|
84
88
|
|
|
85
89
|
```bash
|
|
@@ -162,6 +162,48 @@ launchctl load ~/Library/LaunchAgents/com.adaptic.maestro.plist
|
|
|
162
162
|
launchctl list | grep com.adaptic.maestro
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
### Cadence Bus Recovery (1.8+)
|
|
166
|
+
|
|
167
|
+
The cadence bus at `state/cadence-bus/` is the work queue for scheduled cadence ticks. The daemon's consumer drains it; if the daemon crashes mid-tick, the in-flight event remains in `state/cadence-bus/claimed/` until recovered.
|
|
168
|
+
|
|
169
|
+
**Symptoms of a stuck bus**:
|
|
170
|
+
- `npm run cadence:status` shows `claimed > 0` and `heartbeat_age_ms > 5 minutes`.
|
|
171
|
+
- `logs/cadence-bus/<date>.jsonl` has `stage:claimed` events with no matching `stage:processed` or `stage:dlq`.
|
|
172
|
+
- Bus inbox depth grows over time (`state/cadence-bus/inbox/` has many events).
|
|
173
|
+
|
|
174
|
+
**Manual recovery**:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# 1. Inspect bus state
|
|
178
|
+
npm run cadence:status
|
|
179
|
+
|
|
180
|
+
# 2. Recover stale claims (returns claims older than 30 min to inbox/, or
|
|
181
|
+
# moves them to dlq if past retry budget). The consumer also runs this
|
|
182
|
+
# automatically every 5 minutes — manual run is for impatient operators.
|
|
183
|
+
node -e "import('./lib/cadence-bus.mjs').then(m => console.log(m.recoverStaleClaims(process.cwd())))"
|
|
184
|
+
|
|
185
|
+
# 3. If the bus is genuinely stuck, restart the daemon
|
|
186
|
+
launchctl unload ~/Library/LaunchAgents/ai.adaptic.<agent>-daemon.plist
|
|
187
|
+
launchctl load ~/Library/LaunchAgents/ai.adaptic.<agent>-daemon.plist
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Resetting the bus** (last resort — loses pending ticks, but they re-arrive at next cadence interval):
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Move all in-flight events to a dated archive, then wipe the bus
|
|
194
|
+
ts=$(date -u +%Y-%m-%dT%H-%M-%SZ)
|
|
195
|
+
mkdir -p .maestro/archive/cadence-bus/$ts
|
|
196
|
+
mv state/cadence-bus/inbox state/cadence-bus/claimed state/cadence-bus/dlq .maestro/archive/cadence-bus/$ts/
|
|
197
|
+
mkdir -p state/cadence-bus/{inbox,claimed,processed,failed,dlq}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Dead-letter queue** (`state/cadence-bus/dlq/`): events that exceeded their retry budget (default 5) land here. Each file is a full event payload with `last_error` and `failed_at`. Review periodically; re-enqueue manually if appropriate:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
ls state/cadence-bus/dlq/
|
|
204
|
+
node scripts/cadence/enqueue-cadence-tick.mjs <cadence> --source=manual --metadata=note=dlq-replay
|
|
205
|
+
```
|
|
206
|
+
|
|
165
207
|
### Desktop App Crashes (Slack, Safari, WhatsApp)
|
|
166
208
|
|
|
167
209
|
**Symptoms**: Follow-up messages not sent, comms triage returning empty results, desktop control errors in logs.
|