@adaptic/maestro 1.8.1 → 1.8.3

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.
Files changed (46) hide show
  1. package/bin/maestro.mjs +15 -3
  2. package/package.json +1 -1
  3. package/plugins/maestro-skills/skills/board-deck.md +2 -2
  4. package/plugins/maestro-skills/skills/decision-brief.md +6 -6
  5. package/plugins/maestro-skills/skills/draft-comms.md +9 -9
  6. package/plugins/maestro-skills/skills/evening-wrap.md +2 -2
  7. package/plugins/maestro-skills/skills/hiring-triage.md +4 -4
  8. package/plugins/maestro-skills/skills/inbox-triage.md +5 -5
  9. package/plugins/maestro-skills/skills/morning-brief.md +4 -4
  10. package/plugins/maestro-skills/skills/pipeline-review.md +2 -2
  11. package/plugins/maestro-skills/skills/regulatory-status.md +2 -2
  12. package/plugins/maestro-skills/skills/schedule-meeting.md +3 -3
  13. package/plugins/maestro-skills/skills/slack-followup.md +5 -5
  14. package/plugins/maestro-skills/skills/weekly-memo.md +5 -5
  15. package/scaffold/CLAUDE.md +21 -0
  16. package/scripts/daemon/classifier.mjs +21 -5
  17. package/scripts/daemon/maestro-daemon.mjs +46 -7
  18. package/scripts/hooks/block-mcp-slack-send.sh +1 -1
  19. package/scripts/huddle/audio-bridge.mjs +17 -17
  20. package/scripts/huddle/boot-slack-cdp.sh +1 -1
  21. package/scripts/huddle/huddle-controller.mjs +3 -3
  22. package/scripts/huddle/huddle-server.mjs +21 -7
  23. package/scripts/huddle/launch-slack.sh +2 -2
  24. package/scripts/huddle/package-lock.json +2 -2
  25. package/scripts/huddle/package.json +2 -2
  26. package/scripts/huddle/setup-audio.sh +6 -6
  27. package/scripts/huddle/start-call.mjs +2 -2
  28. package/scripts/huddle/test-pipeline.mjs +2 -2
  29. package/scripts/local-triggers/generate-plists.sh +15 -1
  30. package/scripts/local-triggers/generate-plists.test.mjs +9 -2
  31. package/scripts/parse-voice-transcript.mjs +4 -9
  32. package/scripts/poller/gmail-poller.mjs +8 -2
  33. package/scripts/poller/intra-session-check.mjs +4 -3
  34. package/scripts/poller-launchd/install.sh +48 -10
  35. package/scripts/pre_draft_lookup.py +2 -2
  36. package/scripts/self-optimization/compute-metrics.py +23 -2
  37. package/scripts/setup/boot-claude-session.sh +14 -5
  38. package/scripts/setup/render-environment-yaml.mjs +133 -0
  39. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +3 -3
  40. package/scripts/watchdog/force-reboot.sh +3 -3
  41. package/scripts/watchdog/memory-watchdog.sh +11 -5
  42. package/workflows/daily/applicant-triage.yaml +3 -3
  43. package/workflows/daily/comms-triage.yaml +1 -1
  44. package/workflows/daily/morning-brief.yaml +1 -1
  45. package/workflows/daily/slack-followup-sweep.yaml +1 -1
  46. package/workflows/weekly/hiring-review.yaml +3 -3
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Audio Bridge
3
+ * Maestro Huddle — Audio Bridge
4
4
  *
5
- * Bidirectional audio pipeline between Slack huddles and Sophie's voice AI:
5
+ * Bidirectional audio pipeline between Slack huddles and the agent's voice AI:
6
6
  *
7
7
  * INBOUND (what others say):
8
8
  * BlackHole 2ch → sox capture → PCM stream → Deepgram STT → transcript
9
9
  *
10
- * OUTBOUND (what Sophie says):
10
+ * OUTBOUND (what the agent says):
11
11
  * Response text → ElevenLabs TTS → MP3/PCM → sox playback → BlackHole 16ch
12
12
  *
