@adaptic/maestro 1.9.5 → 1.10.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/.env.example +9 -0
- package/bin/maestro.mjs +92 -0
- package/framework-features.json +10 -0
- package/lib/claude-bin.mjs +27 -0
- package/package.json +2 -2
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/daemon/cadence-consumer.mjs +2 -2
- package/scripts/daemon/classifier.mjs +2 -1
- package/scripts/daemon/dispatcher.mjs +2 -1
- package/scripts/daemon/responder.mjs +2 -1
- 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/lib/claude-bin.mjs
CHANGED
|
@@ -49,6 +49,33 @@ export function resolveClaudeBin() {
|
|
|
49
49
|
return _resolved;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Default args every daemon-spawned `claude --print` should receive on
|
|
54
|
+
* top of the prompt + model selection. Currently:
|
|
55
|
+
*
|
|
56
|
+
* --strict-mcp-config
|
|
57
|
+
* Skip every globally-installed Claude Code MCP plugin (Serena,
|
|
58
|
+
* Playwright, etc). Daemon spawns don't need IDE-grade code
|
|
59
|
+
* intelligence; they need to write a Slack reply / draft an email
|
|
60
|
+
* / classify an item. Loading MCPs wastes 3-5s per spawn AND opens
|
|
61
|
+
* a browser dashboard tab per session (Serena does this).
|
|
62
|
+
*
|
|
63
|
+
* --bare (opt-in via DAEMON_BARE_MODE=1)
|
|
64
|
+
* Skip hooks, LSP, plugin sync, auto-memory, keychain reads. Even
|
|
65
|
+
* more minimal. Use when you specifically want a stateless spawn —
|
|
66
|
+
* but it disables Keychain OAuth, so MAESTRO_PREFER_SUBSCRIPTION_AUTH
|
|
67
|
+
* callers should NOT set this.
|
|
68
|
+
*
|
|
69
|
+
* Operators who want full MCP support on daemon spawns can set
|
|
70
|
+
* DAEMON_LOAD_MCPS=1 in .env; the spawn sites will skip these flags.
|
|
71
|
+
*/
|
|
72
|
+
export function daemonClaudeArgs() {
|
|
73
|
+
if (process.env.DAEMON_LOAD_MCPS === "1") return [];
|
|
74
|
+
const args = ["--strict-mcp-config"];
|
|
75
|
+
if (process.env.DAEMON_BARE_MODE === "1") args.unshift("--bare");
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
/**
|
|
53
80
|
* Build a PATH suitable for child processes spawned from a daemon.
|
|
54
81
|
* launchd strips PATH down to /usr/bin:/bin; this returns a string that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptic/maestro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"always-build-npm": true,
|
|
48
48
|
"scripts": {
|
|
49
|
-
"test": "node --test lib/cadence-bus.test.mjs scripts/cadence/enqueue-cadence-tick.test.mjs scripts/daemon/cadence-consumer.test.mjs scripts/daemon/dispatcher-cooldown.test.mjs scripts/daemon/lib/session-router.test.mjs scripts/local-triggers/generate-plists.test.mjs bin/maestro.test.mjs",
|
|
49
|
+
"test": "node --test lib/cadence-bus.test.mjs scripts/cadence/enqueue-cadence-tick.test.mjs scripts/daemon/cadence-consumer.test.mjs scripts/daemon/dispatcher-cooldown.test.mjs scripts/daemon/lib/session-router.test.mjs scripts/local-triggers/generate-plists.test.mjs scripts/poller/slack-socket-mode.test.mjs bin/maestro.test.mjs",
|
|
50
50
|
"test:cadence": "node --test lib/cadence-bus.test.mjs scripts/cadence/enqueue-cadence-tick.test.mjs scripts/daemon/cadence-consumer.test.mjs",
|
|
51
51
|
"test:cli": "node --test bin/maestro.test.mjs",
|
|
52
52
|
"test:plists": "node --test scripts/local-triggers/generate-plists.test.mjs",
|
|
@@ -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
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
logBusEvent,
|
|
60
60
|
busDepth,
|
|
61
61
|
} from "../../lib/cadence-bus.mjs";
|
|
62
|
-
import { resolveClaudeBin as sharedResolveClaude, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
62
|
+
import { resolveClaudeBin as sharedResolveClaude, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
63
63
|
import { getCadenceDef } from "./cadence-handlers.mjs";
|
|
64
64
|
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
@@ -135,7 +135,7 @@ function realSpawnSession({ agentRoot, cadence, promptPath, timeoutMs, log }) {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const bin = resolveClaudeBin();
|
|
138
|
-
const args = ["--print", "--dangerously-skip-permissions", body];
|
|
138
|
+
const args = ["--print", "--dangerously-skip-permissions", ...daemonClaudeArgs(), body];
|
|
139
139
|
// PATH augmented via lib/claude-bin.mjs so subsession can find jq/node.
|
|
140
140
|
const env = {
|
|
141
141
|
...process.env,
|
|
@@ -103,7 +103,7 @@ function loadAgentRegistry() {
|
|
|
103
103
|
const ANTHROPIC_MODEL = "claude-haiku-4-5-20251001";
|
|
104
104
|
const OPENAI_MODEL = "gpt-4o-mini";
|
|
105
105
|
// Resolve claude against the agent's PATH (not launchd's bare env).
|
|
106
|
-
import { resolveClaudeBin, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
106
|
+
import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
107
107
|
const CLAUDE_BIN = resolveClaudeBin();
|
|
108
108
|
const CLAUDE_CLI_TIMEOUT_MS = 30_000;
|
|
109
109
|
|
|
@@ -314,6 +314,7 @@ async function runClaudeCLI(systemPrompt, userPrompt) {
|
|
|
314
314
|
const args = [
|
|
315
315
|
"--print",
|
|
316
316
|
"--dangerously-skip-permissions",
|
|
317
|
+
...daemonClaudeArgs(),
|
|
317
318
|
"--model", ANTHROPIC_MODEL,
|
|
318
319
|
"--append-system-prompt", systemPrompt,
|
|
319
320
|
];
|
|
@@ -11,7 +11,7 @@ import { recordSession } from "./health.mjs";
|
|
|
11
11
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
12
12
|
// Resolve the claude binary against the agent's PATH (not launchd's bare
|
|
13
13
|
// env). Without this, every daemon-spawned `claude --print` exits ENOENT.
|
|
14
|
-
import { resolveClaudeBin, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
14
|
+
import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
15
15
|
const CLAUDE_BIN = resolveClaudeBin();
|
|
16
16
|
const MAX_CONCURRENT = parseInt(process.env.DAEMON_MAX_CONCURRENT || "10", 10);
|
|
17
17
|
const RESERVED_INBOX_SLOTS = 3; // Always keep 3 slots free for real-time inbox items
|
|
@@ -318,6 +318,7 @@ function spawnSession(entry) {
|
|
|
318
318
|
const args = [
|
|
319
319
|
"--print",
|
|
320
320
|
"--dangerously-skip-permissions",
|
|
321
|
+
...daemonClaudeArgs(),
|
|
321
322
|
"--model", model,
|
|
322
323
|
prompt,
|
|
323
324
|
];
|
|
@@ -26,7 +26,7 @@ import { routingKey as deriveRoutingKey, createRouter } from "./lib/session-rout
|
|
|
26
26
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
27
27
|
const SONNET_MODEL = "claude-sonnet-4-6";
|
|
28
28
|
// Resolve claude against the agent's PATH (not launchd's bare env).
|
|
29
|
-
import { resolveClaudeBin, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
29
|
+
import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
|
|
30
30
|
const CLAUDE_BIN = resolveClaudeBin();
|
|
31
31
|
const CLAUDE_CLI_TIMEOUT_MS = 60_000;
|
|
32
32
|
const SESSION_REGISTRY_PATH = join(AGENT_REPO_DIR, "state", "daemon", "session-router-registry.json");
|
|
@@ -129,6 +129,7 @@ function runClaudeCLI(systemPrompt, userPrompt, model, opts = {}) {
|
|
|
129
129
|
const args = [
|
|
130
130
|
"--print",
|
|
131
131
|
"--dangerously-skip-permissions",
|
|
132
|
+
...daemonClaudeArgs(),
|
|
132
133
|
"--model", model,
|
|
133
134
|
"--append-system-prompt", systemPrompt,
|
|
134
135
|
];
|
|
@@ -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"),
|