@adaptic/maestro 1.9.2 → 1.9.4
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/lib/claude-bin.mjs +70 -0
- package/package.json +1 -1
- package/scripts/daemon/agent-daemon.mjs +558 -0
- package/scripts/daemon/cadence-consumer.mjs +7 -47
- package/scripts/daemon/classifier.mjs +5 -3
- package/scripts/daemon/dispatcher.mjs +6 -2
- package/scripts/daemon/maestro-daemon.mjs +53 -24
- package/scripts/daemon/responder.mjs +5 -2
- package/scripts/daemon/sophie-daemon.mjs +11 -552
- package/scripts/local-triggers/generate-plists.sh +7 -2
- package/scripts/local-triggers/generate-plists.test.mjs +6 -1
|
@@ -59,6 +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
63
|
import { getCadenceDef } from "./cadence-handlers.mjs";
|
|
63
64
|
|
|
64
65
|
// ---------------------------------------------------------------------------
|
|
@@ -104,39 +105,10 @@ function defaultLogger(entry) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
* stuck in ravi-ai's DLQ. This resolver returns the first existing
|
|
112
|
-
* candidate among:
|
|
113
|
-
*
|
|
114
|
-
* 1. $CLAUDE_BIN env var (if set + executable)
|
|
115
|
-
* 2. ~/.local/bin/claude (default Claude Code install path)
|
|
116
|
-
* 3. /opt/homebrew/bin/claude (homebrew on Apple Silicon)
|
|
117
|
-
* 4. /usr/local/bin/claude (homebrew on Intel)
|
|
118
|
-
* 5. /usr/bin/claude
|
|
119
|
-
*
|
|
120
|
-
* Falls back to bare "claude" so the spawn's own error stays informative
|
|
121
|
-
* when nothing is found.
|
|
122
|
-
*/
|
|
123
|
-
let _resolvedClaude = null;
|
|
124
|
-
function resolveClaudeBin() {
|
|
125
|
-
if (_resolvedClaude) return _resolvedClaude;
|
|
126
|
-
const envOverride = process.env.CLAUDE_BIN;
|
|
127
|
-
const candidates = [
|
|
128
|
-
envOverride,
|
|
129
|
-
join(homedir(), ".local/bin/claude"),
|
|
130
|
-
"/opt/homebrew/bin/claude",
|
|
131
|
-
"/usr/local/bin/claude",
|
|
132
|
-
"/usr/bin/claude",
|
|
133
|
-
].filter(Boolean);
|
|
134
|
-
for (const c of candidates) {
|
|
135
|
-
if (existsSync(c)) { _resolvedClaude = c; return c; }
|
|
136
|
-
}
|
|
137
|
-
_resolvedClaude = "claude"; // last-resort; spawn will report ENOENT
|
|
138
|
-
return _resolvedClaude;
|
|
139
|
-
}
|
|
108
|
+
// Claude binary resolution moved to lib/claude-bin.mjs (shared by
|
|
109
|
+
// dispatcher, responder, and this consumer). See that file for the
|
|
110
|
+
// candidate search order.
|
|
111
|
+
const resolveClaudeBin = sharedResolveClaude;
|
|
140
112
|
|
|
141
113
|
/**
|
|
142
114
|
* Spawn a sub-session running the cadence's trigger prompt and resolve
|
|
@@ -164,24 +136,12 @@ function realSpawnSession({ agentRoot, cadence, promptPath, timeoutMs, log }) {
|
|
|
164
136
|
|
|
165
137
|
const bin = resolveClaudeBin();
|
|
166
138
|
const args = ["--print", "--dangerously-skip-permissions", body];
|
|
167
|
-
//
|
|
168
|
-
// can still be found. launchd's bare env strips /opt/homebrew/bin etc.
|
|
169
|
-
const augmentedPath = [
|
|
170
|
-
process.env.PATH || "",
|
|
171
|
-
`${homedir()}/.local/bin`,
|
|
172
|
-
"/opt/homebrew/bin",
|
|
173
|
-
"/opt/homebrew/sbin",
|
|
174
|
-
"/usr/local/bin",
|
|
175
|
-
"/usr/bin",
|
|
176
|
-
"/bin",
|
|
177
|
-
"/usr/sbin",
|
|
178
|
-
"/sbin",
|
|
179
|
-
].filter(Boolean).join(":");
|
|
139
|
+
// PATH augmented via lib/claude-bin.mjs so subsession can find jq/node.
|
|
180
140
|
const env = {
|
|
181
141
|
...process.env,
|
|
182
142
|
AGENT_ROOT: agentRoot,
|
|
183
143
|
AGENT_DIR: agentRoot,
|
|
184
|
-
PATH: augmentedPath,
|
|
144
|
+
PATH: augmentedPath(),
|
|
185
145
|
};
|
|
186
146
|
// Auth handling. Claude Code authenticates via macOS Keychain
|
|
187
147
|
// (OAuth from the user's Pro/Max subscription) when no API key is
|
|
@@ -102,8 +102,9 @@ function loadAgentRegistry() {
|
|
|
102
102
|
|
|
103
103
|
const ANTHROPIC_MODEL = "claude-haiku-4-5-20251001";
|
|
104
104
|
const OPENAI_MODEL = "gpt-4o-mini";
|
|
105
|
-
//
|
|
106
|
-
|
|
105
|
+
// Resolve claude against the agent's PATH (not launchd's bare env).
|
|
106
|
+
import { resolveClaudeBin, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
107
|
+
const CLAUDE_BIN = resolveClaudeBin();
|
|
107
108
|
const CLAUDE_CLI_TIMEOUT_MS = 30_000;
|
|
108
109
|
|
|
109
110
|
// ── System prompt shared by both LLM classifiers ────────────────────────────
|
|
@@ -321,7 +322,8 @@ async function runClaudeCLI(systemPrompt, userPrompt) {
|
|
|
321
322
|
stdio: ["pipe", "pipe", "pipe"],
|
|
322
323
|
// Force claude CLI onto keychain OAuth (Max subscription); strip any
|
|
323
324
|
// stale ANTHROPIC_API_KEY/AUTH_TOKEN inherited from the daemon env.
|
|
324
|
-
|
|
325
|
+
// Augment PATH so spawned subsessions find homebrew/nvm binaries.
|
|
326
|
+
env: { ...process.env, PATH: augmentedPath(), ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "" },
|
|
325
327
|
});
|
|
326
328
|
|
|
327
329
|
let stdout = "";
|
|
@@ -9,7 +9,10 @@ import { releaseLock, releaseThreadLock, releaseRequestClaim, claimItem, release
|
|
|
9
9
|
import { recordSession } from "./health.mjs";
|
|
10
10
|
|
|
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
|
+
// env). Without this, every daemon-spawned `claude --print` exits ENOENT.
|
|
14
|
+
import { resolveClaudeBin, augmentedPath } from "../../lib/claude-bin.mjs";
|
|
15
|
+
const CLAUDE_BIN = resolveClaudeBin();
|
|
13
16
|
const MAX_CONCURRENT = parseInt(process.env.DAEMON_MAX_CONCURRENT || "10", 10);
|
|
14
17
|
const RESERVED_INBOX_SLOTS = 3; // Always keep 3 slots free for real-time inbox items
|
|
15
18
|
|
|
@@ -281,9 +284,10 @@ function spawnSession(entry) {
|
|
|
281
284
|
// to the keychain OAuth (Max subscription) per CEO directive 2026-04-27.
|
|
282
285
|
// A stale ANTHROPIC_API_KEY in the daemon's inherited env will otherwise
|
|
283
286
|
// override the OAuth token and cause "Invalid API key" failures.
|
|
287
|
+
// PATH is augmented so the spawn finds homebrew/nvm tools (jq, node, etc).
|
|
284
288
|
const proc = spawn(CLAUDE_BIN, args, {
|
|
285
289
|
cwd: AGENT_REPO_DIR,
|
|
286
|
-
env: { ...process.env, ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "" },
|
|
290
|
+
env: { ...process.env, PATH: augmentedPath(), ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "" },
|
|
287
291
|
stdio: ["ignore", "pipe", "pipe"],
|
|
288
292
|
});
|
|
289
293
|
|
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
// Maestro Daemon — Reactive event-driven message processor
|
|
4
4
|
// =============================================================================
|
|
5
5
|
//
|
|
6
|
-
// Generic entry point that loads agent identity from config/agent.
|
|
6
|
+
// Generic entry point that loads agent identity from config/agent.json and
|
|
7
7
|
// delegates to the core daemon logic. This file is agent-name-agnostic.
|
|
8
8
|
//
|
|
9
|
+
// Lifecycle:
|
|
10
|
+
// 1. Honour .emergency-stop BEFORE doing anything (don't acquire singleton
|
|
11
|
+
// lock, don't start consumer, don't import the core daemon). Stops the
|
|
12
|
+
// launchd restart treadmill cold.
|
|
13
|
+
// 2. Acquire the daemon singleton lock so only one instance runs.
|
|
14
|
+
// 3. Start the cadence consumer (state/cadence-bus/ drain loop).
|
|
15
|
+
// 4. Import the core daemon (agent-daemon.mjs canonical, with fallbacks
|
|
16
|
+
// to legacy sophie-daemon.mjs or <firstName>-daemon.mjs for back-compat).
|
|
17
|
+
//
|
|
9
18
|
// Run: node scripts/daemon/maestro-daemon.mjs
|
|
10
|
-
// Install: launchd plist with KeepAlive:
|
|
19
|
+
// Install: launchd plist with KeepAlive.SuccessfulExit: false (clean exits
|
|
20
|
+
// during emergency-stop should NOT trigger restart; non-zero exits should).
|
|
11
21
|
// =============================================================================
|
|
12
22
|
|
|
13
|
-
// The core daemon implementation is in sophie-daemon.mjs by default, but
|
|
14
|
-
// older agent repos may have a renamed copy at <firstname>-daemon.mjs (a
|
|
15
|
-
// pre-1.7 convention). This entry-point auto-detects the correct file so
|
|
16
|
-
// the launchd plist stays stable regardless of agent identity history.
|
|
17
|
-
|
|
18
23
|
import { resolve, dirname, join } from "node:path";
|
|
19
24
|
import { fileURLToPath } from "node:url";
|
|
20
25
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -22,25 +27,44 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
22
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
28
|
const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
|
|
24
29
|
|
|
25
|
-
// Set AGENT_DIR as env var so all modules can use it
|
|
26
30
|
process.env.AGENT_DIR = AGENT_DIR;
|
|
27
31
|
process.env.AGENT_ROOT = AGENT_DIR;
|
|
28
32
|
|
|
29
|
-
//
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 1. Emergency-stop gate
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// If the operator dropped .emergency-stop, we exit cleanly (code 0). With
|
|
37
|
+
// KeepAlive.SuccessfulExit:false this means launchd will NOT restart us
|
|
38
|
+
// until the flag is lifted + the plist is reloaded by resume-operations.sh.
|
|
39
|
+
// Older plists with plain KeepAlive:true will still restart, so we hold
|
|
40
|
+
// the process for 30s before exiting to give launchd's ThrottleInterval
|
|
41
|
+
// something to throttle.
|
|
42
|
+
if (existsSync(join(AGENT_DIR, ".emergency-stop"))) {
|
|
43
|
+
console.error("[DAEMON] .emergency-stop flag present — refusing to start.");
|
|
44
|
+
console.error("[DAEMON] Lift with: scripts/resume-operations.sh");
|
|
45
|
+
await new Promise((r) => setTimeout(r, 30_000));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// 2. Singleton guard
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
30
52
|
try {
|
|
31
53
|
const { acquireLock } = await import(resolve(process.env.HOME, "maestro/lib/singleton.js"));
|
|
32
54
|
if (!acquireLock("daemon")) {
|
|
33
55
|
console.log("[DAEMON] Already running — exiting");
|
|
34
56
|
process.exit(0);
|
|
35
57
|
}
|
|
36
|
-
} catch { /* maestro singleton not available */ }
|
|
58
|
+
} catch { /* maestro singleton not available — proceed without lock */ }
|
|
37
59
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// 3. Cadence consumer (cadence bus drain loop)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Start alongside the reactive event loop. This is the single persistent
|
|
64
|
+
// owner of cadence housekeeping — launchd plists enqueue cadence ticks
|
|
65
|
+
// onto state/cadence-bus/ and the consumer drains them here, either
|
|
66
|
+
// handling them inline or escalating to a sub-session when warranted.
|
|
67
|
+
// Failure to start the consumer must NOT take the reactive daemon down.
|
|
44
68
|
try {
|
|
45
69
|
const { startConsumer } = await import("./cadence-consumer.mjs");
|
|
46
70
|
const consumer = startConsumer({ agentRoot: AGENT_DIR });
|
|
@@ -55,14 +79,19 @@ try {
|
|
|
55
79
|
// Reactive daemon continues. Doctor / healthcheck will surface this.
|
|
56
80
|
}
|
|
57
81
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// 4. Core daemon import
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Resolve the core daemon module. Try, in order:
|
|
86
|
+
// 1. ./agent-daemon.mjs — canonical filename (1.9.4+)
|
|
87
|
+
// 2. ./sophie-daemon.mjs — legacy canonical (pre-1.9.4)
|
|
88
|
+
// 3. ./<firstName>-daemon.mjs — even older per-agent rename
|
|
89
|
+
// 4. The first scripts/daemon/*-daemon.mjs that isn't this file
|
|
62
90
|
function resolveCoreDaemon() {
|
|
63
|
-
const localCandidates = [
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
const localCandidates = [
|
|
92
|
+
resolve(__dirname, "agent-daemon.mjs"),
|
|
93
|
+
resolve(__dirname, "sophie-daemon.mjs"),
|
|
94
|
+
];
|
|
66
95
|
|
|
67
96
|
try {
|
|
68
97
|
const agentJson = join(AGENT_DIR, "config", "agent.json");
|
|
@@ -90,7 +119,7 @@ function resolveCoreDaemon() {
|
|
|
90
119
|
|
|
91
120
|
const coreDaemon = resolveCoreDaemon();
|
|
92
121
|
if (!coreDaemon) {
|
|
93
|
-
console.error("[DAEMON] could not locate a core daemon module under scripts/daemon/. Expected
|
|
122
|
+
console.error("[DAEMON] could not locate a core daemon module under scripts/daemon/. Expected agent-daemon.mjs (canonical) or sophie-daemon.mjs / <firstName>-daemon.mjs (legacy).");
|
|
94
123
|
process.exit(78);
|
|
95
124
|
}
|
|
96
125
|
// Import and run the daemon (handles its own .env loading).
|
|
@@ -25,7 +25,9 @@ import { routingKey as deriveRoutingKey, createRouter } from "./lib/session-rout
|
|
|
25
25
|
|
|
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";
|
|
30
|
+
const CLAUDE_BIN = resolveClaudeBin();
|
|
29
31
|
const CLAUDE_CLI_TIMEOUT_MS = 60_000;
|
|
30
32
|
const SESSION_REGISTRY_PATH = join(AGENT_REPO_DIR, "state", "daemon", "session-router-registry.json");
|
|
31
33
|
|
|
@@ -140,7 +142,8 @@ function runClaudeCLI(systemPrompt, userPrompt, model, opts = {}) {
|
|
|
140
142
|
stdio: ["pipe", "pipe", "pipe"],
|
|
141
143
|
// Force claude CLI onto keychain OAuth (Max subscription); strip any
|
|
142
144
|
// stale ANTHROPIC_API_KEY/AUTH_TOKEN inherited from the daemon env.
|
|
143
|
-
|
|
145
|
+
// Augment PATH so the subsession finds homebrew/nvm tools.
|
|
146
|
+
env: { ...process.env, PATH: augmentedPath(), ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "" },
|
|
144
147
|
});
|
|
145
148
|
|
|
146
149
|
let stdout = "";
|