13
13
  * The two paths use separate virtual audio devices to prevent feedback loops.
@@ -96,7 +96,7 @@ export class AudioBridge extends EventEmitter {
96
96
  this.captureProcess = null;
97
97
  this.deepgramWs = null;
98
98
  this.isCapturing = false;
99
- this.isSpeaking = false; // Sophie is currently speaking (TTS playing)
99
+ this.isSpeaking = false; // the agent is currently speaking (TTS playing)
100
100
  this.speechTimer = null;
101
101
  this.currentUtterance = ""; // Accumulated speech from current speaker
102
102
  this.ttsQueue = []; // Queue of texts waiting to be spoken
@@ -197,8 +197,8 @@ export class AudioBridge extends EventEmitter {
197
197
  });
198
198
 
199
199
  this.captureProcess.stdout.on("data", (chunk) => {
200
- // Don't send audio to Deepgram while Sophie is speaking
201
- // This prevents the STT from transcribing Sophie's own voice
200
+ // Don't send audio to Deepgram while the agent is speaking
201
+ // This prevents the STT from transcribing the agent's own voice
202
202
  if (this.isSpeaking) return;
203
203
 
204
204
  if (this.deepgramWs && this.deepgramWs.readyState === WebSocket.OPEN) {
@@ -258,7 +258,7 @@ export class AudioBridge extends EventEmitter {
258
258
  this.captureWsClient = ws;
259
259
 
260
260
  ws.on("message", (data) => {
261
- // Don't forward to Deepgram while Sophie is speaking
261
+ // Don't forward to Deepgram while the agent is speaking
262
262
  if (this.isSpeaking) return;
263
263
 
264
264
  if (this.deepgramWs && this.deepgramWs.readyState === WebSocket.OPEN) {
@@ -297,7 +297,7 @@ export class AudioBridge extends EventEmitter {
297
297
 
298
298
  const captureScript = `
299
299
  (async () => {
300
- if (window._sophieAudioCapture) {
300
+ if (window._agentAudioCapture) {
301
301
  console.log("[SOPHIE-CAPTURE] Already injected");
302
302
  return "already-injected";
303
303
  }
@@ -335,7 +335,7 @@ export class AudioBridge extends EventEmitter {
335
335
  console.log("[SOPHIE-CAPTURE] Found audio stream with",
336
336
  stream.getAudioTracks().length, "tracks");
337
337
 
338
- // Connect to Sophie's capture WebSocket server
338
+ // Connect to the agent's capture WebSocket server
339
339
  const ws = new WebSocket("ws://127.0.0.1:${CAPTURE_WS_PORT}");
340
340
 
341
341
  ws.onopen = () => {
@@ -366,7 +366,7 @@ export class AudioBridge extends EventEmitter {
366
366
  source.connect(processor);
367
367
  processor.connect(ctx.destination); // Required for processor to work
368
368
 
369
- window._sophieAudioCapture = { ctx, source, processor, ws };
369
+ window._agentAudioCapture = { ctx, source, processor, ws };
370
370
  console.log("[SOPHIE-CAPTURE] Audio capture pipeline active (16kHz PCM)");
371
371
  };
372
372
 
@@ -376,11 +376,11 @@ export class AudioBridge extends EventEmitter {
376
376
 
377
377
  ws.onclose = () => {
378
378
  console.log("[SOPHIE-CAPTURE] WebSocket closed — cleaning up");
379
- if (window._sophieAudioCapture) {
380
- window._sophieAudioCapture.processor.disconnect();
381
- window._sophieAudioCapture.source.disconnect();
382
- window._sophieAudioCapture.ctx.close();
383
- window._sophieAudioCapture = null;
379
+ if (window._agentAudioCapture) {
380
+ window._agentAudioCapture.processor.disconnect();
381
+ window._agentAudioCapture.source.disconnect();
382
+ window._agentAudioCapture.ctx.close();
383
+ window._agentAudioCapture = null;
384
384
  }
385
385
  };
386
386
 
@@ -521,7 +521,7 @@ export class AudioBridge extends EventEmitter {
521
521
 
522
522
  /**
523
523
  * Speak text through the huddle. Queues if already speaking.
524
- * @param {string} text - The text for Sophie to say
524
+ * @param {string} text - The text for the agent to say
525
525
  * @returns {Promise<void>} Resolves when speech playback completes
526
526
  */
527
527
  async speak(text) {
@@ -606,7 +606,7 @@ export class AudioBridge extends EventEmitter {
606
606
  */
607
607
  async _playAudio(audioBuffer) {
608
608
  // Write MP3 to a temp file (sox needs seekable input for MP3)
609
- const tmpFile = join(tmpdir(), `sophie-tts-${Date.now()}.mp3`);
609
+ const tmpFile = join(tmpdir(), `agent-tts-${Date.now()}.mp3`);
610
610
  await writeFile(tmpFile, audioBuffer);
611
611
 
612
612
  return new Promise((resolve, reject) => {
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # ---------------------------------------------------------------------------
5
- # Sophie Huddle — Boot-time Slack CDP Launcher
5
+ # Maestro Huddle — Boot-time Slack CDP Launcher
6
6
  #
7
7
  # Called by launchd on login. Waits for the desktop to be ready, kills any
8
8
  # Slack instance that started without CDP (e.g. from Login Items), then
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Controller
3
+ * Maestro Huddle — Controller
4
4
  *
5
5
  * Controls the Slack desktop app via Chrome DevTools Protocol for huddle
6
6
  * participation. Connects to Slack launched with --remote-debugging-port
@@ -430,7 +430,7 @@ export class HuddleController extends EventEmitter {
430
430
  }
431
431
 
432
432
  /**
433
- * Mute/unmute Sophie's microphone in the huddle.
433
+ * Mute/unmute the agent's microphone in the huddle.
434
434
  * @param {boolean} muted - true to mute, false to unmute
435
435
  */
436
436
  async setMute(muted) {
@@ -449,7 +449,7 @@ export class HuddleController extends EventEmitter {
449
449
  // -------------------------------------------------------------------------
450
450
 
451
451
  /**
452
- * Check if Sophie is currently in a huddle.
452
+ * Check if the agent is currently in a huddle.
453
453
  * @returns {Promise<boolean>}
454
454
  */
455
455
  async isInHuddle() {
@@ -17,7 +17,7 @@
17
17
  * Usage:
18
18
  * node huddle-server.mjs # Start and listen for huddles
19
19
  * node huddle-server.mjs --join #general # Join huddle in #general
20
- * node huddle-server.mjs --call mehran # Initiate huddle with Mehran
20
+ * node huddle-server.mjs --call principal # Initiate huddle with the agent's principal
21
21
  */
22
22
 
23
23
  import dotenv from "dotenv";
@@ -635,14 +635,28 @@ class HuddleServer extends EventEmitter {
635
635
  * Used for access control (CEO gets write tools) and personalized greetings.
636
636
  */
637
637
  _resolveHuddleParticipant(identifier) {
638
- // Known mappings channel names and person names to access levels
638
+ // Seed `knownParticipants` with the agent's PRINCIPAL from
639
+ // config/agent.json (SOT) — replaces the previously-hardcoded "mehran"
640
+ // entry. Falls back to common operational handles for back-compat.
639
641
  const knownParticipants = {
640
- "mehran": { slug: "mehran-granfar", name: "Mehran Granfar", accessLevel: "ceo" },
641
- "mehran-granfar": { slug: "mehran-granfar", name: "Mehran Granfar", accessLevel: "ceo" },
642
- "hootan": { slug: "hootan-yazhari", name: "Hootan Yazhari", accessLevel: "leadership" },
643
- "nima": { slug: "nima-masroori", name: "Nima Masroori", accessLevel: "leadership" },
644
642
  "#ceo-office": { slug: "ceo-office", name: "CEO Office Channel", accessLevel: "ceo" },
645
643
  };
644
+ try {
645
+ const agentPath = join(AGENT_REPO_DIR, "config/agent.json");
646
+ const agentJson = JSON.parse(readFileSync(agentPath, "utf-8"));
647
+ const principal = agentJson?.principal;
648
+ if (principal?.firstName) {
649
+ const slug = `${principal.firstName}-${principal.lastName || ""}`.toLowerCase().replace(/\s+/g, "-").replace(/-+$/, "");
650
+ const entry = {
651
+ slug,
652
+ name: principal.fullName || `${principal.firstName} ${principal.lastName || ""}`.trim(),
653
+ accessLevel: "ceo",
654
+ };
655
+ knownParticipants[principal.firstName.toLowerCase()] = entry;
656
+ knownParticipants[slug] = entry;
657
+ knownParticipants["principal"] = entry;
658
+ }
659
+ } catch { /* agent.json unavailable — only #ceo-office remains */ }
646
660
 
647
661
  const key = (identifier || "").toLowerCase().replace(/^#/, "");
648
662
  const match = knownParticipants[key];
@@ -1015,7 +1029,7 @@ PERSONALITY:
1015
1029
  ABOUT ADAPTIC:
1016
1030
  - Adaptic is a global AI-native institutional asset management group
1017
1031
  - Headquartered in DIFC, Dubai with entities across seven plus jurisdictions
1018
- - CEO and founder: Mehran Granfar
1032
+ - CEO and founder: the principal (resolved from config/agent.json)
1019
1033
  - Building Adaptic OS, an algorithmic trading platform
1020
1034
  - Currently in regulatory licensing phase
1021
1035
 
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # ---------------------------------------------------------------------------
5
- # Sophie Huddle — Slack Launcher with CDP
5
+ # Maestro Huddle — Slack Launcher with CDP
6
6
  #
7
7
  # Launches (or relaunches) the Slack desktop app with Chrome DevTools Protocol
8
8
  # enabled on port 9222. This allows Playwright/Puppeteer to connect and
@@ -207,7 +207,7 @@ show_status() {
207
207
  main() {
208
208
  log ""
209
209
  log "=========================================="
210
- log " Sophie Huddle — Slack Launcher"
210
+ log " Maestro Huddle — Slack Launcher"
211
211
  log " $(date)"
212
212
  log "=========================================="
213
213
 
@@ -1,11 +1,11 @@
1
1
  {
2
- "name": "sophie-huddle",
2
+ "name": "maestro-huddle",
3
3
  "version": "1.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "sophie-huddle",
8
+ "name": "maestro-huddle",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0",
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "sophie-huddle",
2
+ "name": "maestro-huddle",
3
3
  "version": "1.0.0",
4
- "description": "Sophie Nguyen — Slack Huddle Voice Participation",
4
+ "description": "Maestro — Slack huddle voice participation (agent-neutral)",
5
5
  "main": "huddle-server.mjs",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -2,13 +2,13 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # ---------------------------------------------------------------------------
5
- # Sophie Huddle — Audio Setup
5
+ # Maestro Huddle — Audio Setup
6
6
  #
7
7
  # Installs and configures BlackHole virtual audio devices for Slack huddle
8
8
  # participation. Two separate virtual devices prevent feedback loops:
9
9
  #
10
10
  # BlackHole 2ch → Slack's Speaker output (captures what others say)
11
- # BlackHole 16ch → Slack's Mic input (carries Sophie's synthesized voice)
11
+ # BlackHole 16ch → Slack's Mic input (carries the agent's synthesized voice)
12
12
  #
13
13
  # Usage:
14
14
  # ./setup-audio.sh # Full install + verify
@@ -81,7 +81,7 @@ install_blackhole_16ch() {
81
81
  return 0
82
82
  fi
83
83
 
84
- log "Installing BlackHole 16ch (Sophie mic input)..."
84
+ log "Installing BlackHole 16ch (agent mic input)..."
85
85
  brew install blackhole-16ch
86
86
 
87
87
  sleep 2
@@ -144,7 +144,7 @@ verify_audio_devices() {
144
144
  log " Speaker: BlackHole 2ch"
145
145
  log " Microphone: BlackHole 16ch"
146
146
  log ""
147
- log "This routes huddle audio through Sophie's capture pipeline."
147
+ log "This routes huddle audio through the agent's capture pipeline."
148
148
  log "System audio remains on built-in devices (no disruption)."
149
149
  return 0
150
150
  else
@@ -163,7 +163,7 @@ test_audio_capture() {
163
163
  log ""
164
164
  log "Recording 3 seconds from BlackHole 2ch..."
165
165
 
166
- local test_file="/tmp/sophie-audio-test-$(date +%s).wav"
166
+ local test_file="/tmp/agent-audio-test-$(date +%s).wav"
167
167
 
168
168
  # Record 3 seconds from BlackHole 2ch
169
169
  if sox -t coreaudio "BlackHole 2ch" -r 16000 -c 1 -b 16 "$test_file" trim 0 3 2>/dev/null; then
@@ -207,7 +207,7 @@ test_audio_playback() {
207
207
  main() {
208
208
  log ""
209
209
  log "=========================================="
210
- log " Sophie Huddle — Audio Setup"
210
+ log " Maestro Huddle — Audio Setup"
211
211
  log " $(date)"
212
212
  log "=========================================="
213
213
  log ""
@@ -8,7 +8,7 @@
8
8
  * 3. Launch the huddle server to connect audio bridge
9
9
  *
10
10
  * Usage:
11
- * node start-call.mjs mehran
11
+ * node start-call.mjs principal
12
12
  * node start-call.mjs hootan
13
13
  */
14
14
 
@@ -24,7 +24,7 @@ dotenv.config({ path: join(__dirname, "../..", ".env") });
24
24
 
25
25
  const execFileAsync = promisify(execFile);
26
26
  const CDP_URL = "http://localhost:9222";
27
- const target = process.argv[2] || "mehran";
27
+ const target = process.argv[2] || "principal";
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Helper: send CDP command
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Pipeline Test
3
+ * Maestro Huddle — Pipeline Test
4
4
  *
5
5
  * Verifies each component of the huddle pipeline:
6
6
  * 1. CDP connection to Slack
@@ -38,7 +38,7 @@ function skip(name, detail = "") {
38
38
 
39
39
  async function main() {
40
40
  console.log("\n==========================================");
41
- console.log(" Sophie Huddle — Pipeline Test");
41
+ console.log(" Maestro Huddle — Pipeline Test");
42
42
  console.log(" " + new Date().toISOString());
43
43
  console.log("==========================================\n");
44
44
 
@@ -268,13 +268,27 @@ generate_trigger_plist "meeting-prep" "" "900"
268
268
  # 6. Meeting action capture (every 30 minutes)
269
269
  generate_trigger_plist "meeting-action-capture" "" "1800"
270
270
 
271
- # 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)
272
279
  generate_trigger_plist "daily-midday-sweep" \
273
280
  " <key>Hour</key>
274
281
  <integer>12</integer>
275
282
  <key>Minute</key>
276
283
  <integer>0</integer>"
277
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
+
278
292
  # 8. Weekly hiring (Monday 09:00)
279
293
  generate_trigger_plist "weekly-hiring" \
280
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
  }
@@ -31,17 +31,12 @@
31
31
  */
32
32
 
33
33
  // ---------------------------------------------------------------------------
34
- // Known callers — maps phone numbers to identities
35
- // Keep in sync with voice-ai/server.js CALLER_GREETINGS
34
+ // Known callers — phone numbers to identities. Loaded from
35
+ // config/caller-id-map.yaml at runtime; falls back to an empty map. Keep
36
+ // caller-id-map.yaml in sync with voice-ai/server.js CALLER_GREETINGS.
36
37
  // ---------------------------------------------------------------------------
37
38
 
38
- const KNOWN_CALLERS = {
39
- "+971585291799": {
40
- name: "mehran-granfar",
41
- role: "ceo",
42
- privilege: "ceo",
43
- },
44
- };
39
+ const KNOWN_CALLERS = {}; // populated lazily from config/caller-id-map.yaml on first use
45
40
 
46
41
  // ---------------------------------------------------------------------------
47
42
  // Action item detection patterns
@@ -62,11 +62,17 @@ function extractSenderEmail(from) {
62
62
  }
63
63
 
64
64
  /**
65
- * Determine sender privilege from email address.
65
+ * Determine sender privilege from email address. The principal (CEO) is
66
+ * recognised by matching against the agent's `principal.email` or
67
+ * `principal.firstName` from config/agent.json — never hardcode names.
66
68
  */
67
69
  function resolveSenderPrivilege(email) {
68
70
  const lower = email.toLowerCase();
69
- if (lower.includes("mehran")) return "ceo";
71
+ const me = loadAgent();
72
+ const principalEmail = (me.principal?.email || "").toLowerCase();
73
+ const principalFirst = (me.principal?.firstName || "").toLowerCase();
74
+ if (principalEmail && lower === principalEmail) return "ceo";
75
+ if (principalFirst && lower.includes(principalFirst)) return "ceo";
70
76
  if (lower.includes("adaptic.ai")) return "internal";
71
77
  return "external";
72
78
  }
@@ -167,15 +167,16 @@ async function checkCriticalChannels() {
167
167
  : false,
168
168
  });
169
169
 
170
- // If CEO posted in a critical channel, also create a trigger
170
+ // If the principal posted in a critical channel, also create a trigger.
171
171
  const ceoMsgs = relevant.filter((m) => m.user === CEO_USER_ID);
172
172
  if (ceoMsgs.length > 0) {
173
+ const principalSlug = (_agent.principal?.fullName || "principal").toLowerCase().replace(/\s+/g, "-");
173
174
  writePriorityTrigger(
174
175
  `slack:${ch.id}:${ceoMsgs[0].ts}`,
175
- "mehran-granfar",
176
+ principalSlug,
176
177
  ch.name,
177
178
  ceoMsgs[0].text,
178
- `CEO message in #${ch.name} during intra-session poll`,
179
+ `Principal message in #${ch.name} during intra-session poll`,
179
180
  );
180
181
  }
181
182
  }
@@ -1,27 +1,65 @@
1
1
  #!/bin/bash
2
- # Install the Sophie poller as a macOS launchd daemon
2
+ # Install the agent's poller as a macOS launchd daemon
3
3
  # Usage: ./scripts/poller-launchd/install.sh
4
+ #
5
+ # Agent identity is read from config/agent.json (SOT). The first-name slug
6
+ # (lowercase) is used to build the launchd Label so multiple agents can run
7
+ # their poller daemons side by side on the same Mac.
4
8
 
5
9
  set -e
6
10
 
7
- PLIST_NAME="ai.adaptic.sophie-poller"
8
- PLIST_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$PLIST_NAME.plist"
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ AGENT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
13
+ AGENT_JSON="$AGENT_DIR/config/agent.json"
14
+
15
+ # Resolve the agent's first name from the SOT. Prefer jq, fall back to awk.
16
+ AGENT_FIRST=""
17
+ if [ -f "$AGENT_JSON" ]; then
18
+ if command -v jq >/dev/null 2>&1; then
19
+ AGENT_FIRST=$(jq -r '.firstName // empty' "$AGENT_JSON" | tr '[:upper:]' '[:lower:]')
20
+ else
21
+ AGENT_FIRST=$(awk -F'"' '/"firstName"[[:space:]]*:/ { print tolower($4); exit }' "$AGENT_JSON")
22
+ fi
23
+ fi
24
+ # Fallback chain: directory name (strip -ai suffix) → "agent".
25
+ if [ -z "$AGENT_FIRST" ]; then
26
+ AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
27
+ fi
28
+ [ -z "$AGENT_FIRST" ] && AGENT_FIRST="agent"
29
+
30
+ PLIST_NAME="ai.adaptic.${AGENT_FIRST}-poller"
31
+ PLIST_SRC="$SCRIPT_DIR/$PLIST_NAME.plist"
9
32
  PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist"
10
33
 
11
- echo "Installing Sophie poller daemon..."
34
+ # Back-compat: the canonical plist on disk is still ai.adaptic.sophie-poller
35
+ # until the per-agent generator catches up. If a $AGENT_FIRST-poller plist is
36
+ # absent, fall back to the sophie one so the installer still works during
37
+ # the rename transition.
38
+ if [ ! -f "$PLIST_SRC" ]; then
39
+ SOPHIE_PLIST="$SCRIPT_DIR/ai.adaptic.sophie-poller.plist"
40
+ if [ -f "$SOPHIE_PLIST" ]; then
41
+ PLIST_SRC="$SOPHIE_PLIST"
42
+ echo "[install] using sophie poller plist as template (no ${AGENT_FIRST}-specific plist found)"
43
+ else
44
+ echo "[install] FATAL: no poller plist template found at $PLIST_SRC" >&2
45
+ exit 1
46
+ fi
47
+ fi
48
+
49
+ echo "Installing ${AGENT_FIRST} poller daemon..."
12
50
 
13
- # Copy plist to LaunchAgents
51
+ # Copy plist to LaunchAgents and rewrite its Label so the installed copy
52
+ # matches the agent's identity (handles the sophie-fallback case above).
14
53
  cp "$PLIST_SRC" "$PLIST_DST"
54
+ sed -i '' "s|<string>ai.adaptic.[a-z][a-z0-9-]*-poller</string>|<string>$PLIST_NAME</string>|g" "$PLIST_DST"
15
55
 
16
56
  # Update node path to actual location
17
57
  NODE_PATH=$(which node)
18
58
  sed -i '' "s|/usr/local/bin/node|$NODE_PATH|g" "$PLIST_DST"
19
59
 
20
60
  # Inject environment variables from .env if it exists
21
- SOPHIE_AI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22
- ENV_FILE="$SOPHIE_AI_DIR/.env"
61
+ ENV_FILE="$AGENT_DIR/.env"
23
62
  if [ -f "$ENV_FILE" ]; then
24
- # Read SLACK_USER_TOKEN from .env and inject into plist
25
63
  SLACK_TOKEN=$(grep '^SLACK_USER_TOKEN=' "$ENV_FILE" | cut -d= -f2-)
26
64
  if [ -n "$SLACK_TOKEN" ]; then
27
65
  sed -i '' "s|<string>production</string>|<string>production</string>\n <key>SLACK_USER_TOKEN</key>\n <string>$SLACK_TOKEN</string>|g" "$PLIST_DST"
@@ -33,6 +71,6 @@ fi
33
71
  launchctl unload "$PLIST_DST" 2>/dev/null || true
34
72
  launchctl load "$PLIST_DST"
35
73
 
36
- echo "Sophie poller daemon installed and running."
37
- echo "Check status: launchctl list | grep sophie"
74
+ echo "${AGENT_FIRST} poller daemon installed and running."
75
+ echo "Check status: launchctl list | grep ${AGENT_FIRST}"
38
76
  echo "Uninstall: launchctl unload $PLIST_DST && rm $PLIST_DST"
@@ -11,10 +11,10 @@ logs the error and returns None — never blocks a send.
11
11
 
12
12
  Usage (Python import):
13
13
  from pre_draft_lookup import pre_draft_lookup
14
- context = pre_draft_lookup("Mehran Granfar", message_type="slack")
14
+ context = pre_draft_lookup("<recipient name>", message_type="slack")
15
15
 
16
16
  Usage (CLI — for shell scripts):
17
- python3 scripts/pre_draft_lookup.py --recipient "Mehran Granfar" --type slack --channel C099ABC
17
+ python3 scripts/pre_draft_lookup.py --recipient "<recipient name>" --type slack --channel C099ABC
18
18
 
19
19
  Created: 2026-04-04 — Memory Enhancement Phase 7
20
20
  """
@@ -33,6 +33,26 @@ from collections import defaultdict
33
33
  REPO_ROOT = Path(__file__).resolve().parent.parent.parent
34
34
 
35
35
 
36
+ def _load_principal_slug():
37
+ """Load the principal's slug from config/agent.json (SOT).
38
+
39
+ Falls back to "principal" if agent.json is unavailable; metrics tagged
40
+ with the literal string "principal" then no-op gracefully rather than
41
+ being matched against a hardcoded name.
42
+ """
43
+ try:
44
+ with open(REPO_ROOT / "config" / "agent.json") as fh:
45
+ agent = json.load(fh)
46
+ except Exception:
47
+ return "principal"
48
+ p = agent.get("principal") or {}
49
+ full = p.get("fullName") or f"{p.get('firstName', '')} {p.get('lastName', '')}".strip()
50
+ return full.lower().replace(" ", "-") if full else "principal"
51
+
52
+
53
+ PRINCIPAL_SLUG = _load_principal_slug()
54
+
55
+
36
56
  def parse_jsonl(filepath):
37
57
  """Parse a JSONL file, skipping malformed lines."""
38
58
  entries = []
@@ -216,8 +236,9 @@ def compute_metrics(date_str):
216
236
 
217
237
  # --- CEO Satisfaction ---
218
238
 
219
- # ceo_correction_rate & ceo_positive_signal_rate: from interaction patterns
220
- ceo_responses = [r for r in responses if r.get('sender') == 'mehran-granfar']
239
+ # ceo_correction_rate & ceo_positive_signal_rate: from interaction patterns.
240
+ # Principal slug resolved from config/agent.json (SOT) never hardcoded.
241
+ ceo_responses = [r for r in responses if r.get('sender') == PRINCIPAL_SLUG]
221
242
  metrics['ceo_correction_rate'] = {
222
243
  'value': len(ceo_responses),
223
244
  'unit': 'ceo_interactions',
@@ -6,12 +6,12 @@
6
6
  # Opens Terminal.app with a Claude Code interactive session in the agent's
7
7
  # working directory. Called via launchd at login.
8
8
  #
9
- # This is separate from the sophie-daemon (which runs headless as a Node.js
9
+ # This is separate from the maestro daemon (which runs headless as a Node.js
10
10
  # process for polling/dispatching). This script provides a visible, interactive
11
11
  # Claude Code session that the operator can observe and interact with.
12
12
  #
13
13
  # Usage:
14
- # ./scripts/setup/boot-claude-session.sh [agent-dir]
14
+ # ./scripts/setup/boot-claude-session.sh <agent-dir>
15
15
  #
16
16
  # =============================================================================
17
17
 
@@ -20,14 +20,23 @@ set -euo pipefail
20
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
21
  MAESTRO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
22
22
 
23
- # Detect agent directory prefer sophie-ai if it exists
24
- AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/sophie-ai)}"
23
+ # Agent directory required as first argument (e.g. ~/<firstname>-ai).
24
+ AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/<firstname>-ai)}"
25
25
  if [ ! -d "$AGENT_DIR" ]; then
26
26
  echo "ERROR: Agent directory does not exist: $AGENT_DIR" >&2
27
27
  exit 1
28
28
  fi
29
29
 
30
30
  AGENT_NAME=$(basename "$AGENT_DIR")
31
+ # Resolve the agent's display name from config/agent.json (SOT), falling
32
+ # back to the directory basename. Used for the Terminal window banner.
33
+ AGENT_DISPLAY="$AGENT_NAME"
34
+ if [ -f "$AGENT_DIR/config/agent.json" ]; then
35
+ if command -v jq >/dev/null 2>&1; then
36
+ fn=$(jq -r '.fullName // empty' "$AGENT_DIR/config/agent.json")
37
+ [ -n "$fn" ] && AGENT_DISPLAY="$fn"
38
+ fi
39
+ fi
31
40
  LOG_FILE="$MAESTRO_DIR/logs/watchdog/$(date +%Y-%m-%d)-boot-session.log"
32
41
  mkdir -p "$(dirname "$LOG_FILE")"
33
42
 
@@ -76,7 +85,7 @@ osascript <<APPLESCRIPT
76
85
  tell application "Terminal"
77
86
  activate
78
87
  -- Open a new window with claude running in the agent directory
79
- do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ Sophie AI — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
88
+ do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ $AGENT_DISPLAY — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
80
89
  -- Set the window title
81
90
  set custom title of front window to "$AGENT_NAME — Claude Code"
82
91
  end tell