@adaptic/maestro 1.10.0 → 1.10.2
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/.env.example +9 -0
- package/bin/maestro.mjs +92 -0
- package/framework-features.json +10 -0
- package/package.json +1 -1
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/healthcheck.sh +15 -9
- package/scripts/local-triggers/generate-plists.sh +15 -0
- package/scripts/local-triggers/generate-plists.test.mjs +19 -7
- package/scripts/poller/slack-socket-mode.mjs +739 -0
- package/scripts/poller/slack-socket-mode.test.mjs +688 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
package/.env.example
CHANGED
|
@@ -76,6 +76,15 @@ SLACK_BOT_TOKEN=xoxb-...
|
|
|
76
76
|
# Found at: https://api.slack.com/apps → Basic Information → Signing Secret
|
|
77
77
|
SLACK_SIGNING_SECRET=
|
|
78
78
|
|
|
79
|
+
# OPTIONAL — App-level token (starts with xapp-) for Slack Socket Mode.
|
|
80
|
+
# Required when the slack-socket-mode feature is enabled. Generate it at:
|
|
81
|
+
# https://api.slack.com/apps → your app → "Basic Information" → "App-Level Tokens"
|
|
82
|
+
# Add the scope: connections:write
|
|
83
|
+
# Then also enable Socket Mode and Event Subscriptions (message.channels,
|
|
84
|
+
# message.groups, message.im, message.mpim, app_mention) on the same app.
|
|
85
|
+
# Run `maestro init slack-socket-mode --apply` to walk through the full setup.
|
|
86
|
+
SLACK_APP_LEVEL_TOKEN=
|
|
87
|
+
|
|
79
88
|
|
|
80
89
|
# ─── GMAIL ───────────────────────────────────────────────────────────────────
|
|
81
90
|
#
|
package/bin/maestro.mjs
CHANGED
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
readdirSync,
|
|
21
21
|
statSync,
|
|
22
22
|
lstatSync,
|
|
23
|
+
openSync,
|
|
24
|
+
readSync,
|
|
25
|
+
closeSync,
|
|
23
26
|
} from "node:fs";
|
|
24
27
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
25
28
|
import { createHash } from "node:crypto";
|
|
@@ -1499,6 +1502,95 @@ function doctor() {
|
|
|
1499
1502
|
} else if (preferSubs) {
|
|
1500
1503
|
ok("MAESTRO_PREFER_SUBSCRIPTION_AUTH=1 — using Claude Code subscription (Keychain OAuth)");
|
|
1501
1504
|
}
|
|
1505
|
+
|
|
1506
|
+
// ── Slack Socket Mode ────────────────────────────────────────────────
|
|
1507
|
+
// When SLACK_APP_LEVEL_TOKEN is set, verify the launchd job is loaded
|
|
1508
|
+
// and the listener has connected recently. The check is fully optional:
|
|
1509
|
+
// the framework still works with the legacy 60s poller if the operator
|
|
1510
|
+
// hasn't enabled Socket Mode yet.
|
|
1511
|
+
const socketTokenMatch = env.match(/^SLACK_APP_LEVEL_TOKEN=(.+)$/m);
|
|
1512
|
+
const socketToken = socketTokenMatch ? socketTokenMatch[1].trim().replace(/^["']|["']$/g, "") : "";
|
|
1513
|
+
if (socketToken && socketToken.startsWith("xapp-")) {
|
|
1514
|
+
ok("SLACK_APP_LEVEL_TOKEN configured (Socket Mode candidate)");
|
|
1515
|
+
|
|
1516
|
+
// Resolve the agent's first name the same way generate-plists.sh does
|
|
1517
|
+
// so the label matches whatever is installed in ~/Library/LaunchAgents/.
|
|
1518
|
+
let firstName = "";
|
|
1519
|
+
try {
|
|
1520
|
+
const aj = JSON.parse(readFileSync(join(cwd, "config/agent.json"), "utf-8"));
|
|
1521
|
+
if (aj.firstName) firstName = String(aj.firstName).toLowerCase();
|
|
1522
|
+
} catch { /* fall back */ }
|
|
1523
|
+
if (!firstName) firstName = cwd.split("/").pop().replace(/-ai$/, "").toLowerCase();
|
|
1524
|
+
const socketLabel = `ai.adaptic.${firstName}-slack-socket`;
|
|
1525
|
+
|
|
1526
|
+
// Is the plist installed at ~/Library/LaunchAgents/?
|
|
1527
|
+
const installedDir = join(process.env.HOME || "/", "Library/LaunchAgents");
|
|
1528
|
+
const installedPath = join(installedDir, `${socketLabel}.plist`);
|
|
1529
|
+
if (existsSync(installedPath)) {
|
|
1530
|
+
// Is it actually loaded? launchctl list returns 0 when the label exists.
|
|
1531
|
+
const listRes = spawnSync("launchctl", ["list", socketLabel], { encoding: "utf-8" });
|
|
1532
|
+
if (listRes.status === 0) ok(`launchd job loaded: ${socketLabel}`);
|
|
1533
|
+
else {
|
|
1534
|
+
warn(`Plist installed but launchd job not loaded: ${socketLabel}`);
|
|
1535
|
+
warn(` Fix: launchctl load ${installedPath}`);
|
|
1536
|
+
issues++;
|
|
1537
|
+
}
|
|
1538
|
+
} else {
|
|
1539
|
+
warn(`SLACK_APP_LEVEL_TOKEN set but ${installedPath} missing.`);
|
|
1540
|
+
warn(" Run: node scripts/setup/init-slack-socket-mode.mjs");
|
|
1541
|
+
issues++;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Did the listener log a `hello` envelope recently? The log lives at
|
|
1545
|
+
// logs/polling/<today>-slack-socket.jsonl — scan the last few lines
|
|
1546
|
+
// for a fresh "hello" or "inbox-item-written" entry.
|
|
1547
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1548
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
1549
|
+
let foundRecent = false;
|
|
1550
|
+
let latestTs = null;
|
|
1551
|
+
for (const day of [today, yesterday]) {
|
|
1552
|
+
const logPath = join(cwd, "logs/polling", `${day}-slack-socket.jsonl`);
|
|
1553
|
+
if (!existsSync(logPath)) continue;
|
|
1554
|
+
// Read the tail (last 50KB) to avoid loading huge files.
|
|
1555
|
+
try {
|
|
1556
|
+
const stat = statSync(logPath);
|
|
1557
|
+
const buf = Buffer.alloc(Math.min(stat.size, 50_000));
|
|
1558
|
+
const fd = openSync(logPath, "r");
|
|
1559
|
+
try {
|
|
1560
|
+
const start = Math.max(0, stat.size - buf.length);
|
|
1561
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
1562
|
+
} finally { closeSync(fd); }
|
|
1563
|
+
const tail = buf.toString("utf-8");
|
|
1564
|
+
for (const line of tail.split("\n").reverse()) {
|
|
1565
|
+
if (!line) continue;
|
|
1566
|
+
try {
|
|
1567
|
+
const entry = JSON.parse(line);
|
|
1568
|
+
if (entry.message === "hello — connected to Slack Socket Mode" ||
|
|
1569
|
+
entry.message === "inbox-item-written" ||
|
|
1570
|
+
entry.message === "starting Socket Mode listener") {
|
|
1571
|
+
latestTs = entry.ts;
|
|
1572
|
+
const ageMs = Date.now() - new Date(entry.ts).getTime();
|
|
1573
|
+
if (ageMs >= 0 && ageMs < 60 * 60_000) foundRecent = true;
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
} catch { /* malformed line — skip */ }
|
|
1577
|
+
}
|
|
1578
|
+
if (latestTs) break;
|
|
1579
|
+
} catch { /* unreadable log — skip */ }
|
|
1580
|
+
}
|
|
1581
|
+
if (foundRecent) {
|
|
1582
|
+
const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
|
|
1583
|
+
ok(`Socket Mode listener was active ${ageMin}m ago`);
|
|
1584
|
+
} else if (latestTs) {
|
|
1585
|
+
const ageMin = Math.round((Date.now() - new Date(latestTs).getTime()) / 60_000);
|
|
1586
|
+
warn(`Socket Mode last connect ${ageMin}m ago — may be stalled`);
|
|
1587
|
+
issues++;
|
|
1588
|
+
} else {
|
|
1589
|
+
warn("Socket Mode listener has not produced a connect log yet.");
|
|
1590
|
+
warn(" Check: tail -f logs/polling/$(date +%Y-%m-%d)-slack-socket.jsonl");
|
|
1591
|
+
issues++;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1502
1594
|
} else {
|
|
1503
1595
|
fail(".env file not found — copy from .env.example");
|
|
1504
1596
|
issues++;
|
package/framework-features.json
CHANGED
|
@@ -102,6 +102,16 @@
|
|
|
102
102
|
"command": "node scripts/setup/init-backup.mjs",
|
|
103
103
|
"description": "Configure off-machine backup of state/, knowledge/, outputs/, and rotated logs to GCS or S3. Requires bucket name + credentials. Doctor flags if last successful backup is >24h old once configured."
|
|
104
104
|
}
|
|
105
|
+
},
|
|
106
|
+
"slack-socket-mode": {
|
|
107
|
+
"version": "1",
|
|
108
|
+
"since": "1.10.0",
|
|
109
|
+
"title": "Slack Socket Mode (realtime DM + @mention ingestion)",
|
|
110
|
+
"init": {
|
|
111
|
+
"auto": false,
|
|
112
|
+
"command": "node scripts/setup/init-slack-socket-mode.mjs",
|
|
113
|
+
"description": "Replace the 60s slack-poller cycle with a persistent WebSocket to Slack. Three steps in the Slack admin UI: (1) open the agent's Slack app at api.slack.com/apps; (2) enable Socket Mode and generate an app-level token (xapp-…) with the connections:write scope; (3) enable Event Subscriptions and subscribe to message.channels, message.groups, message.im, message.mpim, and app_mention. The init wizard prompts for the token, appends it to .env, regenerates the launchd plist, and launchctl-loads it. Doctor verifies the token is present, the plist is loaded, and the listener recently connected."
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# launchd-socket-mode-wrapper.sh — Bootstraps env for slack-socket-mode.mjs
|
|
3
|
+
# under launchd.
|
|
4
|
+
#
|
|
5
|
+
# Mirrors launchd-wrapper.sh exactly: launchd's bare env doesn't include
|
|
6
|
+
# HOME, PATH, or AGENT_ROOT, so we hydrate them before exec'ing the
|
|
7
|
+
# Socket Mode listener. Logs land on the external SSD when available
|
|
8
|
+
# (same fallback semantics as the main daemon wrapper).
|
|
9
|
+
#
|
|
10
|
+
# This wrapper is exec'd by ai.adaptic.{firstname}-slack-socket.plist.
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
AGENT_ROOT="$(cd "$(dirname "$0")/../.." && pwd -P)"
|
|
15
|
+
export AGENT_ROOT
|
|
16
|
+
export HOME="${HOME:-/Users/$(whoami)}"
|
|
17
|
+
export USER="${USER:-$(whoami)}"
|
|
18
|
+
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
19
|
+
|
|
20
|
+
# ── SSD redirect ────────────────────────────────────────────────────────────
|
|
21
|
+
# If an external SSD is mounted at /Volumes/{name}, redirect:
|
|
22
|
+
# - Claude Code per-cwd temp (CLAUDE_CODE_TMPDIR)
|
|
23
|
+
# - Listener stdout/stderr (via shell redirection at exec time)
|
|
24
|
+
#
|
|
25
|
+
# Detection mirrors launchd-wrapper.sh — first volume under /Volumes that's
|
|
26
|
+
# not a system mount; MAESTRO_SSD_VOLUME env var overrides if multiple SSDs.
|
|
27
|
+
|
|
28
|
+
SSD_VOLUME="${MAESTRO_SSD_VOLUME:-}"
|
|
29
|
+
if [ -z "$SSD_VOLUME" ]; then
|
|
30
|
+
for v in /Volumes/*-SSD /Volumes/*SSD* /Volumes/maestro-data; do
|
|
31
|
+
if [ -d "$v" ] && [ "$v" != "/Volumes/Macintosh HD" ]; then
|
|
32
|
+
SSD_VOLUME="$v"
|
|
33
|
+
break
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
AGENT_NAME="$(basename "$AGENT_ROOT" | sed 's/-ai$//')"
|
|
39
|
+
SSD_AGENT_ROOT=""
|
|
40
|
+
SSD_WRITABLE=0
|
|
41
|
+
if [ -n "$SSD_VOLUME" ] && [ -d "$SSD_VOLUME" ]; then
|
|
42
|
+
SSD_AGENT_ROOT="$SSD_VOLUME/maestro/$AGENT_NAME"
|
|
43
|
+
if mkdir -p "$SSD_AGENT_ROOT/claude-tmp" "$SSD_AGENT_ROOT/logs/slack-socket" 2>/dev/null && \
|
|
44
|
+
touch "$SSD_AGENT_ROOT/.write-test-$$" 2>/dev/null; then
|
|
45
|
+
rm -f "$SSD_AGENT_ROOT/.write-test-$$"
|
|
46
|
+
SSD_WRITABLE=1
|
|
47
|
+
export CLAUDE_CODE_TMPDIR="$SSD_AGENT_ROOT/claude-tmp"
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
cd "$AGENT_ROOT"
|
|
52
|
+
|
|
53
|
+
# Resolve node binary — prefer nvm, fall back to homebrew, then system.
|
|
54
|
+
NODE_BIN=""
|
|
55
|
+
for candidate in \
|
|
56
|
+
"$HOME/.nvm/versions/node/v24.11.1/bin/node" \
|
|
57
|
+
"$HOME/.nvm/versions/node/v24/bin/node" \
|
|
58
|
+
"$HOME/.nvm/versions/node/v22/bin/node" \
|
|
59
|
+
"$HOME/.nvm/versions/node/v20/bin/node" \
|
|
60
|
+
/opt/homebrew/bin/node \
|
|
61
|
+
/usr/local/bin/node \
|
|
62
|
+
/usr/bin/node; do
|
|
63
|
+
if [ -x "$candidate" ]; then
|
|
64
|
+
NODE_BIN="$candidate"
|
|
65
|
+
break
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
if [ -z "$NODE_BIN" ] && [ -d "$HOME/.nvm/versions/node" ]; then
|
|
69
|
+
NODE_BIN=$(ls -1d "$HOME/.nvm/versions/node"/v*/bin/node 2>/dev/null | sort -V | tail -1)
|
|
70
|
+
fi
|
|
71
|
+
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
|
72
|
+
echo "[slack-socket-wrapper] FATAL: could not find node binary" >&2
|
|
73
|
+
exit 127
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Node 22.4+ is required for the global WebSocket. Warn (don't fail) on
|
|
77
|
+
# older versions — the user might have polyfilled via `--experimental-websocket`
|
|
78
|
+
# or installed the `ws` package as a fallback.
|
|
79
|
+
NODE_VERSION="$("$NODE_BIN" --version 2>/dev/null || echo 'v0.0.0')"
|
|
80
|
+
NODE_MAJOR="$(echo "$NODE_VERSION" | sed -E 's/^v([0-9]+).*/\1/')"
|
|
81
|
+
if [ "$NODE_MAJOR" -lt 22 ] 2>/dev/null; then
|
|
82
|
+
echo "[slack-socket-wrapper] WARNING: Node $NODE_VERSION is older than v22 — global WebSocket may be missing." >&2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Exec the listener. Prefer SSD log path if writable, otherwise fall back
|
|
86
|
+
# to internal disk so the listener stays up even when macOS denies launchd
|
|
87
|
+
# write access to /Volumes/{name}.
|
|
88
|
+
if [ "$SSD_WRITABLE" = "1" ]; then
|
|
89
|
+
LISTENER_LOG="$SSD_AGENT_ROOT/logs/slack-socket/listener-$(date +%Y-%m-%d).log"
|
|
90
|
+
exec "$NODE_BIN" "$AGENT_ROOT/scripts/poller/slack-socket-mode.mjs" >> "$LISTENER_LOG" 2>&1
|
|
91
|
+
else
|
|
92
|
+
LISTENER_LOG="$AGENT_ROOT/logs/polling/slack-socket-$(date +%Y-%m-%d).log"
|
|
93
|
+
mkdir -p "$(dirname "$LISTENER_LOG")" 2>/dev/null || true
|
|
94
|
+
exec "$NODE_BIN" "$AGENT_ROOT/scripts/poller/slack-socket-mode.mjs" >> "$LISTENER_LOG" 2>&1
|
|
95
|
+
fi
|
package/scripts/healthcheck.sh
CHANGED
|
@@ -23,15 +23,21 @@ else
|
|
|
23
23
|
echo "[OK] No emergency stop flag"
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
# Check 2:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
# Check 2: Anthropic auth — either ANTHROPIC_API_KEY set OR subscription
|
|
27
|
+
# auth enabled. Daemons launched under launchd don't inherit the agent's
|
|
28
|
+
# shell env, so a "missing" ANTHROPIC_API_KEY is normal when the operator
|
|
29
|
+
# opted into Claude Code subscription auth (MAESTRO_PREFER_SUBSCRIPTION_AUTH=1
|
|
30
|
+
# in .env strips the var and falls back to Keychain OAuth).
|
|
31
|
+
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
32
|
+
echo "[OK] Environment variable: ANTHROPIC_API_KEY"
|
|
33
|
+
elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(1|true|yes)$" "$SOPHIE_AI_DIR/.env"; then
|
|
34
|
+
echo "[OK] Anthropic auth: subscription mode (.env: MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
|
|
35
|
+
elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^ANTHROPIC_API_KEY=." "$SOPHIE_AI_DIR/.env"; then
|
|
36
|
+
echo "[OK] Anthropic auth: ANTHROPIC_API_KEY in .env"
|
|
37
|
+
else
|
|
38
|
+
echo "[FAIL] Anthropic auth not configured (.env needs either ANTHROPIC_API_KEY or MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
|
|
39
|
+
ERRORS=$((ERRORS + 1))
|
|
40
|
+
fi
|
|
35
41
|
|
|
36
42
|
# Check 3: Key directories exist and are writable
|
|
37
43
|
for dir in logs outputs knowledge/memory config; do
|
|
@@ -261,6 +261,21 @@ generate_plist "ai.adaptic.${AGENT_FIRST}-poll-relay" \
|
|
|
261
261
|
"${AGENT_DIR}/scripts/daemon/launchd-wrapper-generic.sh|${AGENT_DIR}/scripts/poll-slack-events.sh" \
|
|
262
262
|
"" "5" ""
|
|
263
263
|
|
|
264
|
+
# 2b. Slack Socket Mode listener (persistent WebSocket — KeepAlive).
|
|
265
|
+
# Replaces the 60s slack-poller cycle for DMs + @mentions with a
|
|
266
|
+
# realtime WSS connection to Slack. Requires SLACK_APP_LEVEL_TOKEN
|
|
267
|
+
# in .env (the operator generates this via api.slack.com → App-Level
|
|
268
|
+
# Tokens with connections:write scope). The wrapper at
|
|
269
|
+
# scripts/cadence/launchd-socket-mode-wrapper.sh bootstraps env +
|
|
270
|
+
# execs scripts/poller/slack-socket-mode.mjs.
|
|
271
|
+
#
|
|
272
|
+
# KeepAlive uses the same SuccessfulExit:false / Crashed:true semantics
|
|
273
|
+
# as the daemon plist so .emergency-stop (clean exit 0) refuses to
|
|
274
|
+
# restart, but unexpected crashes do trigger a restart treadmill.
|
|
275
|
+
generate_plist "ai.adaptic.${AGENT_FIRST}-slack-socket" \
|
|
276
|
+
"${AGENT_DIR}/scripts/cadence/launchd-socket-mode-wrapper.sh" \
|
|
277
|
+
"" "" "true"
|
|
278
|
+
|
|
264
279
|
# 3. Inbox processor (every 5 minutes)
|
|
265
280
|
generate_trigger_plist "inbox-processor" "" "300"
|
|
266
281
|
|
|
@@ -56,6 +56,10 @@ async function makeAgent(firstName) {
|
|
|
56
56
|
join(MAESTRO_ROOT, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
57
57
|
join(root, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
58
58
|
);
|
|
59
|
+
copyFileSync(
|
|
60
|
+
join(MAESTRO_ROOT, "scripts/cadence/launchd-socket-mode-wrapper.sh"),
|
|
61
|
+
join(root, "scripts/cadence/launchd-socket-mode-wrapper.sh"),
|
|
62
|
+
);
|
|
59
63
|
copyFileSync(
|
|
60
64
|
join(MAESTRO_ROOT, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
61
65
|
join(root, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
@@ -68,6 +72,7 @@ async function makeAgent(firstName) {
|
|
|
68
72
|
"scripts/daemon/launchd-wrapper.sh",
|
|
69
73
|
"scripts/daemon/launchd-wrapper-generic.sh",
|
|
70
74
|
"scripts/cadence/launchd-cadence-wrapper.sh",
|
|
75
|
+
"scripts/cadence/launchd-socket-mode-wrapper.sh",
|
|
71
76
|
]) {
|
|
72
77
|
await fsp.chmod(join(root, p), 0o755);
|
|
73
78
|
}
|
|
@@ -95,9 +100,9 @@ function listPlists(agentRoot) {
|
|
|
95
100
|
// Tests
|
|
96
101
|
// ---------------------------------------------------------------------------
|
|
97
102
|
|
|
98
|
-
test("generator emits
|
|
99
|
-
// Inventory (as of cadence-bus v1):
|
|
100
|
-
// daemon, poll-relay,
|
|
103
|
+
test("generator emits 16 plists with the agent's first name", async () => {
|
|
104
|
+
// Inventory (as of cadence-bus v1 + slack-socket-mode v1):
|
|
105
|
+
// daemon, poll-relay, slack-socket,
|
|
101
106
|
// inbox-processor, backlog-executor, meeting-prep, meeting-action-capture,
|
|
102
107
|
// daily-morning-brief, daily-midday-sweep, daily-evening-wrap,
|
|
103
108
|
// weekly-hiring, weekly-priorities, weekly-execution,
|
|
@@ -108,7 +113,7 @@ test("generator emits 15 plists with the agent's first name", async () => {
|
|
|
108
113
|
const r = runGenerator(root);
|
|
109
114
|
assert.equal(r.status, 0, r.stderr);
|
|
110
115
|
const plists = listPlists(root);
|
|
111
|
-
assert.equal(plists.length,
|
|
116
|
+
assert.equal(plists.length, 16);
|
|
112
117
|
for (const p of plists) {
|
|
113
118
|
assert.match(p, /^ai\.adaptic\.alice-/);
|
|
114
119
|
}
|
|
@@ -134,9 +139,12 @@ test("NO trigger plist invokes run-trigger.sh", async () => {
|
|
|
134
139
|
const dir = join(root, "scripts/local-triggers/plists");
|
|
135
140
|
for (const name of listPlists(root)) {
|
|
136
141
|
const body = readFileSync(join(dir, name), "utf-8");
|
|
137
|
-
// Exception: the daemon/poll-relay plists don't run
|
|
138
|
-
//
|
|
139
|
-
|
|
142
|
+
// Exception: the daemon/poll-relay/slack-socket plists don't run cadence
|
|
143
|
+
// triggers at all (the first two are KeepAlive workers; slack-socket is
|
|
144
|
+
// a KeepAlive WSS listener). The trigger plists must invoke
|
|
145
|
+
// enqueue-cadence-tick.mjs.
|
|
146
|
+
if (name.endsWith("-daemon.plist") || name.endsWith("-poll-relay.plist") ||
|
|
147
|
+
name.endsWith("-slack-socket.plist")) {
|
|
140
148
|
continue;
|
|
141
149
|
}
|
|
142
150
|
assert.ok(!body.includes("run-trigger.sh"),
|
|
@@ -229,6 +237,10 @@ export interface AgentConfig { firstName: string; }
|
|
|
229
237
|
join(MAESTRO_ROOT, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
230
238
|
join(root, "scripts/cadence/launchd-cadence-wrapper.sh"),
|
|
231
239
|
);
|
|
240
|
+
copyFileSync(
|
|
241
|
+
join(MAESTRO_ROOT, "scripts/cadence/launchd-socket-mode-wrapper.sh"),
|
|
242
|
+
join(root, "scripts/cadence/launchd-socket-mode-wrapper.sh"),
|
|
243
|
+
);
|
|
232
244
|
copyFileSync(
|
|
233
245
|
join(MAESTRO_ROOT, "scripts/cadence/enqueue-cadence-tick.mjs"),
|
|
234
246
|
join(root, "scripts/cadence/enqueue-cadence-tick.mjs"),
|