@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.
- package/bin/maestro.mjs +15 -3
- package/package.json +1 -1
- package/plugins/maestro-skills/skills/board-deck.md +2 -2
- package/plugins/maestro-skills/skills/decision-brief.md +6 -6
- package/plugins/maestro-skills/skills/draft-comms.md +9 -9
- package/plugins/maestro-skills/skills/evening-wrap.md +2 -2
- package/plugins/maestro-skills/skills/hiring-triage.md +4 -4
- package/plugins/maestro-skills/skills/inbox-triage.md +5 -5
- package/plugins/maestro-skills/skills/morning-brief.md +4 -4
- package/plugins/maestro-skills/skills/pipeline-review.md +2 -2
- package/plugins/maestro-skills/skills/regulatory-status.md +2 -2
- package/plugins/maestro-skills/skills/schedule-meeting.md +3 -3
- package/plugins/maestro-skills/skills/slack-followup.md +5 -5
- package/plugins/maestro-skills/skills/weekly-memo.md +5 -5
- package/scaffold/CLAUDE.md +21 -0
- package/scripts/daemon/classifier.mjs +21 -5
- package/scripts/daemon/maestro-daemon.mjs +46 -7
- package/scripts/hooks/block-mcp-slack-send.sh +1 -1
- package/scripts/huddle/audio-bridge.mjs +17 -17
- package/scripts/huddle/boot-slack-cdp.sh +1 -1
- package/scripts/huddle/huddle-controller.mjs +3 -3
- package/scripts/huddle/huddle-server.mjs +21 -7
- package/scripts/huddle/launch-slack.sh +2 -2
- package/scripts/huddle/package-lock.json +2 -2
- package/scripts/huddle/package.json +2 -2
- package/scripts/huddle/setup-audio.sh +6 -6
- package/scripts/huddle/start-call.mjs +2 -2
- package/scripts/huddle/test-pipeline.mjs +2 -2
- package/scripts/local-triggers/generate-plists.sh +15 -1
- package/scripts/local-triggers/generate-plists.test.mjs +9 -2
- package/scripts/parse-voice-transcript.mjs +4 -9
- package/scripts/poller/gmail-poller.mjs +8 -2
- package/scripts/poller/intra-session-check.mjs +4 -3
- package/scripts/poller-launchd/install.sh +48 -10
- package/scripts/pre_draft_lookup.py +2 -2
- package/scripts/self-optimization/compute-metrics.py +23 -2
- package/scripts/setup/boot-claude-session.sh +14 -5
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +3 -3
- package/scripts/watchdog/force-reboot.sh +3 -3
- package/scripts/watchdog/memory-watchdog.sh +11 -5
- package/workflows/daily/applicant-triage.yaml +3 -3
- package/workflows/daily/comms-triage.yaml +1 -1
- package/workflows/daily/morning-brief.yaml +1 -1
- package/workflows/daily/slack-followup-sweep.yaml +1 -1
- package/workflows/weekly/hiring-review.yaml +3 -3
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Maestro Huddle — Audio Bridge
|
|
4
4
|
*
|
|
5
|
-
* Bidirectional audio pipeline between Slack huddles and
|
|
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
|
|
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; //
|
|
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
|
|
201
|
-
// This prevents the STT from transcribing
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
380
|
-
window.
|
|
381
|
-
window.
|
|
382
|
-
window.
|
|
383
|
-
window.
|
|
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
|
|
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(), `
|
|
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
|
-
#
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
#
|
|
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 "
|
|
210
|
+
log " Maestro Huddle — Slack Launcher"
|
|
211
211
|
log " $(date)"
|
|
212
212
|
log "=========================================="
|
|
213
213
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
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": "
|
|
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": "
|
|
2
|
+
"name": "maestro-huddle",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "
|
|
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
|
-
#
|
|
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
|
|
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 (
|
|
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
|
|
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/
|
|
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 "
|
|
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
|
|
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] || "
|
|
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
|
-
*
|
|
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("
|
|
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
|
-
#
|
|
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
|
|
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,
|
|
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 —
|
|
35
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
176
|
+
principalSlug,
|
|
176
177
|
ch.name,
|
|
177
178
|
ceoMsgs[0].text,
|
|
178
|
-
`
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
37
|
-
echo "Check status: launchctl list | grep
|
|
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("
|
|
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 "
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
24
|
-
AGENT_DIR="${1:?Agent directory required as first argument (e.g.
|
|
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 '║
|
|
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
|