@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
|
@@ -6,25 +6,68 @@
|
|
|
6
6
|
# Reads config/agent.ts to extract the agent's first name and generates
|
|
7
7
|
# properly configured plist files for all scheduled triggers and the daemon.
|
|
8
8
|
#
|
|
9
|
+
# Architecture marker:
|
|
10
|
+
# MAESTRO_PLIST_ARCH=cadence-bus
|
|
11
|
+
#
|
|
12
|
+
# This generator emits the *cadence-bus* plist architecture (introduced in
|
|
13
|
+
# maestro 1.8). Scheduled cadence ticks invoke a lightweight Node enqueue
|
|
14
|
+
# script (scripts/cadence/enqueue-cadence-tick.mjs) that drops an event onto
|
|
15
|
+
# state/cadence-bus/. The persistent maestro-daemon.mjs consumes the bus and
|
|
16
|
+
# decides — per cadence — whether to handle the tick inline or escalate to a
|
|
17
|
+
# managed sub-session. Launchd NEVER spawns a fresh Claude Code session for
|
|
18
|
+
# routine cadence ticks anymore.
|
|
19
|
+
#
|
|
20
|
+
# Doctor verifies the `MAESTRO_PLIST_ARCH=cadence-bus` marker is present in
|
|
21
|
+
# installed plists, and flags any plist that still runs run-trigger.sh
|
|
22
|
+
# directly as the legacy spawn-per-tick pattern.
|
|
23
|
+
#
|
|
9
24
|
# Usage:
|
|
10
25
|
# ./scripts/local-triggers/generate-plists.sh
|
|
11
26
|
#
|
|
12
|
-
# Called by: init-agent.sh (Step 5)
|
|
27
|
+
# Called by: init-agent.sh (Step 5) and by `maestro upgrade` migration.
|
|
13
28
|
# =============================================================================
|
|
14
29
|
|
|
15
30
|
set -e
|
|
16
31
|
|
|
32
|
+
# Plist architecture marker — embedded as a comment in every generated plist.
|
|
33
|
+
MAESTRO_PLIST_ARCH="cadence-bus"
|
|
34
|
+
MAESTRO_PLIST_ARCH_VERSION="1"
|
|
35
|
+
|
|
17
36
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
37
|
AGENT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
19
38
|
PLIST_DIR="$SCRIPT_DIR/plists"
|
|
20
|
-
AGENT_CONFIG="$AGENT_DIR/config/agent.ts"
|
|
21
39
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
# Resolve the agent's first name. Order of preference:
|
|
41
|
+
# 1. config/agent.json — single source of truth (SOT) introduced when
|
|
42
|
+
# maestro split identity out of TypeScript into JSON. Use jq if
|
|
43
|
+
# available, otherwise a small awk fallback.
|
|
44
|
+
# 2. config/agent.ts — legacy path; only works if firstName is
|
|
45
|
+
# defined inline (string literal), not as a type-only declaration
|
|
46
|
+
# like `firstName: string;`.
|
|
47
|
+
# 3. Directory name — last-resort fallback (e.g. ~/ravi-ai → ravi).
|
|
48
|
+
AGENT_JSON="$AGENT_DIR/config/agent.json"
|
|
49
|
+
AGENT_TS="$AGENT_DIR/config/agent.ts"
|
|
50
|
+
AGENT_FIRST=""
|
|
51
|
+
|
|
52
|
+
if [ -z "$AGENT_FIRST" ] && [ -f "$AGENT_JSON" ]; then
|
|
53
|
+
if command -v jq >/dev/null 2>&1; then
|
|
54
|
+
AGENT_FIRST=$(jq -r '.firstName // empty' "$AGENT_JSON" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
55
|
+
else
|
|
56
|
+
# Awk fallback — handle "firstName": "value" with optional whitespace
|
|
57
|
+
AGENT_FIRST=$(awk -F'"' '/"firstName"[[:space:]]*:/ { print tolower($4); exit }' "$AGENT_JSON")
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [ -z "$AGENT_FIRST" ] && [ -f "$AGENT_TS" ]; then
|
|
62
|
+
# Only accept a quoted inline value — `firstName: string;` (type
|
|
63
|
+
# declaration) must not match. The grep requires a quote on the line.
|
|
64
|
+
AGENT_FIRST=$(grep "firstName:[[:space:]]*['\"]" "$AGENT_TS" \
|
|
65
|
+
| head -1 \
|
|
66
|
+
| sed "s/.*firstName:[[:space:]]*['\"]//; s/['\"].*//" \
|
|
67
|
+
| tr '[:upper:]' '[:lower:]')
|
|
25
68
|
fi
|
|
26
69
|
|
|
27
|
-
# Fall back to directory name if config not set or UNCONFIGURED
|
|
70
|
+
# Fall back to directory name if config not set or UNCONFIGURED.
|
|
28
71
|
if [ -z "$AGENT_FIRST" ] || [ "$AGENT_FIRST" = "unconfigured" ]; then
|
|
29
72
|
AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
|
|
30
73
|
fi
|
|
@@ -57,6 +100,8 @@ generate_plist() {
|
|
|
57
100
|
|
|
58
101
|
cat > "$FILE" << PLIST_HEADER
|
|
59
102
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
103
|
+
<!-- maestro-plist-arch: $MAESTRO_PLIST_ARCH v$MAESTRO_PLIST_ARCH_VERSION -->
|
|
104
|
+
<!-- generated-by: scripts/local-triggers/generate-plists.sh -->
|
|
60
105
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
61
106
|
<plist version="1.0">
|
|
62
107
|
<dict>
|
|
@@ -161,16 +206,16 @@ PLIST_FOOTER
|
|
|
161
206
|
echo " Generated: $LABEL"
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
# ── Helper: trigger plist (
|
|
209
|
+
# ── Helper: trigger plist (enqueues a cadence tick) ──────────────────────────
|
|
165
210
|
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
211
|
+
# Cadence-bus architecture: launchd invokes the lightweight Node enqueue
|
|
212
|
+
# script (scripts/cadence/enqueue-cadence-tick.mjs) which drops a JSON event
|
|
213
|
+
# onto state/cadence-bus/inbox/. The persistent maestro-daemon.mjs consumes
|
|
214
|
+
# the bus and either handles the tick inline or escalates to a managed
|
|
215
|
+
# sub-session.
|
|
169
216
|
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
# itself. Passing the full path causes a doubled path bug:
|
|
173
|
-
# /agent/schedules/triggers//agent/schedules/triggers/meeting-prep.md.md
|
|
217
|
+
# This deliberately does NOT spawn Claude Code per tick anymore. The legacy
|
|
218
|
+
# run-trigger.sh is preserved for manual one-shot invocations only.
|
|
174
219
|
|
|
175
220
|
generate_trigger_plist() {
|
|
176
221
|
local TRIGGER_NAME="$1"
|
|
@@ -178,11 +223,11 @@ generate_trigger_plist() {
|
|
|
178
223
|
local INTERVAL="$3"
|
|
179
224
|
|
|
180
225
|
local LABEL="ai.adaptic.${AGENT_FIRST}-${TRIGGER_NAME}"
|
|
181
|
-
local
|
|
182
|
-
local WRAPPER="$AGENT_DIR/scripts/
|
|
226
|
+
local ENQUEUE="$AGENT_DIR/scripts/cadence/enqueue-cadence-tick.mjs"
|
|
227
|
+
local WRAPPER="$AGENT_DIR/scripts/cadence/launchd-cadence-wrapper.sh"
|
|
183
228
|
|
|
184
229
|
generate_plist "$LABEL" \
|
|
185
|
-
"$WRAPPER|$
|
|
230
|
+
"$WRAPPER|$ENQUEUE|$TRIGGER_NAME|--source=launchd|--quiet" \
|
|
186
231
|
"$SCHEDULE" \
|
|
187
232
|
"$INTERVAL" \
|
|
188
233
|
""
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generate-plists.test.mjs — Coverage for scripts/local-triggers/generate-plists.sh
|
|
3
|
+
*
|
|
4
|
+
* Builds a fixture agent repo (minimal config/agent.ts + script tree),
|
|
5
|
+
* runs the generator, and asserts every emitted plist matches the
|
|
6
|
+
* cadence-bus architecture marker and never references the legacy
|
|
7
|
+
* run-trigger.sh spawn-per-tick pattern.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { promises as fsp } from "fs";
|
|
13
|
+
import { readFileSync, readdirSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { join, resolve, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const MAESTRO_ROOT = resolve(__dirname, "..", "..");
|
|
21
|
+
const GENERATOR = resolve(__dirname, "generate-plists.sh");
|
|
22
|
+
|
|
23
|
+
async function makeAgent(firstName) {
|
|
24
|
+
const root = join(
|
|
25
|
+
tmpdir(),
|
|
26
|
+
`plist-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
27
|
+
);
|
|
28
|
+
await fsp.mkdir(root, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Minimal agent identity.
|
|
31
|
+
await fsp.mkdir(join(root, "config"), { recursive: true });
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(root, "config/agent.ts"),
|
|
34
|
+
`export const AGENT = { firstName: '${firstName}', lastName: 'Tester' };\n`
|
|
35
|
+
);
|
|
36
|
+
writeFileSync(join(root, "package.json"), '{"name":"t"}');
|
|
37
|
+
|
|
38
|
+
// Copy the scripts the generator references.
|
|
39
|
+
await fsp.mkdir(join(root, "scripts/local-triggers"), { recursive: true });
|
|
40
|
+
await fsp.mkdir(join(root, "scripts/daemon"), { recursive: true });
|
|
41
|
+
await fsp.mkdir(join(root, "scripts/cadence"), { recursive: true });
|
|
42
|
+
copyFileSync(GENERATOR, join(root, "scripts/local-triggers/generate-plists.sh"));
|
|
43
|
+
copyFileSync(
|
|
44
|
+
join(MAESTRO_ROOT, "scripts/local-triggers/run-trigger.sh"),
|
|
45
|
+
join(root, "scripts/local-triggers/run-trigger.sh"),
|
|
46
|
+
);
|
|
47
|
+
copyFileSync(
|
|
48
|
+
join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper.sh"),
|
|
49
|
+
join(root, "scripts/daemon/launchd-wrapper.sh"),
|
|
50
|
+
);
|
|
51
|
+
copyFileSync(
|
|
52
|
+
join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper-generic.sh"),
|
|
53
|
+
join(root, "scripts/daemon/launchd-wrapper-generic.sh"),
|
|
54
|
+
);
|
|
55
|
+
copyFileSync(
|
|
56
|
+
join(MAESTRO_ROOT, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
57
|
+
join(root, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
58
|
+
);
|
|
59
|
+
copyFileSync(
|
|
60
|
+
join(MAESTRO_ROOT, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
61
|
+
join(root, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Make scripts executable.
|
|
65
|
+
for (const p of [
|
|
66
|
+
"scripts/local-triggers/generate-plists.sh",
|
|
67
|
+
"scripts/local-triggers/run-trigger.sh",
|
|
68
|
+
"scripts/daemon/launchd-wrapper.sh",
|
|
69
|
+
"scripts/daemon/launchd-wrapper-generic.sh",
|
|
70
|
+
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
71
|
+
]) {
|
|
72
|
+
await fsp.chmod(join(root, p), 0o755);
|
|
73
|
+
}
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function rmRoot(path) {
|
|
78
|
+
try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runGenerator(agentRoot) {
|
|
82
|
+
return spawnSync("/bin/bash", [join(agentRoot, "scripts/local-triggers/generate-plists.sh")], {
|
|
83
|
+
cwd: agentRoot,
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
env: { ...process.env },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listPlists(agentRoot) {
|
|
90
|
+
const dir = join(agentRoot, "scripts/local-triggers/plists");
|
|
91
|
+
return readdirSync(dir).filter((n) => n.endsWith(".plist"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Tests
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
test("generator emits 13 plists with the agent's first name", async () => {
|
|
99
|
+
const root = await makeAgent("alice");
|
|
100
|
+
try {
|
|
101
|
+
const r = runGenerator(root);
|
|
102
|
+
assert.equal(r.status, 0, r.stderr);
|
|
103
|
+
const plists = listPlists(root);
|
|
104
|
+
assert.equal(plists.length, 13);
|
|
105
|
+
for (const p of plists) {
|
|
106
|
+
assert.match(p, /^ai\.adaptic\.alice-/);
|
|
107
|
+
}
|
|
108
|
+
} finally { await rmRoot(root); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("every trigger plist carries the cadence-bus architecture marker", async () => {
|
|
112
|
+
const root = await makeAgent("bob");
|
|
113
|
+
try {
|
|
114
|
+
runGenerator(root);
|
|
115
|
+
const dir = join(root, "scripts/local-triggers/plists");
|
|
116
|
+
for (const name of listPlists(root)) {
|
|
117
|
+
const body = readFileSync(join(dir, name), "utf-8");
|
|
118
|
+
assert.match(body, /maestro-plist-arch: cadence-bus/, `${name} missing arch marker`);
|
|
119
|
+
}
|
|
120
|
+
} finally { await rmRoot(root); }
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("NO trigger plist invokes run-trigger.sh", async () => {
|
|
124
|
+
const root = await makeAgent("carol");
|
|
125
|
+
try {
|
|
126
|
+
runGenerator(root);
|
|
127
|
+
const dir = join(root, "scripts/local-triggers/plists");
|
|
128
|
+
for (const name of listPlists(root)) {
|
|
129
|
+
const body = readFileSync(join(dir, name), "utf-8");
|
|
130
|
+
// Exception: the daemon/poll-relay plists don't run triggers at all.
|
|
131
|
+
// The trigger plists must invoke enqueue-cadence-tick.mjs.
|
|
132
|
+
if (name.endsWith("-daemon.plist") || name.endsWith("-poll-relay.plist")) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
assert.ok(!body.includes("run-trigger.sh"),
|
|
136
|
+
`${name} still references run-trigger.sh:\n${body}`);
|
|
137
|
+
assert.ok(body.includes("enqueue-cadence-tick.mjs"),
|
|
138
|
+
`${name} should invoke enqueue script`);
|
|
139
|
+
}
|
|
140
|
+
} finally { await rmRoot(root); }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("trigger plists pass --source=launchd to the enqueue script", async () => {
|
|
144
|
+
const root = await makeAgent("dana");
|
|
145
|
+
try {
|
|
146
|
+
runGenerator(root);
|
|
147
|
+
const dir = join(root, "scripts/local-triggers/plists");
|
|
148
|
+
const triggerPlist = join(dir, "ai.adaptic.dana-inbox-processor.plist");
|
|
149
|
+
const body = readFileSync(triggerPlist, "utf-8");
|
|
150
|
+
assert.match(body, /<string>--source=launchd<\/string>/);
|
|
151
|
+
assert.match(body, /<string>inbox-processor<\/string>/);
|
|
152
|
+
// StartInterval 300 (every 5 min).
|
|
153
|
+
assert.match(body, /<integer>300<\/integer>/);
|
|
154
|
+
} finally { await rmRoot(root); }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("daemon plist remains a KeepAlive job (not a cadence enqueue)", async () => {
|
|
158
|
+
const root = await makeAgent("erin");
|
|
159
|
+
try {
|
|
160
|
+
runGenerator(root);
|
|
161
|
+
const path = join(root, "scripts/local-triggers/plists/ai.adaptic.erin-daemon.plist");
|
|
162
|
+
const body = readFileSync(path, "utf-8");
|
|
163
|
+
assert.match(body, /<key>KeepAlive<\/key>\s*<true\/>/);
|
|
164
|
+
assert.match(body, /launchd-wrapper\.sh/);
|
|
165
|
+
assert.ok(!body.includes("enqueue-cadence-tick.mjs"),
|
|
166
|
+
"daemon plist should not enqueue ticks — it IS the consumer");
|
|
167
|
+
} finally { await rmRoot(root); }
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("generator reads firstName from agent.json when agent.ts is type-only (SOT layout)", async () => {
|
|
171
|
+
// Reproduce the SOT layout where agent.ts only declares interfaces
|
|
172
|
+
// (containing `firstName: string;` as a type, not a value) and the
|
|
173
|
+
// actual identity lives in agent.json. The legacy resolver matched the
|
|
174
|
+
// interface line and produced labels like
|
|
175
|
+
// `ai.adaptic. firstname: string;-…` — guard against regression.
|
|
176
|
+
const root = join(
|
|
177
|
+
tmpdir(),
|
|
178
|
+
`plist-sot-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
179
|
+
);
|
|
180
|
+
await fsp.mkdir(root, { recursive: true });
|
|
181
|
+
// Manually wire the same fixture-builder used by other tests, except
|
|
182
|
+
// overwrite agent.ts to look like a type-only TypeScript file and add
|
|
183
|
+
// a real agent.json.
|
|
184
|
+
await fsp.mkdir(join(root, "config"), { recursive: true });
|
|
185
|
+
writeFileSync(
|
|
186
|
+
join(root, "config/agent.ts"),
|
|
187
|
+
`// Type declarations only — value lives in agent.json
|
|
188
|
+
export interface PrincipalConfig { firstName: string; }
|
|
189
|
+
export interface AgentConfig { firstName: string; }
|
|
190
|
+
`,
|
|
191
|
+
);
|
|
192
|
+
writeFileSync(
|
|
193
|
+
join(root, "config/agent.json"),
|
|
194
|
+
JSON.stringify({ firstName: "Sigrid", lastName: "Test" }) + "\n",
|
|
195
|
+
);
|
|
196
|
+
writeFileSync(join(root, "package.json"), '{"name":"t"}');
|
|
197
|
+
|
|
198
|
+
await fsp.mkdir(join(root, "scripts/local-triggers"), { recursive: true });
|
|
199
|
+
await fsp.mkdir(join(root, "scripts/daemon"), { recursive: true });
|
|
200
|
+
await fsp.mkdir(join(root, "scripts/cadence"), { recursive: true });
|
|
201
|
+
// Same set of helper scripts as makeAgent().
|
|
202
|
+
const { copyFileSync } = await import("node:fs");
|
|
203
|
+
copyFileSync(GENERATOR, join(root, "scripts/local-triggers/generate-plists.sh"));
|
|
204
|
+
copyFileSync(
|
|
205
|
+
join(MAESTRO_ROOT, "scripts/local-triggers/run-trigger.sh"),
|
|
206
|
+
join(root, "scripts/local-triggers/run-trigger.sh"),
|
|
207
|
+
);
|
|
208
|
+
copyFileSync(
|
|
209
|
+
join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper.sh"),
|
|
210
|
+
join(root, "scripts/daemon/launchd-wrapper.sh"),
|
|
211
|
+
);
|
|
212
|
+
copyFileSync(
|
|
213
|
+
join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper-generic.sh"),
|
|
214
|
+
join(root, "scripts/daemon/launchd-wrapper-generic.sh"),
|
|
215
|
+
);
|
|
216
|
+
copyFileSync(
|
|
217
|
+
join(MAESTRO_ROOT, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
218
|
+
join(root, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
219
|
+
);
|
|
220
|
+
copyFileSync(
|
|
221
|
+
join(MAESTRO_ROOT, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
222
|
+
join(root, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
223
|
+
);
|
|
224
|
+
await fsp.chmod(join(root, "scripts/local-triggers/generate-plists.sh"), 0o755);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const r = runGenerator(root);
|
|
228
|
+
assert.equal(r.status, 0, r.stderr);
|
|
229
|
+
const plists = listPlists(root);
|
|
230
|
+
assert.ok(plists.length >= 1);
|
|
231
|
+
for (const p of plists) {
|
|
232
|
+
assert.match(p, /^ai\.adaptic\.sigrid-/, `expected "sigrid" label, got: ${p}`);
|
|
233
|
+
assert.ok(!p.includes("string"),
|
|
234
|
+
`label leaked the TypeScript type: ${p}`);
|
|
235
|
+
}
|
|
236
|
+
} finally { await rmRoot(root); }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("regeneration clears stale plists", async () => {
|
|
240
|
+
const root = await makeAgent("frank");
|
|
241
|
+
try {
|
|
242
|
+
// Plant a stale plist for a long-gone agent.
|
|
243
|
+
const dir = join(root, "scripts/local-triggers/plists");
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
writeFileSync(join(dir, "ai.adaptic.OLD-AGENT-stale.plist"), "<plist/>");
|
|
246
|
+
runGenerator(root);
|
|
247
|
+
const remaining = listPlists(root);
|
|
248
|
+
assert.ok(!remaining.includes("ai.adaptic.OLD-AGENT-stale.plist"),
|
|
249
|
+
"generator must clear stale plists");
|
|
250
|
+
for (const p of remaining) {
|
|
251
|
+
assert.match(p, /^ai\.adaptic\.frank-/);
|
|
252
|
+
}
|
|
253
|
+
} finally { await rmRoot(root); }
|
|
254
|
+
});
|
|
File without changes
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Run a local scheduled trigger via Claude Code CLI
|
|
2
|
+
# Run a local scheduled trigger via Claude Code CLI (legacy / manual path).
|
|
3
|
+
#
|
|
3
4
|
# Usage: run-trigger.sh <trigger-name>
|
|
4
5
|
#
|
|
5
|
-
# Reads the prompt from schedules/triggers/<trigger-name>.md
|
|
6
|
-
#
|
|
6
|
+
# Reads the prompt from schedules/triggers/<trigger-name>.md and runs it as
|
|
7
|
+
# a non-interactive `claude --print` session.
|
|
8
|
+
#
|
|
9
|
+
# ⚠ LEGACY PATH — DO NOT USE FROM LAUNCHD ⚠
|
|
10
|
+
#
|
|
11
|
+
# As of the cadence-bus architecture (maestro 1.8), scheduled cadence ticks
|
|
12
|
+
# go through scripts/cadence/enqueue-cadence-tick.mjs instead of running
|
|
13
|
+
# claude directly. The persistent maestro-daemon.mjs consumes events from
|
|
14
|
+
# state/cadence-bus/ and decides whether to handle them inline or escalate
|
|
15
|
+
# to a managed sub-session.
|
|
16
|
+
#
|
|
17
|
+
# This script remains available for two specific purposes:
|
|
18
|
+
# 1. Manual one-shot invocation by an operator who wants to run a single
|
|
19
|
+
# cadence prompt immediately, without waiting for the daemon.
|
|
20
|
+
# 2. Compatibility with very old launchd plists during migration. Doctor
|
|
21
|
+
# / upgrade will flag plists that still call this script and rewrite
|
|
22
|
+
# them through the cadence bus.
|
|
23
|
+
#
|
|
24
|
+
# Prefer instead:
|
|
25
|
+
# node scripts/cadence/enqueue-cadence-tick.mjs <trigger-name> --source=manual
|
|
7
26
|
|
|
8
27
|
set -e
|
|
9
28
|
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-backlog-executor</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>backlog-executor</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartInterval</key>
|
|
13
|
-
<integer>600</integer>
|
|
14
|
-
<key>WorkingDirectory</key>
|
|
15
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
16
|
-
<key>StandardOutPath</key>
|
|
17
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-backlog-executor-stdout.log</string>
|
|
18
|
-
<key>StandardErrorPath</key>
|
|
19
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-backlog-executor-stderr.log</string>
|
|
20
|
-
</dict>
|
|
21
|
-
</plist>
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-daemon</string>
|
|
7
|
-
|
|
8
|
-
<key>ProgramArguments</key>
|
|
9
|
-
<array>
|
|
10
|
-
<string>/opt/homebrew/bin/node</string>
|
|
11
|
-
<string>/Users/sophie/sophie-ai/scripts/daemon/sophie-daemon.mjs</string>
|
|
12
|
-
</array>
|
|
13
|
-
|
|
14
|
-
<key>WorkingDirectory</key>
|
|
15
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
16
|
-
|
|
17
|
-
<key>KeepAlive</key>
|
|
18
|
-
<true/>
|
|
19
|
-
|
|
20
|
-
<key>RunAtLoad</key>
|
|
21
|
-
<true/>
|
|
22
|
-
|
|
23
|
-
<key>ThrottleInterval</key>
|
|
24
|
-
<integer>5</integer>
|
|
25
|
-
|
|
26
|
-
<key>StandardOutPath</key>
|
|
27
|
-
<string>/Users/sophie/sophie-ai/logs/daemon/launchd-stdout.log</string>
|
|
28
|
-
|
|
29
|
-
<key>StandardErrorPath</key>
|
|
30
|
-
<string>/Users/sophie/sophie-ai/logs/daemon/launchd-stderr.log</string>
|
|
31
|
-
</dict>
|
|
32
|
-
</plist>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-inbox-processor</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>inbox-processor</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartInterval</key>
|
|
13
|
-
<integer>300</integer>
|
|
14
|
-
<key>WorkingDirectory</key>
|
|
15
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
16
|
-
<key>StandardOutPath</key>
|
|
17
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-inbox-processor-stdout.log</string>
|
|
18
|
-
<key>StandardErrorPath</key>
|
|
19
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-inbox-processor-stderr.log</string>
|
|
20
|
-
</dict>
|
|
21
|
-
</plist>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-meeting-action-capture</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>meeting-action-capture</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartInterval</key>
|
|
13
|
-
<integer>1800</integer>
|
|
14
|
-
<key>WorkingDirectory</key>
|
|
15
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
16
|
-
<key>StandardOutPath</key>
|
|
17
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-meeting-action-capture-stdout.log</string>
|
|
18
|
-
<key>StandardErrorPath</key>
|
|
19
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-meeting-action-capture-stderr.log</string>
|
|
20
|
-
</dict>
|
|
21
|
-
</plist>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-meeting-prep</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>meeting-prep</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartInterval</key>
|
|
13
|
-
<integer>900</integer>
|
|
14
|
-
<key>WorkingDirectory</key>
|
|
15
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
16
|
-
<key>StandardOutPath</key>
|
|
17
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-meeting-prep-stdout.log</string>
|
|
18
|
-
<key>StandardErrorPath</key>
|
|
19
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-meeting-prep-stderr.log</string>
|
|
20
|
-
</dict>
|
|
21
|
-
</plist>
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-midday-sweep</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>daily-midday-sweep</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartCalendarInterval</key>
|
|
13
|
-
<dict>
|
|
14
|
-
<key>Hour</key>
|
|
15
|
-
<integer>12</integer>
|
|
16
|
-
<key>Minute</key>
|
|
17
|
-
<integer>0</integer>
|
|
18
|
-
</dict>
|
|
19
|
-
<key>WorkingDirectory</key>
|
|
20
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
21
|
-
<key>StandardOutPath</key>
|
|
22
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-midday-sweep-stdout.log</string>
|
|
23
|
-
<key>StandardErrorPath</key>
|
|
24
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-midday-sweep-stderr.log</string>
|
|
25
|
-
</dict>
|
|
26
|
-
</plist>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-quarterly-self-assessment</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>quarterly-self-assessment</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartCalendarInterval</key>
|
|
13
|
-
<array>
|
|
14
|
-
<dict>
|
|
15
|
-
<key>Month</key>
|
|
16
|
-
<integer>1</integer>
|
|
17
|
-
<key>Weekday</key>
|
|
18
|
-
<integer>1</integer>
|
|
19
|
-
<key>Hour</key>
|
|
20
|
-
<integer>9</integer>
|
|
21
|
-
<key>Minute</key>
|
|
22
|
-
<integer>30</integer>
|
|
23
|
-
</dict>
|
|
24
|
-
<dict>
|
|
25
|
-
<key>Month</key>
|
|
26
|
-
<integer>4</integer>
|
|
27
|
-
<key>Weekday</key>
|
|
28
|
-
<integer>1</integer>
|
|
29
|
-
<key>Hour</key>
|
|
30
|
-
<integer>9</integer>
|
|
31
|
-
<key>Minute</key>
|
|
32
|
-
<integer>30</integer>
|
|
33
|
-
</dict>
|
|
34
|
-
<dict>
|
|
35
|
-
<key>Month</key>
|
|
36
|
-
<integer>7</integer>
|
|
37
|
-
<key>Weekday</key>
|
|
38
|
-
<integer>1</integer>
|
|
39
|
-
<key>Hour</key>
|
|
40
|
-
<integer>9</integer>
|
|
41
|
-
<key>Minute</key>
|
|
42
|
-
<integer>30</integer>
|
|
43
|
-
</dict>
|
|
44
|
-
<dict>
|
|
45
|
-
<key>Month</key>
|
|
46
|
-
<integer>10</integer>
|
|
47
|
-
<key>Weekday</key>
|
|
48
|
-
<integer>1</integer>
|
|
49
|
-
<key>Hour</key>
|
|
50
|
-
<integer>9</integer>
|
|
51
|
-
<key>Minute</key>
|
|
52
|
-
<integer>30</integer>
|
|
53
|
-
</dict>
|
|
54
|
-
</array>
|
|
55
|
-
<key>WorkingDirectory</key>
|
|
56
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
57
|
-
<key>StandardOutPath</key>
|
|
58
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-quarterly-self-assessment-stdout.log</string>
|
|
59
|
-
<key>StandardErrorPath</key>
|
|
60
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-quarterly-self-assessment-stderr.log</string>
|
|
61
|
-
</dict>
|
|
62
|
-
</plist>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>Label</key>
|
|
6
|
-
<string>ai.adaptic.sophie-weekly-engineering-health</string>
|
|
7
|
-
<key>ProgramArguments</key>
|
|
8
|
-
<array>
|
|
9
|
-
<string>/Users/sophie/sophie-ai/scripts/local-triggers/run-trigger.sh</string>
|
|
10
|
-
<string>weekly-engineering-health</string>
|
|
11
|
-
</array>
|
|
12
|
-
<key>StartCalendarInterval</key>
|
|
13
|
-
<dict>
|
|
14
|
-
<key>Weekday</key>
|
|
15
|
-
<integer>3</integer>
|
|
16
|
-
<key>Hour</key>
|
|
17
|
-
<integer>10</integer>
|
|
18
|
-
<key>Minute</key>
|
|
19
|
-
<integer>0</integer>
|
|
20
|
-
</dict>
|
|
21
|
-
<key>WorkingDirectory</key>
|
|
22
|
-
<string>/Users/sophie/sophie-ai</string>
|
|
23
|
-
<key>StandardOutPath</key>
|
|
24
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-engineering-health-stdout.log</string>
|
|
25
|
-
<key>StandardErrorPath</key>
|
|
26
|
-
<string>/Users/sophie/sophie-ai/logs/workflows/launchd-engineering-health-stderr.log</string>
|
|
27
|
-
</dict>
|
|
28
|
-
</plist>
|