@adaptic/maestro 1.8.0 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,13 +10,14 @@
10
10
  // Install: launchd plist with KeepAlive: true
11
11
  // =============================================================================
12
12
 
13
- // The core daemon implementation lives in sophie-daemon.mjs (filename
14
- // preserved for launchd-plist compatibility; the content is fully
15
- // agent-neutral and reads identity from config/agent.json).
16
- // It uses AGENT_DIR (resolved from this file's location) as the base path.
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
17
 
18
- import { resolve, dirname } from "node:path";
18
+ import { resolve, dirname, join } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
20
21
 
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
@@ -54,5 +55,43 @@ try {
54
55
  // Reactive daemon continues. Doctor / healthcheck will surface this.
55
56
  }
56
57
 
57
- // Import and run the daemon (handles its own .env loading)
58
- await import("./sophie-daemon.mjs");
58
+ // Locate the core daemon module. Try, in order:
59
+ // 1. ./sophie-daemon.mjs — canonical filename (post-Phase-2.5 SOT)
60
+ // 2. ./<firstName>-daemon.mjs — legacy rename from init-maestro Phase 1
61
+ // 3. The first scripts/daemon/*-daemon.mjs that isn't this file
62
+ function resolveCoreDaemon() {
63
+ const localCandidates = [];
64
+ const canonical = resolve(__dirname, "sophie-daemon.mjs");
65
+ localCandidates.push(canonical);
66
+
67
+ try {
68
+ const agentJson = join(AGENT_DIR, "config", "agent.json");
69
+ if (existsSync(agentJson)) {
70
+ const { firstName } = JSON.parse(readFileSync(agentJson, "utf-8"));
71
+ if (firstName && typeof firstName === "string") {
72
+ const lower = firstName.toLowerCase();
73
+ localCandidates.push(resolve(__dirname, `${lower}-daemon.mjs`));
74
+ }
75
+ }
76
+ } catch { /* identity unavailable — fall through */ }
77
+
78
+ for (const p of localCandidates) if (existsSync(p)) return p;
79
+
80
+ // Final fallback: scan the directory for *-daemon.mjs (excluding self).
81
+ try {
82
+ for (const name of readdirSync(__dirname)) {
83
+ if (name === "maestro-daemon.mjs") continue;
84
+ if (name.endsWith("-daemon.mjs")) return resolve(__dirname, name);
85
+ }
86
+ } catch { /* */ }
87
+
88
+ return null;
89
+ }
90
+
91
+ const coreDaemon = resolveCoreDaemon();
92
+ if (!coreDaemon) {
93
+ console.error("[DAEMON] could not locate a core daemon module under scripts/daemon/. Expected sophie-daemon.mjs or <firstName>-daemon.mjs.");
94
+ process.exit(78);
95
+ }
96
+ // Import and run the daemon (handles its own .env loading).
97
+ await import(coreDaemon);
@@ -36,14 +36,38 @@ MAESTRO_PLIST_ARCH_VERSION="1"
36
36
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
37
  AGENT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
38
38
  PLIST_DIR="$SCRIPT_DIR/plists"
39
- AGENT_CONFIG="$AGENT_DIR/config/agent.ts"
40
39
 
41
- # Extract agent first name from config/agent.ts
42
- if [ -f "$AGENT_CONFIG" ]; then
43
- AGENT_FIRST=$(grep "firstName:" "$AGENT_CONFIG" | head -1 | sed "s/.*firstName:[[:space:]]*['\"]//; s/['\"].*//" | tr '[:upper:]' '[:lower:]')
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:]')
44
68
  fi
45
69
 
46
- # Fall back to directory name if config not set or UNCONFIGURED
70
+ # Fall back to directory name if config not set or UNCONFIGURED.
47
71
  if [ -z "$AGENT_FIRST" ] || [ "$AGENT_FIRST" = "unconfigured" ]; then
48
72
  AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
49
73
  fi
@@ -244,13 +268,27 @@ generate_trigger_plist "meeting-prep" "" "900"
244
268
  # 6. Meeting action capture (every 30 minutes)
245
269
  generate_trigger_plist "meeting-action-capture" "" "1800"
246
270
 
247
- # 7. Midday sweep (daily at 12:00 local)
271
+ # 7a. Daily morning brief (06:30 local)
272
+ generate_trigger_plist "daily-morning-brief" \
273
+ " <key>Hour</key>
274
+ <integer>6</integer>
275
+ <key>Minute</key>
276
+ <integer>30</integer>"
277
+
278
+ # 7b. Midday sweep (daily at 12:00 local)
248
279
  generate_trigger_plist "daily-midday-sweep" \
249
280
  " <key>Hour</key>
250
281
  <integer>12</integer>
251
282
  <key>Minute</key>
252
283
  <integer>0</integer>"
253
284
 
285
+ # 7c. Daily evening wrap (18:00 local)
286
+ generate_trigger_plist "daily-evening-wrap" \
287
+ " <key>Hour</key>
288
+ <integer>18</integer>
289
+ <key>Minute</key>
290
+ <integer>0</integer>"
291
+
254
292
  # 8. Weekly hiring (Monday 09:00)
255
293
  generate_trigger_plist "weekly-hiring" \
256
294
  " <key>Weekday</key>
@@ -95,13 +95,20 @@ function listPlists(agentRoot) {
95
95
  // Tests
96
96
  // ---------------------------------------------------------------------------
97
97
 
98
- test("generator emits 13 plists with the agent's first name", async () => {
98
+ test("generator emits 15 plists with the agent's first name", async () => {
99
+ // Inventory (as of cadence-bus v1):
100
+ // daemon, poll-relay,
101
+ // inbox-processor, backlog-executor, meeting-prep, meeting-action-capture,
102
+ // daily-morning-brief, daily-midday-sweep, daily-evening-wrap,
103
+ // weekly-hiring, weekly-priorities, weekly-execution,
104
+ // weekly-engineering-health, weekly-strategic-memo,
105
+ // quarterly-self-assessment.
99
106
  const root = await makeAgent("alice");
100
107
  try {
101
108
  const r = runGenerator(root);
102
109
  assert.equal(r.status, 0, r.stderr);
103
110
  const plists = listPlists(root);
104
- assert.equal(plists.length, 13);
111
+ assert.equal(plists.length, 15);
105
112
  for (const p of plists) {
106
113
  assert.match(p, /^ai\.adaptic\.alice-/);
107
114
  }
@@ -167,6 +174,75 @@ test("daemon plist remains a KeepAlive job (not a cadence enqueue)", async () =>
167
174
  } finally { await rmRoot(root); }
168
175
  });
169
176
 
177
+ test("generator reads firstName from agent.json when agent.ts is type-only (SOT layout)", async () => {
178
+ // Reproduce the SOT layout where agent.ts only declares interfaces
179
+ // (containing `firstName: string;` as a type, not a value) and the
180
+ // actual identity lives in agent.json. The legacy resolver matched the
181
+ // interface line and produced labels like
182
+ // `ai.adaptic. firstname: string;-…` — guard against regression.
183
+ const root = join(
184
+ tmpdir(),
185
+ `plist-sot-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
186
+ );
187
+ await fsp.mkdir(root, { recursive: true });
188
+ // Manually wire the same fixture-builder used by other tests, except
189
+ // overwrite agent.ts to look like a type-only TypeScript file and add
190
+ // a real agent.json.
191
+ await fsp.mkdir(join(root, "config"), { recursive: true });
192
+ writeFileSync(
193
+ join(root, "config/agent.ts"),
194
+ `// Type declarations only — value lives in agent.json
195
+ export interface PrincipalConfig { firstName: string; }
196
+ export interface AgentConfig { firstName: string; }
197
+ `,
198
+ );
199
+ writeFileSync(
200
+ join(root, "config/agent.json"),
201
+ JSON.stringify({ firstName: "Sigrid", lastName: "Test" }) + "\n",
202
+ );
203
+ writeFileSync(join(root, "package.json"), '{"name":"t"}');
204
+
205
+ await fsp.mkdir(join(root, "scripts/local-triggers"), { recursive: true });
206
+ await fsp.mkdir(join(root, "scripts/daemon"), { recursive: true });
207
+ await fsp.mkdir(join(root, "scripts/cadence"), { recursive: true });
208
+ // Same set of helper scripts as makeAgent().
209
+ const { copyFileSync } = await import("node:fs");
210
+ copyFileSync(GENERATOR, join(root, "scripts/local-triggers/generate-plists.sh"));
211
+ copyFileSync(
212
+ join(MAESTRO_ROOT, "scripts/local-triggers/run-trigger.sh"),
213
+ join(root, "scripts/local-triggers/run-trigger.sh"),
214
+ );
215
+ copyFileSync(
216
+ join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper.sh"),
217
+ join(root, "scripts/daemon/launchd-wrapper.sh"),
218
+ );
219
+ copyFileSync(
220
+ join(MAESTRO_ROOT, "scripts/daemon/launchd-wrapper-generic.sh"),
221
+ join(root, "scripts/daemon/launchd-wrapper-generic.sh"),
222
+ );
223
+ copyFileSync(
224
+ join(MAESTRO_ROOT, "scripts/cadence/launchd-cadence-wrapper.sh"),
225
+ join(root, "scripts/cadence/launchd-cadence-wrapper.sh"),
226
+ );
227
+ copyFileSync(
228
+ join(MAESTRO_ROOT, "scripts/cadence/enqueue-cadence-tick.mjs"),
229
+ join(root, "scripts/cadence/enqueue-cadence-tick.mjs"),
230
+ );
231
+ await fsp.chmod(join(root, "scripts/local-triggers/generate-plists.sh"), 0o755);
232
+
233
+ try {
234
+ const r = runGenerator(root);
235
+ assert.equal(r.status, 0, r.stderr);
236
+ const plists = listPlists(root);
237
+ assert.ok(plists.length >= 1);
238
+ for (const p of plists) {
239
+ assert.match(p, /^ai\.adaptic\.sigrid-/, `expected "sigrid" label, got: ${p}`);
240
+ assert.ok(!p.includes("string"),
241
+ `label leaked the TypeScript type: ${p}`);
242
+ }
243
+ } finally { await rmRoot(root); }
244
+ });
245
+
170
246
  test("regeneration clears stale plists", async () => {
171
247
  const root = await makeAgent("frank");
172
248
  try {